@samitouri / QOS-React / commits / 961eb65b4b

Use unique thread ID for each partial render to access Context (#14182)

* BUG: ReactPartialRenderer / New Context polutes mutable global state The new context API stores the provided values on the shared context instance. When used in a synchronous context, this is not an issue. However when used in an concurrent context this can cause a "push provider" from one react render to have an effect on an unrelated concurrent react render. I've encountered this bug in production when using renderToNodeStream, which asks ReactPartialRenderer for bytes up to a high water mark before yielding. If two Node Streams are created and read from in parallel, the state of one can polute the other. I wrote a failing test to illustrate the conditions under which this happens. I'm also concerned that the experimental concurrent/async React rendering on the client could suffer from the same issue. * Use unique thread ID for each partial render to access Context This first adds an allocator that keeps track of a unique ThreadID index for each currently executing partial renderer. IDs are not just growing but are reused as streams are destroyed. This ensures that IDs are kept nice and compact. This lets us use an "array" for each Context object to store the current values. The look up for these are fast because they're just looking up an offset in a tightly packed "array". I don't use an actual Array object to store the values. Instead, I rely on that VMs (notably V8) treat storage of numeric index property access as a separate "elements" allocation. This lets us avoid an extra indirection. However, we must ensure that these arrays are not holey to preserve this feature. To do that I store the _threadCount on each context (effectively it takes the place of the .length property on an array). This lets us first validate that the context has enough slots before we access the slot. If not, we fill in the slots with the default value.

Sebastian Markbåge committed Nov 9, 2018 at 15:38 UTC 961eb65b4ba5de6bbfb6b8a075a924c284541177
12 files changed +336 -74
packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js
+91
@@ -348,5 +348,96 @@ describe('ReactDOMServerIntegration', () => {
348 await render(<App />, 1);
349 },
350 );
351 +
352 + it('does not pollute parallel node streams', () => {
353 + const LoggedInUser = React.createContext();
354 +
355 + const AppWithUser = user => (
356 + <LoggedInUser.Provider value={user}>
357 + <header>
358 + <LoggedInUser.Consumer>{whoAmI => whoAmI}</LoggedInUser.Consumer>
359 + </header>
360 + <footer>
361 + <LoggedInUser.Consumer>{whoAmI => whoAmI}</LoggedInUser.Consumer>
362 + </footer>
363 + </LoggedInUser.Provider>
364 + );
365 +
366 + const streamAmy = ReactDOMServer.renderToNodeStream(
367 + AppWithUser('Amy'),
368 + ).setEncoding('utf8');
369 + const streamBob = ReactDOMServer.renderToNodeStream(
370 + AppWithUser('Bob'),
371 + ).setEncoding('utf8');
372 +
373 + // Testing by filling the buffer using internal _read() with a small
374 + // number of bytes to avoid a test case which needs to align to a
375 + // highWaterMark boundary of 2^14 chars.
376 + streamAmy._read(20);
377 + streamBob._read(20);
378 + streamAmy._read(20);
379 + streamBob._read(20);
380 +
381 + expect(streamAmy.read()).toBe('<header>Amy</header><footer>Amy</footer>');
382 + expect(streamBob.read()).toBe('<header>Bob</header><footer>Bob</footer>');
383 + });
384 +
385 + it('does not pollute parallel node streams when many are used', () => {
386 + const CurrentIndex = React.createContext();
387 +
388 + const NthRender = index => (
389 + <CurrentIndex.Provider value={index}>
390 + <header>
391 + <CurrentIndex.Consumer>{idx => idx}</CurrentIndex.Consumer>
392 + </header>
393 + <footer>
394 + <CurrentIndex.Consumer>{idx => idx}</CurrentIndex.Consumer>
395 + </footer>
396 + </CurrentIndex.Provider>
397 + );
398 +
399 + let streams = [];
400 +
401 + // Test with more than 32 streams to test that growing the thread count
402 + // works properly.
403 + let streamCount = 34;
404 +
405 + for (let i = 0; i < streamCount; i++) {
406 + streams[i] = ReactDOMServer.renderToNodeStream(
407 + NthRender(i % 2 === 0 ? 'Expected to be recreated' : i),
408 + ).setEncoding('utf8');
409 + }
410 +
411 + // Testing by filling the buffer using internal _read() with a small
412 + // number of bytes to avoid a test case which needs to align to a
413 + // highWaterMark boundary of 2^14 chars.
414 + for (let i = 0; i < streamCount; i++) {
415 + streams[i]._read(20);
416 + }
417 +
418 + // Early destroy every other stream
419 + for (let i = 0; i < streamCount; i += 2) {
420 + streams[i].destroy();
421 + }
422 +
423 + // Recreate those same streams.
424 + for (let i = 0; i < streamCount; i += 2) {
425 + streams[i] = ReactDOMServer.renderToNodeStream(
426 + NthRender(i),
427 + ).setEncoding('utf8');
428 + }
429 +
430 + // Read a bit from all streams again.
431 + for (let i = 0; i < streamCount; i++) {
432 + streams[i]._read(20);
433 + }
434 +
435 + // Assert that all stream rendered the expected output.
436 + for (let i = 0; i < streamCount; i++) {
437 + expect(streams[i].read()).toBe(
438 + '<header>' + i + '</header><footer>' + i + '</footer>',
439 + );
440 + }
441 + });
442 });
443 });
packages/react-dom/src/server/ReactDOMNodeStreamRenderer.js
+5 -1
@@ -18,11 +18,15 @@ class ReactMarkupReadableStream extends Readable {
18 this.partialRenderer = new ReactPartialRenderer(element, makeStaticMarkup);
19 }
20
21 + _destroy() {
22 + this.partialRenderer.destroy();
23 + }
24 +
25 _read(size) {
26 try {
27 this.push(this.partialRenderer.read(size));
28 } catch (err) {
25 - this.emit('error', err);
29 + this.destroy(err);
30 }
31 }
32 }
packages/react-dom/src/server/ReactDOMStringRenderer.js
+12 -4
@@ -14,8 +14,12 @@ import ReactPartialRenderer from './ReactPartialRenderer';
14 */
15 export function renderToString(element) {
16 const renderer = new ReactPartialRenderer(element, false);
17 - const markup = renderer.read(Infinity);
18 - return markup;
17 + try {
18 + const markup = renderer.read(Infinity);
19 + return markup;
20 + } finally {
21 + renderer.destroy();
22 + }
23 }
24
25 /**
@@ -25,6 +29,10 @@ export function renderToString(element) {
29 */
30 export function renderToStaticMarkup(element) {
31 const renderer = new ReactPartialRenderer(element, true);
28 - const markup = renderer.read(Infinity);
29 - return markup;
32 + try {
33 + const markup = renderer.read(Infinity);
34 + return markup;
35 + } finally {
36 + renderer.destroy();
37 + }
38 }
packages/react-dom/src/server/ReactPartialRenderer.js
+35 -67
@@ -7,6 +7,7 @@
7 * @flow
8 */
9
10 +import type {ThreadID} from './ReactThreadIDAllocator';
11 import type {ReactElement} from 'shared/ReactElementType';
12 import type {ReactProvider, ReactContext} from 'shared/ReactTypes';
13
@@ -16,7 +17,6 @@ import getComponentName from 'shared/getComponentName';
17 import lowPriorityWarning from 'shared/lowPriorityWarning';
18 import warning from 'shared/warning';
19 import warningWithoutStack from 'shared/warningWithoutStack';
19 -import checkPropTypes from 'prop-types/checkPropTypes';
20 import describeComponentFrame from 'shared/describeComponentFrame';
21 import ReactSharedInternals from 'shared/ReactSharedInternals';
22 import {
@@ -39,6 +39,12 @@ import {
39 REACT_MEMO_TYPE,
40 } from 'shared/ReactSymbols';
41
42 +import {
43 + emptyObject,
44 + processContext,
45 + validateContextBounds,
46 +} from './ReactPartialRendererContext';
47 +import {allocThreadID, freeThreadID} from './ReactThreadIDAllocator';
48 import {
49 createMarkupForCustomAttribute,
50 createMarkupForProperty,
@@ -50,6 +56,8 @@ import {
56 finishHooks,
57 Dispatcher,
58 DispatcherWithoutHooks,
59 + currentThreadID,
60 + setCurrentThreadID,
61 } from './ReactPartialRendererHooks';
62 import {
63 Namespaces,
@@ -176,7 +184,6 @@ const didWarnAboutBadClass = {};
184 const didWarnAboutDeprecatedWillMount = {};
185 const didWarnAboutUndefinedDerivedState = {};
186 const didWarnAboutUninitializedState = {};
179 -const didWarnAboutInvalidateContextType = {};
187 const valuePropNames = ['value', 'defaultValue'];
188 const newlineEatingTags = {
189 listing: true,
@@ -324,65 +331,6 @@ function flattenOptionChildren(children: mixed): ?string {
331 return content;
332 }
333
327 -const emptyObject = {};
328 -if (__DEV__) {
329 - Object.freeze(emptyObject);
330 -}
331 -
332 -function maskContext(type, context) {
333 - const contextTypes = type.contextTypes;
334 - if (!contextTypes) {
335 - return emptyObject;
336 - }
337 - const maskedContext = {};
338 - for (const contextName in contextTypes) {
339 - maskedContext[contextName] = context[contextName];
340 - }
341 - return maskedContext;
342 -}
343 -
344 -function checkContextTypes(typeSpecs, values, location: string) {
345 - if (__DEV__) {
346 - checkPropTypes(
347 - typeSpecs,
348 - values,
349 - location,
350 - 'Component',
351 - getCurrentServerStackImpl,
352 - );
353 - }
354 -}
355 -
356 -function processContext(type, context) {
357 - const contextType = type.contextType;
358 - if (typeof contextType === 'object' && contextType !== null) {
359 - if (__DEV__) {
360 - if (contextType.$$typeof !== REACT_CONTEXT_TYPE) {
361 - let name = getComponentName(type) || 'Component';
362 - if (!didWarnAboutInvalidateContextType[name]) {
363 - didWarnAboutInvalidateContextType[type] = true;
364 - warningWithoutStack(
365 - false,
366 - '%s defines an invalid contextType. ' +
367 - 'contextType should point to the Context object returned by React.createContext(). ' +
368 - 'Did you accidentally pass the Context.Provider instead?',
369 - name,
370 - );
371 - }
372 - }
373 - }
374 - return contextType._currentValue;
375 - } else {
376 - const maskedContext = maskContext(type, context);
377 - if (__DEV__) {
378 - if (type.contextTypes) {
379 - checkContextTypes(type.contextTypes, maskedContext, 'context');
380 - }
381 - }
382 - return maskedContext;
383 - }
384 -}
385 -
334 const hasOwnProperty = Object.prototype.hasOwnProperty;
335 const STYLE = 'style';
336 const RESERVED_PROPS = {
@@ -453,6 +401,7 @@ function validateRenderResult(child, type) {
401 function resolve(
402 child: mixed,
403 context: Object,
404 + threadID: ThreadID,
405 ): {|
406 child: mixed,
407 context: Object,
@@ -472,7 +421,7 @@ function resolve(
421
422 // Extra closure so queue and replace can be captured properly
423 function processChild(element, Component) {
475 - let publicContext = processContext(Component, context);
424 + let publicContext = processContext(Component, context, threadID);
425
426 let queue = [];
427 let replace = false;
@@ -718,6 +667,7 @@ type FrameDev = Frame & {
667 };
668
669 class ReactDOMServerRenderer {
670 + threadID: ThreadID;
671 stack: Array<Frame>;
672 exhausted: boolean;
673 // TODO: type this more strictly:
@@ -747,6 +697,7 @@ class ReactDOMServerRenderer {
697 if (__DEV__) {
698 ((topFrame: any): FrameDev).debugElementStack = [];
699 }
700 + this.threadID = allocThreadID();
701 this.stack = [topFrame];
702 this.exhausted = false;
703 this.currentSelectValue = null;
@@ -763,6 +714,13 @@ class ReactDOMServerRenderer {
714 }
715 }
716
717 + destroy() {
718 + if (!this.exhausted) {
719 + this.exhausted = true;
720 + freeThreadID(this.threadID);
721 + }
722 + }
723 +
724 /**
725 * Note: We use just two stacks regardless of how many context providers you have.
726 * Providers are always popped in the reverse order to how they were pushed
@@ -776,7 +734,9 @@ class ReactDOMServerRenderer {
734 pushProvider<T>(provider: ReactProvider<T>): void {
735 const index = ++this.contextIndex;
736 const context: ReactContext<any> = provider.type._context;
779 - const previousValue = context._currentValue;
737 + const threadID = this.threadID;
738 + validateContextBounds(context, threadID);
739 + const previousValue = context[threadID];
740
741 // Remember which value to restore this context to on our way up.
742 this.contextStack[index] = context;
@@ -787,7 +747,7 @@ class ReactDOMServerRenderer {
747 }
748
749 // Mutate the current value.
790 - context._currentValue = provider.props.value;
750 + context[threadID] = provider.props.value;
751 }
752
753 popProvider<T>(provider: ReactProvider<T>): void {
@@ -813,7 +773,9 @@ class ReactDOMServerRenderer {
773 this.contextIndex--;
774
775 // Restore to the previous value we stored as we were walking down.
816 - context._currentValue = previousValue;
776 + // We've already verified that this context has been expanded to accommodate
777 + // this thread id, so we don't need to do it again.
778 + context[this.threadID] = previousValue;
779 }
780
781 read(bytes: number): string | null {
@@ -821,6 +783,8 @@ class ReactDOMServerRenderer {
783 return null;
784 }
785
786 + const prevThreadID = currentThreadID;
787 + setCurrentThreadID(this.threadID);
788 const prevDispatcher = ReactCurrentOwner.currentDispatcher;
789 if (enableHooks) {
790 ReactCurrentOwner.currentDispatcher = Dispatcher;
@@ -835,6 +799,7 @@ class ReactDOMServerRenderer {
799 while (out[0].length < bytes) {
800 if (this.stack.length === 0) {
801 this.exhausted = true;
802 + freeThreadID(this.threadID);
803 break;
804 }
805 const frame: Frame = this.stack[this.stack.length - 1];
@@ -906,6 +871,7 @@ class ReactDOMServerRenderer {
871 return out[0];
872 } finally {
873 ReactCurrentOwner.currentDispatcher = prevDispatcher;
874 + setCurrentThreadID(prevThreadID);
875 }
876 }
877
@@ -929,7 +895,7 @@ class ReactDOMServerRenderer {
895 return escapeTextForBrowser(text);
896 } else {
897 let nextChild;
932 - ({child: nextChild, context} = resolve(child, context));
898 + ({child: nextChild, context} = resolve(child, context, this.threadID));
899 if (nextChild === null || nextChild === false) {
900 return '';
901 } else if (!React.isValidElement(nextChild)) {
@@ -1136,7 +1102,9 @@ class ReactDOMServerRenderer {
1102 }
1103 }
1104 const nextProps: any = (nextChild: any).props;
1139 - const nextValue = reactContext._currentValue;
1105 + const threadID = this.threadID;
1106 + validateContextBounds(reactContext, threadID);
1107 + const nextValue = reactContext[threadID];
1108
1109 const nextChildren = toArray(nextProps.children(nextValue));
1110 const frame: Frame = {
packages/react-dom/src/server/ReactPartialRendererContext.js new
+104
@@ -0,0 +1,104 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {ThreadID} from './ReactThreadIDAllocator';
11 +import type {ReactContext} from 'shared/ReactTypes';
12 +
13 +import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
14 +import ReactSharedInternals from 'shared/ReactSharedInternals';
15 +import getComponentName from 'shared/getComponentName';
16 +import warningWithoutStack from 'shared/warningWithoutStack';
17 +import checkPropTypes from 'prop-types/checkPropTypes';
18 +
19 +let ReactDebugCurrentFrame;
20 +if (__DEV__) {
21 + ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
22 +}
23 +
24 +const didWarnAboutInvalidateContextType = {};
25 +
26 +export const emptyObject = {};
27 +if (__DEV__) {
28 + Object.freeze(emptyObject);
29 +}
30 +
31 +function maskContext(type, context) {
32 + const contextTypes = type.contextTypes;
33 + if (!contextTypes) {
34 + return emptyObject;
35 + }
36 + const maskedContext = {};
37 + for (const contextName in contextTypes) {
38 + maskedContext[contextName] = context[contextName];
39 + }
40 + return maskedContext;
41 +}
42 +
43 +function checkContextTypes(typeSpecs, values, location: string) {
44 + if (__DEV__) {
45 + checkPropTypes(
46 + typeSpecs,
47 + values,
48 + location,
49 + 'Component',
50 + ReactDebugCurrentFrame.getCurrentStack,
51 + );
52 + }
53 +}
54 +
55 +export function validateContextBounds(
56 + context: ReactContext<any>,
57 + threadID: ThreadID,
58 +) {
59 + // If we don't have enough slots in this context to store this threadID,
60 + // fill it in without leaving any holes to ensure that the VM optimizes
61 + // this as non-holey index properties.
62 + for (let i = context._threadCount; i <= threadID; i++) {
63 + // We assume that this is the same as the defaultValue which might not be
64 + // true if we're rendering inside a secondary renderer but they are
65 + // secondary because these use cases are very rare.
66 + context[i] = context._currentValue2;
67 + context._threadCount = i + 1;
68 + }
69 +}
70 +
71 +export function processContext(
72 + type: Function,
73 + context: Object,
74 + threadID: ThreadID,
75 +) {
76 + const contextType = type.contextType;
77 + if (typeof contextType === 'object' && contextType !== null) {
78 + if (__DEV__) {
79 + if (contextType.$$typeof !== REACT_CONTEXT_TYPE) {
80 + let name = getComponentName(type) || 'Component';
81 + if (!didWarnAboutInvalidateContextType[name]) {
82 + didWarnAboutInvalidateContextType[name] = true;
83 + warningWithoutStack(
84 + false,
85 + '%s defines an invalid contextType. ' +
86 + 'contextType should point to the Context object returned by React.createContext(). ' +
87 + 'Did you accidentally pass the Context.Provider instead?',
88 + name,
89 + );
90 + }
91 + }
92 + }
93 + validateContextBounds(contextType, threadID);
94 + return contextType[threadID];
95 + } else {
96 + const maskedContext = maskContext(type, context);
97 + if (__DEV__) {
98 + if (type.contextTypes) {
99 + checkContextTypes(type.contextTypes, maskedContext, 'context');
100 + }
101 + }
102 + return maskedContext;
103 + }
104 +}
packages/react-dom/src/server/ReactPartialRendererHooks.js
+16 -2
@@ -6,9 +6,13 @@
6 *
7 * @flow
8 */
9 +
10 +import type {ThreadID} from './ReactThreadIDAllocator';
11 import type {ReactContext} from 'shared/ReactTypes';
12 import areHookInputsEqual from 'shared/areHookInputsEqual';
13
14 +import {validateContextBounds} from './ReactPartialRendererContext';
15 +
16 import invariant from 'shared/invariant';
17 import warning from 'shared/warning';
18
@@ -139,7 +143,9 @@ function readContext<T>(
143 context: ReactContext<T>,
144 observedBits: void | number | boolean,
145 ): T {
142 - return context._currentValue;
146 + let threadID = currentThreadID;
147 + validateContextBounds(context, threadID);
148 + return context[threadID];
149 }
150
151 function useContext<T>(
@@ -147,7 +153,9 @@ function useContext<T>(
153 observedBits: void | number | boolean,
154 ): T {
155 resolveCurrentlyRenderingComponent();
150 - return context._currentValue;
156 + let threadID = currentThreadID;
157 + validateContextBounds(context, threadID);
158 + return context[threadID];
159 }
160
161 function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
@@ -334,6 +342,12 @@ function dispatchAction<A>(
342
343 function noop(): void {}
344
345 +export let currentThreadID: ThreadID = 0;
346 +
347 +export function setCurrentThreadID(threadID: ThreadID) {
348 + currentThreadID = threadID;
349 +}
350 +
351 export const Dispatcher = {
352 readContext,
353 useContext,
packages/react-dom/src/server/ReactThreadIDAllocator.js new
+58
@@ -0,0 +1,58 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +// Allocates a new index for each request. Tries to stay as compact as possible so that these
11 +// indices can be used to reference a tightly packaged array. As opposed to being used in a Map.
12 +// The first allocated index is 1.
13 +
14 +import invariant from 'shared/invariant';
15 +
16 +export type ThreadID = number;
17 +
18 +let nextAvailableThreadIDs = new Uint16Array(16);
19 +for (let i = 0; i < 15; i++) {
20 + nextAvailableThreadIDs[i] = i + 1;
21 +}
22 +nextAvailableThreadIDs[15] = 0;
23 +
24 +function growThreadCountAndReturnNextAvailable() {
25 + let oldArray = nextAvailableThreadIDs;
26 + let oldSize = oldArray.length;
27 + let newSize = oldSize * 2;
28 + invariant(
29 + newSize <= 0x10000,
30 + 'Maximum number of concurrent React renderers exceeded. ' +
31 + 'This can happen if you are not properly destroying the Readable provided by React. ' +
32 + 'Ensure that you call .destroy() on it if you no longer want to read from it.' +
33 + ', and did not read to the end. If you use .pipe() this should be automatic.',
34 + );
35 + let newArray = new Uint16Array(newSize);
36 + newArray.set(oldArray);
37 + nextAvailableThreadIDs = newArray;
38 + nextAvailableThreadIDs[0] = oldSize + 1;
39 + for (let i = oldSize; i < newSize - 1; i++) {
40 + nextAvailableThreadIDs[i] = i + 1;
41 + }
42 + nextAvailableThreadIDs[newSize - 1] = 0;
43 + return oldSize;
44 +}
45 +
46 +export function allocThreadID(): ThreadID {
47 + let nextID = nextAvailableThreadIDs[0];
48 + if (nextID === 0) {
49 + return growThreadCountAndReturnNextAvailable();
50 + }
51 + nextAvailableThreadIDs[0] = nextAvailableThreadIDs[nextID];
52 + return nextID;
53 +}
54 +
55 +export function freeThreadID(id: ThreadID) {
56 + nextAvailableThreadIDs[id] = nextAvailableThreadIDs[0];
57 + nextAvailableThreadIDs[0] = id;
58 +}
packages/react/src/ReactContext.js
+11
@@ -42,6 +42,9 @@ export function createContext<T>(
42 // Secondary renderers store their context values on separate fields.
43 _currentValue: defaultValue,
44 _currentValue2: defaultValue,
45 + // Used to track how many concurrent renderers this context currently
46 + // supports within in a single renderer. Such as parallel server rendering.
47 + _threadCount: 0,
48 // These are circular
49 Provider: (null: any),
50 Consumer: (null: any),
@@ -98,6 +101,14 @@ export function createContext<T>(
101 context._currentValue2 = _currentValue2;
102 },
103 },
104 + _threadCount: {
105 + get() {
106 + return context._threadCount;
107 + },
108 + set(_threadCount) {
109 + context._threadCount = _threadCount;
110 + },
111 + },
112 Consumer: {
113 get() {
114 if (!hasWarnedAboutUsingNestedContextConsumers) {
packages/shared/ReactTypes.js
+1
@@ -59,6 +59,7 @@ export type ReactContext<T> = {
59
60 _currentValue: T,
61 _currentValue2: T,
62 + _threadCount: number,
63
64 // DEV only
65 _currentRenderer?: Object | null,
scripts/rollup/validate/eslintrc.cjs.js
+1
@@ -12,6 +12,7 @@ module.exports = {
12 Proxy: true,
13 Symbol: true,
14 WeakMap: true,
15 + Uint16Array: true,
16 // Vendor specific
17 MSApp: true,
18 __REACT_DEVTOOLS_GLOBAL_HOOK__: true,
scripts/rollup/validate/eslintrc.fb.js
+1
@@ -12,6 +12,7 @@ module.exports = {
12 Symbol: true,
13 Proxy: true,
14 WeakMap: true,
15 + Uint16Array: true,
16 // Vendor specific
17 MSApp: true,
18 __REACT_DEVTOOLS_GLOBAL_HOOK__: true,
scripts/rollup/validate/eslintrc.umd.js
+1
@@ -11,6 +11,7 @@ module.exports = {
11 Symbol: true,
12 Proxy: true,
13 WeakMap: true,
14 + Uint16Array: true,
15 // Vendor specific
16 MSApp: true,
17 __REACT_DEVTOOLS_GLOBAL_HOOK__: true,