Skip to content

Commit

Permalink
Try assigning fetch to globalThis if global assignment fails (#25571)
Browse files Browse the repository at this point in the history
In case it's a more modern yet rigid environment.
  • Loading branch information
sebmarkbage authored and rickhanlonii committed Dec 3, 2022
1 parent e0ab78b commit 6b893e1
Show file tree
Hide file tree
Showing 8 changed files with 93 additions and 80 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,5 +277,6 @@ module.exports = {
trustedTypes: 'readonly',
IS_REACT_ACT_ENVIRONMENT: 'readonly',
AsyncLocalStorage: 'readonly',
globalThis: 'readonly',
},
};
164 changes: 85 additions & 79 deletions packages/react/src/ReactFetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,92 +42,98 @@ function generateCacheKey(request: Request): string {
if (enableCache && enableFetchInstrumentation) {
if (typeof fetch === 'function') {
const originalFetch = fetch;
try {
// eslint-disable-next-line no-native-reassign
fetch = function fetch(
resource: URL | RequestInfo,
options?: RequestOptions,
const cachedFetch = function fetch(
resource: URL | RequestInfo,
options?: RequestOptions,
) {
const dispatcher = ReactCurrentCache.current;
if (!dispatcher) {
// We're outside a cached scope.
return originalFetch(resource, options);
}
if (
options &&
options.signal &&
options.signal !== dispatcher.getCacheSignal()
) {
const dispatcher = ReactCurrentCache.current;
if (!dispatcher) {
// We're outside a cached scope.
return originalFetch(resource, options);
}
// If we're passed a signal that is not ours, then we assume that
// someone else controls the lifetime of this object and opts out of
// caching. It's effectively the opt-out mechanism.
// Ideally we should be able to check this on the Request but
// it always gets initialized with its own signal so we don't
// know if it's supposed to override - unless we also override the
// Request constructor.
return originalFetch(resource, options);
}
// Normalize the Request
let url: string;
let cacheKey: string;
if (typeof resource === 'string' && !options) {
// Fast path.
cacheKey = simpleCacheKey;
url = resource;
} else {
// Normalize the request.
const request = new Request(resource, options);
if (
options &&
options.signal &&
options.signal !== dispatcher.getCacheSignal()
(request.method !== 'GET' && request.method !== 'HEAD') ||
// $FlowFixMe: keepalive is real
request.keepalive
) {
// If we're passed a signal that is not ours, then we assume that
// someone else controls the lifetime of this object and opts out of
// caching. It's effectively the opt-out mechanism.
// Ideally we should be able to check this on the Request but
// it always gets initialized with its own signal so we don't
// know if it's supposed to override - unless we also override the
// Request constructor.
// We currently don't dedupe requests that might have side-effects. Those
// have to be explicitly cached. We assume that the request doesn't have a
// body if it's GET or HEAD.
// keepalive gets treated the same as if you passed a custom cache signal.
return originalFetch(resource, options);
}
// Normalize the Request
let url: string;
let cacheKey: string;
if (typeof resource === 'string' && !options) {
// Fast path.
cacheKey = simpleCacheKey;
url = resource;
} else {
// Normalize the request.
const request = new Request(resource, options);
if (
(request.method !== 'GET' && request.method !== 'HEAD') ||
// $FlowFixMe: keepalive is real
request.keepalive
) {
// We currently don't dedupe requests that might have side-effects. Those
// have to be explicitly cached. We assume that the request doesn't have a
// body if it's GET or HEAD.
// keepalive gets treated the same as if you passed a custom cache signal.
return originalFetch(resource, options);
cacheKey = generateCacheKey(request);
url = request.url;
}
const cache = dispatcher.getCacheForType(createFetchCache);
const cacheEntries = cache.get(url);
let match;
if (cacheEntries === undefined) {
// We pass the original arguments here in case normalizing the Request
// doesn't include all the options in this environment.
match = originalFetch(resource, options);
cache.set(url, [cacheKey, match]);
} else {
// We use an array as the inner data structure since it's lighter and
// we typically only expect to see one or two entries here.
for (let i = 0, l = cacheEntries.length; i < l; i += 2) {
const key = cacheEntries[i];
const value = cacheEntries[i + 1];
if (key === cacheKey) {
match = value;
// I would've preferred a labelled break but lint says no.
return match.then(response => response.clone());
}
cacheKey = generateCacheKey(request);
url = request.url;
}
const cache = dispatcher.getCacheForType(createFetchCache);
const cacheEntries = cache.get(url);
let match;
if (cacheEntries === undefined) {
// We pass the original arguments here in case normalizing the Request
// doesn't include all the options in this environment.
match = originalFetch(resource, options);
cache.set(url, [cacheKey, match]);
} else {
// We use an array as the inner data structure since it's lighter and
// we typically only expect to see one or two entries here.
for (let i = 0, l = cacheEntries.length; i < l; i += 2) {
const key = cacheEntries[i];
const value = cacheEntries[i + 1];
if (key === cacheKey) {
match = value;
// I would've preferred a labelled break but lint says no.
return match.then(response => response.clone());
}
}
match = originalFetch(resource, options);
cacheEntries.push(cacheKey, match);
}
// We clone the response so that each time you call this you get a new read
// of the body so that it can be read multiple times.
return match.then(response => response.clone());
};
// We don't expect to see any extra properties on fetch but if there are any,
// copy them over. Useful for extended fetch environments or mocks.
Object.assign(fetch, originalFetch);
} catch (error) {
// Log even in production just to make sure this is seen if only prod is frozen.
// eslint-disable-next-line react-internal/no-production-logging
console.warn(
'React was unable to patch the fetch() function in this environment. ' +
'Suspensey APIs might not work correctly as a result.',
);
match = originalFetch(resource, options);
cacheEntries.push(cacheKey, match);
}
// We clone the response so that each time you call this you get a new read
// of the body so that it can be read multiple times.
return match.then(response => response.clone());
};
// We don't expect to see any extra properties on fetch but if there are any,
// copy them over. Useful for extended fetch environments or mocks.
Object.assign(cachedFetch, originalFetch);
try {
// eslint-disable-next-line no-native-reassign
fetch = cachedFetch;
} catch (error1) {
try {
// In case assigning it globally fails, try globalThis instead just in case it exists.
globalThis.fetch = cachedFetch;
} catch (error2) {
// Log even in production just to make sure this is seen if only prod is frozen.
// eslint-disable-next-line react-internal/no-production-logging
console.warn(
'React was unable to patch the fetch() function in this environment. ' +
'Suspensey APIs might not work correctly as a result.',
);
}
}
}
}
2 changes: 2 additions & 0 deletions scripts/flow/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ declare var __REACT_DEVTOOLS_GLOBAL_HOOK__: any; /*?{
inject: ?((stuff: Object) => void)
};*/

declare var globalThis: Object;

declare var queueMicrotask: (fn: Function) => void;
declare var reportError: (error: mixed) => void;

Expand Down
1 change: 1 addition & 0 deletions scripts/rollup/validate/eslintrc.cjs2015.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ module.exports = {
WeakSet: 'readonly',
Uint16Array: 'readonly',
Reflect: 'readonly',
globalThis: 'readonly',
// Vendor specific
MSApp: 'readonly',
__REACT_DEVTOOLS_GLOBAL_HOOK__: 'readonly',
Expand Down
1 change: 1 addition & 0 deletions scripts/rollup/validate/eslintrc.esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module.exports = {
WeakSet: 'readonly',
Uint16Array: 'readonly',
Reflect: 'readonly',
globalThis: 'readonly',
// Vendor specific
MSApp: 'readonly',
__REACT_DEVTOOLS_GLOBAL_HOOK__: 'readonly',
Expand Down
1 change: 1 addition & 0 deletions scripts/rollup/validate/eslintrc.fb.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module.exports = {
WeakSet: 'readonly',
Uint16Array: 'readonly',
Reflect: 'readonly',
globalThis: 'readonly',
// Vendor specific
MSApp: 'readonly',
__REACT_DEVTOOLS_GLOBAL_HOOK__: 'readonly',
Expand Down
1 change: 1 addition & 0 deletions scripts/rollup/validate/eslintrc.rn.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module.exports = {
WeakMap: 'readonly',
WeakSet: 'readonly',
Reflect: 'readonly',
globalThis: 'readonly',
// Vendor specific
MSApp: 'readonly',
__REACT_DEVTOOLS_GLOBAL_HOOK__: 'readonly',
Expand Down
2 changes: 1 addition & 1 deletion scripts/rollup/validate/eslintrc.umd.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module.exports = {
WeakSet: 'readonly',
Uint16Array: 'readonly',
Reflect: 'readonly',
globalThis: 'readonly',
// Vendor specific
MSApp: 'readonly',
__REACT_DEVTOOLS_GLOBAL_HOOK__: 'readonly',
Expand All @@ -24,7 +25,6 @@ module.exports = {
module: 'readonly',
define: 'readonly',
require: 'readonly',
globalThis: 'readonly',
global: 'readonly',
// Internet Explorer
setImmediate: 'readonly',
Expand Down

0 comments on commit 6b893e1

Please sign in to comment.