import React, { useEffect, useLayoutEffect, createContext, useContext, useMemo, useRef, createElement } from 'react'; // Shared state between server components and client components const noop = ()=>{}; // Using noop() as the undefined value as undefined can be replaced // by something else. Prettier ignore and extra parentheses are necessary here // to ensure that tsc doesn't remove the __NOINLINE__ comment. // prettier-ignore const UNDEFINED = /*#__NOINLINE__*/ noop(); const OBJECT = Object; const isUndefined = (v)=>v === UNDEFINED; const isFunction = (v)=>typeof v == 'function'; const mergeObjects = (a, b)=>({ ...a, ...b }); const isPromiseLike = (x)=>isFunction(x.then); // use WeakMap to store the object->key mapping // so the objects can be garbage collected. // WeakMap uses a hashtable under the hood, so the lookup // complexity is almost O(1). const table = new WeakMap(); // counter of the key let counter = 0; // A stable hash implementation that supports: // - Fast and ensures unique hash properties // - Handles unserializable values // - Handles object key ordering // - Generates short results // // This is not a serialization function, and the result is not guaranteed to be // parsable. const stableHash = (arg)=>{ const type = typeof arg; const constructor = arg && arg.constructor; const isDate = constructor == Date; let result; let index; if (OBJECT(arg) === arg && !isDate && constructor != RegExp) { // Object/function, not null/date/regexp. Use WeakMap to store the id first. // If it's already hashed, directly return the result. result = table.get(arg); if (result) return result; // Store the hash first for circular reference detection before entering the // recursive `stableHash` calls. // For other objects like set and map, we use this id directly as the hash. result = ++counter + '~'; table.set(arg, result); if (constructor == Array) { // Array. result = '@'; for(index = 0; index < arg.length; index++){ result += stableHash(arg[index]) + ','; } table.set(arg, result); } if (constructor == OBJECT) { // Object, sort keys. result = '#'; const keys = OBJECT.keys(arg).sort(); while(!isUndefined(index = keys.pop())){ if (!isUndefined(arg[index])) { result += index + ':' + stableHash(arg[index]) + ','; } } table.set(arg, result); } } else { result = isDate ? arg.toJSON() : type == 'symbol' ? arg.toString() : type == 'string' ? JSON.stringify(arg) : '' + arg; } return result; }; // Global state used to deduplicate requests and store listeners const SWRGlobalState = new WeakMap(); const EMPTY_CACHE = {}; const INITIAL_CACHE = {}; const STR_UNDEFINED = 'undefined'; // NOTE: Use the function to guarantee it's re-evaluated between jsdom and node runtime for tests. const isWindowDefined = typeof window != STR_UNDEFINED; const isDocumentDefined = typeof document != STR_UNDEFINED; const hasRequestAnimationFrame = ()=>isWindowDefined && typeof window['requestAnimationFrame'] != STR_UNDEFINED; const createCacheHelper = (cache, key)=>{ const state = SWRGlobalState.get(cache); return [ // Getter ()=>!isUndefined(key) && cache.get(key) || EMPTY_CACHE, // Setter (info)=>{ if (!isUndefined(key)) { const prev = cache.get(key); // Before writing to the store, we keep the value in the initial cache // if it's not there yet. if (!(key in INITIAL_CACHE)) { INITIAL_CACHE[key] = prev; } state[5](key, mergeObjects(prev, info), prev || EMPTY_CACHE); } }, // Subscriber state[6], // Get server cache snapshot ()=>{ if (!isUndefined(key)) { // If the cache was updated on the client, we return the stored initial value. if (key in INITIAL_CACHE) return INITIAL_CACHE[key]; } // If we haven't done any client-side updates, we return the current value. return !isUndefined(key) && cache.get(key) || EMPTY_CACHE; } ]; } // export { UNDEFINED, OBJECT, isUndefined, isFunction, mergeObjects, isPromiseLike } ; /** * Due to the bug https://bugs.chromium.org/p/chromium/issues/detail?id=678075, * it's not reliable to detect if the browser is currently online or offline * based on `navigator.onLine`. * As a workaround, we always assume it's online on the first load, and change * the status upon `online` or `offline` events. */ let online = true; const isOnline = ()=>online; // For node and React Native, `add/removeEventListener` doesn't exist on window. const [onWindowEvent, offWindowEvent] = isWindowDefined && window.addEventListener ? [ window.addEventListener.bind(window), window.removeEventListener.bind(window) ] : [ noop, noop ]; const isVisible = ()=>{ const visibilityState = isDocumentDefined && document.visibilityState; return isUndefined(visibilityState) || visibilityState !== 'hidden'; }; const initFocus = (callback)=>{ // focus revalidate if (isDocumentDefined) { document.addEventListener('visibilitychange', callback); } onWindowEvent('focus', callback); return ()=>{ if (isDocumentDefined) { document.removeEventListener('visibilitychange', callback); } offWindowEvent('focus', callback); }; }; const initReconnect = (callback)=>{ // revalidate on reconnected const onOnline = ()=>{ online = true; callback(); }; // nothing to revalidate, just update the status const onOffline = ()=>{ online = false; }; onWindowEvent('online', onOnline); onWindowEvent('offline', onOffline); return ()=>{ offWindowEvent('online', onOnline); offWindowEvent('offline', onOffline); }; }; const preset = { isOnline, isVisible }; const defaultConfigOptions = { initFocus, initReconnect }; const IS_REACT_LEGACY = !React.useId; const IS_SERVER = !isWindowDefined || 'Deno' in window; // Polyfill requestAnimationFrame const rAF = (f)=>hasRequestAnimationFrame() ? window['requestAnimationFrame'](f) : setTimeout(f, 1); // React currently throws a warning when using useLayoutEffect on the server. // To get around it, we can conditionally useEffect on the server (no-op) and // useLayoutEffect in the browser. const useIsomorphicLayoutEffect = IS_SERVER ? useEffect : useLayoutEffect; // This assignment is to extend the Navigator type to use effectiveType. const navigatorConnection = typeof navigator !== 'undefined' && navigator.connection; // Adjust the config based on slow connection status (<= 70Kbps). const slowConnection = !IS_SERVER && navigatorConnection && ([ 'slow-2g', '2g' ].includes(navigatorConnection.effectiveType) || navigatorConnection.saveData); const serialize = (key)=>{ if (isFunction(key)) { try { key = key(); } catch (err) { // dependencies not ready key = ''; } } // Use the original key as the argument of fetcher. This can be a string or an // array of values. const args = key; // If key is not falsy, or not an empty array, hash it. key = typeof key == 'string' ? key : (Array.isArray(key) ? key.length : key) ? stableHash(key) : ''; return [ key, args ]; }; // Global timestamp. let __timestamp = 0; const getTimestamp = ()=>++__timestamp; const FOCUS_EVENT = 0; const RECONNECT_EVENT = 1; const MUTATE_EVENT = 2; const ERROR_REVALIDATE_EVENT = 3; var events = { __proto__: null, ERROR_REVALIDATE_EVENT: ERROR_REVALIDATE_EVENT, FOCUS_EVENT: FOCUS_EVENT, MUTATE_EVENT: MUTATE_EVENT, RECONNECT_EVENT: RECONNECT_EVENT }; async function internalMutate(...args) { const [cache, _key, _data, _opts] = args; // When passing as a boolean, it's explicitly used to disable/enable // revalidation. const options = mergeObjects({ populateCache: true, throwOnError: true }, typeof _opts === 'boolean' ? { revalidate: _opts } : _opts || {}); let populateCache = options.populateCache; const rollbackOnErrorOption = options.rollbackOnError; let optimisticData = options.optimisticData; const rollbackOnError = (error)=>{ return typeof rollbackOnErrorOption === 'function' ? rollbackOnErrorOption(error) : rollbackOnErrorOption !== false; }; const throwOnError = options.throwOnError; // If the second argument is a key filter, return the mutation results for all // filtered keys. if (isFunction(_key)) { const keyFilter = _key; const matchedKeys = []; const it = cache.keys(); for (const key of it){ if (// Skip the special useSWRInfinite and useSWRSubscription keys. !/^\$(inf|sub)\$/.test(key) && keyFilter(cache.get(key)._k)) { matchedKeys.push(key); } } return Promise.all(matchedKeys.map(mutateByKey)); } return mutateByKey(_key); async function mutateByKey(_k) { // Serialize key const [key] = serialize(_k); if (!key) return; const [get, set] = createCacheHelper(cache, key); const [EVENT_REVALIDATORS, MUTATION, FETCH, PRELOAD] = SWRGlobalState.get(cache); const startRevalidate = ()=>{ const revalidators = EVENT_REVALIDATORS[key]; const revalidate = isFunction(options.revalidate) ? options.revalidate(get().data, _k) : options.revalidate !== false; if (revalidate) { // Invalidate the key by deleting the concurrent request markers so new // requests will not be deduped. delete FETCH[key]; delete PRELOAD[key]; if (revalidators && revalidators[0]) { return revalidators[0](MUTATE_EVENT).then(()=>get().data); } } return get().data; }; // If there is no new data provided, revalidate the key with current state. if (args.length < 3) { // Revalidate and broadcast state. return startRevalidate(); } let data = _data; let error; // Update global timestamps. const beforeMutationTs = getTimestamp(); MUTATION[key] = [ beforeMutationTs, 0 ]; const hasOptimisticData = !isUndefined(optimisticData); const state = get(); // `displayedData` is the current value on screen. It could be the optimistic value // that is going to be overridden by a `committedData`, or get reverted back. // `committedData` is the validated value that comes from a fetch or mutation. const displayedData = state.data; const currentData = state._c; const committedData = isUndefined(currentData) ? displayedData : currentData; // Do optimistic data update. if (hasOptimisticData) { optimisticData = isFunction(optimisticData) ? optimisticData(committedData, displayedData) : optimisticData; // When we set optimistic data, backup the current committedData data in `_c`. set({ data: optimisticData, _c: committedData }); } if (isFunction(data)) { // `data` is a function, call it passing current cache value. try { data = data(committedData); } catch (err) { // If it throws an error synchronously, we shouldn't update the cache. error = err; } } // `data` is a promise/thenable, resolve the final data first. if (data && isPromiseLike(data)) { // This means that the mutation is async, we need to check timestamps to // avoid race conditions. data = await data.catch((err)=>{ error = err; }); // Check if other mutations have occurred since we've started this mutation. // If there's a race we don't update cache or broadcast the change, // just return the data. if (beforeMutationTs !== MUTATION[key][0]) { if (error) throw error; return data; } else if (error && hasOptimisticData && rollbackOnError(error)) { // Rollback. Always populate the cache in this case but without // transforming the data. populateCache = true; // Reset data to be the latest committed data, and clear the `_c` value. set({ data: committedData, _c: UNDEFINED }); } } // If we should write back the cache after request. if (populateCache) { if (!error) { // Transform the result into data. if (isFunction(populateCache)) { const populateCachedData = populateCache(data, committedData); set({ data: populateCachedData, error: UNDEFINED, _c: UNDEFINED }); } else { // Only update cached data and reset the error if there's no error. Data can be `undefined` here. set({ data, error: UNDEFINED, _c: UNDEFINED }); } } } // Reset the timestamp to mark the mutation has ended. MUTATION[key][1] = getTimestamp(); // Update existing SWR Hooks' internal states: Promise.resolve(startRevalidate()).then(()=>{ // The mutation and revalidation are ended, we can clear it since the data is // not an optimistic value anymore. set({ _c: UNDEFINED }); }); // Throw error or return data if (error) { if (throwOnError) throw error; return; } return data; } } const revalidateAllKeys = (revalidators, type)=>{ for(const key in revalidators){ if (revalidators[key][0]) revalidators[key][0](type); } }; const initCache = (provider, options)=>{ // The global state for a specific provider will be used to deduplicate // requests and store listeners. As well as a mutate function that is bound to // the cache. // The provider's global state might be already initialized. Let's try to get the // global state associated with the provider first. if (!SWRGlobalState.has(provider)) { const opts = mergeObjects(defaultConfigOptions, options); // If there's no global state bound to the provider, create a new one with the // new mutate function. const EVENT_REVALIDATORS = {}; const mutate = internalMutate.bind(UNDEFINED, provider); let unmount = noop; const subscriptions = {}; const subscribe = (key, callback)=>{ const subs = subscriptions[key] || []; subscriptions[key] = subs; subs.push(callback); return ()=>subs.splice(subs.indexOf(callback), 1); }; const setter = (key, value, prev)=>{ provider.set(key, value); const subs = subscriptions[key]; if (subs) { for (const fn of subs){ fn(value, prev); } } }; const initProvider = ()=>{ if (!SWRGlobalState.has(provider)) { // Update the state if it's new, or if the provider has been extended. SWRGlobalState.set(provider, [ EVENT_REVALIDATORS, {}, {}, {}, mutate, setter, subscribe ]); if (!IS_SERVER) { // When listening to the native events for auto revalidations, // we intentionally put a delay (setTimeout) here to make sure they are // fired after immediate JavaScript executions, which can be // React's state updates. // This avoids some unnecessary revalidations such as // https://github.com/vercel/swr/issues/1680. const releaseFocus = opts.initFocus(setTimeout.bind(UNDEFINED, revalidateAllKeys.bind(UNDEFINED, EVENT_REVALIDATORS, FOCUS_EVENT))); const releaseReconnect = opts.initReconnect(setTimeout.bind(UNDEFINED, revalidateAllKeys.bind(UNDEFINED, EVENT_REVALIDATORS, RECONNECT_EVENT))); unmount = ()=>{ releaseFocus && releaseFocus(); releaseReconnect && releaseReconnect(); // When un-mounting, we need to remove the cache provider from the state // storage too because it's a side-effect. Otherwise, when re-mounting we // will not re-register those event listeners. SWRGlobalState.delete(provider); }; } } }; initProvider(); // This is a new provider, we need to initialize it and setup DOM events // listeners for `focus` and `reconnect` actions. // We might want to inject an extra layer on top of `provider` in the future, // such as key serialization, auto GC, etc. // For now, it's just a `Map` interface without any modifications. return [ provider, mutate, initProvider, unmount ]; } return [ provider, SWRGlobalState.get(provider)[4] ]; }; // error retry const onErrorRetry = (_, __, config, revalidate, opts)=>{ const maxRetryCount = config.errorRetryCount; const currentRetryCount = opts.retryCount; // Exponential backoff const timeout = ~~((Math.random() + 0.5) * (1 << (currentRetryCount < 8 ? currentRetryCount : 8))) * config.errorRetryInterval; if (!isUndefined(maxRetryCount) && currentRetryCount > maxRetryCount) { return; } setTimeout(revalidate, timeout, opts); }; const compare = (currentData, newData)=>stableHash(currentData) == stableHash(newData); // Default cache provider const [cache, mutate] = initCache(new Map()); // Default config const defaultConfig = mergeObjects({ // events onLoadingSlow: noop, onSuccess: noop, onError: noop, onErrorRetry, onDiscarded: noop, // switches revalidateOnFocus: true, revalidateOnReconnect: true, revalidateIfStale: true, shouldRetryOnError: true, // timeouts errorRetryInterval: slowConnection ? 10000 : 5000, focusThrottleInterval: 5 * 1000, dedupingInterval: 2 * 1000, loadingTimeout: slowConnection ? 5000 : 3000, // providers compare, isPaused: ()=>false, cache, mutate, fallback: {} }, // use web preset by default preset); const mergeConfigs = (a, b)=>{ // Need to create a new object to avoid mutating the original here. const v = mergeObjects(a, b); // If two configs are provided, merge their `use` and `fallback` options. if (b) { const { use: u1, fallback: f1 } = a; const { use: u2, fallback: f2 } = b; if (u1 && u2) { v.use = u1.concat(u2); } if (f1 && f2) { v.fallback = mergeObjects(f1, f2); } } return v; }; const SWRConfigContext = createContext({}); const SWRConfig = (props)=>{ const { value } = props; const parentConfig = useContext(SWRConfigContext); const isFunctionalConfig = isFunction(value); const config = useMemo(()=>isFunctionalConfig ? value(parentConfig) : value, [ isFunctionalConfig, parentConfig, value ]); // Extend parent context values and middleware. const extendedConfig = useMemo(()=>isFunctionalConfig ? config : mergeConfigs(parentConfig, config), [ isFunctionalConfig, parentConfig, config ]); // Should not use the inherited provider. const provider = config && config.provider; // initialize the cache only on first access. const cacheContextRef = useRef(UNDEFINED); if (provider && !cacheContextRef.current) { cacheContextRef.current = initCache(provider(extendedConfig.cache || cache), config); } const cacheContext = cacheContextRef.current; // Override the cache if a new provider is given. if (cacheContext) { extendedConfig.cache = cacheContext[0]; extendedConfig.mutate = cacheContext[1]; } // Unsubscribe events. useIsomorphicLayoutEffect(()=>{ if (cacheContext) { cacheContext[2] && cacheContext[2](); return cacheContext[3]; } }, []); return createElement(SWRConfigContext.Provider, mergeObjects(props, { value: extendedConfig })); }; const INFINITE_PREFIX = '$inf$'; // @ts-expect-error const enableDevtools = isWindowDefined && window.__SWR_DEVTOOLS_USE__; const use = enableDevtools ? window.__SWR_DEVTOOLS_USE__ : []; const setupDevTools = ()=>{ if (enableDevtools) { // @ts-expect-error window.__SWR_DEVTOOLS_REACT__ = React; } }; const normalize = (args)=>{ return isFunction(args[1]) ? [ args[0], args[1], args[2] || {} ] : [ args[0], null, (args[1] === null ? args[2] : args[1]) || {} ]; }; const useSWRConfig = ()=>{ return mergeObjects(defaultConfig, useContext(SWRConfigContext)); }; const preload = (key_, fetcher)=>{ const [key, fnArg] = serialize(key_); const [, , , PRELOAD] = SWRGlobalState.get(cache); // Prevent preload to be called multiple times before used. if (PRELOAD[key]) return PRELOAD[key]; const req = fetcher(fnArg); PRELOAD[key] = req; return req; }; const middleware = (useSWRNext)=>(key_, fetcher_, config)=>{ // fetcher might be a sync function, so this should not be an async function const fetcher = fetcher_ && ((...args)=>{ const [key] = serialize(key_); const [, , , PRELOAD] = SWRGlobalState.get(cache); if (key.startsWith(INFINITE_PREFIX)) { // we want the infinite fetcher to be called. // handling of the PRELOAD cache happens there. return fetcher_(...args); } const req = PRELOAD[key]; if (isUndefined(req)) return fetcher_(...args); delete PRELOAD[key]; return req; }); return useSWRNext(key_, fetcher, config); }; const BUILT_IN_MIDDLEWARE = use.concat(middleware); // It's tricky to pass generic types as parameters, so we just directly override // the types here. const withArgs = (hook)=>{ return function useSWRArgs(...args) { // Get the default and inherited configuration. const fallbackConfig = useSWRConfig(); // Normalize arguments. const [key, fn, _config] = normalize(args); // Merge configurations. const config = mergeConfigs(fallbackConfig, _config); // Apply middleware let next = hook; const { use } = config; const middleware = (use || []).concat(BUILT_IN_MIDDLEWARE); for(let i = middleware.length; i--;){ next = middleware[i](next); } return next(key, fn || config.fetcher || null, config); }; }; // Add a callback function to a list of keyed callback functions and return // the unsubscribe function. const subscribeCallback = (key, callbacks, callback)=>{ const keyedRevalidators = callbacks[key] || (callbacks[key] = []); keyedRevalidators.push(callback); return ()=>{ const index = keyedRevalidators.indexOf(callback); if (index >= 0) { // O(1): faster than splice keyedRevalidators[index] = keyedRevalidators[keyedRevalidators.length - 1]; keyedRevalidators.pop(); } }; }; // Create a custom hook with a middleware const withMiddleware = (useSWR, middleware)=>{ return (...args)=>{ const [key, fn, config] = normalize(args); const uses = (config.use || []).concat(middleware); return useSWR(key, fn, { ...config, use: uses }); }; }; setupDevTools(); export { INFINITE_PREFIX, IS_REACT_LEGACY, IS_SERVER, OBJECT, SWRConfig, SWRGlobalState, UNDEFINED, cache, compare, createCacheHelper, defaultConfig, defaultConfigOptions, getTimestamp, hasRequestAnimationFrame, initCache, internalMutate, isDocumentDefined, isFunction, isPromiseLike, isUndefined, isWindowDefined, mergeConfigs, mergeObjects, mutate, noop, normalize, preload, preset, rAF, events as revalidateEvents, serialize, slowConnection, stableHash, subscribeCallback, useIsomorphicLayoutEffect, useSWRConfig, withArgs, withMiddleware };