/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ import React, {createContext} from 'react'; // Cache implementation was forked from the React repo: // https://github.com/facebook/react/blob/master/packages/react-cache/src/ReactCache.js // // This cache is simpler than react-cache in that: // 1. Individual items don't need to be invalidated. // Profiling data is invalidated as a whole. // 2. We didn't need the added overhead of an LRU cache. // The size of this cache is bounded by how many renders were profiled, // and it will be fully reset between profiling sessions. export type Thenable = { then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed, }; type Suspender = { then(resolve: () => mixed, reject: () => mixed): mixed, }; type PendingResult = {| status: 0, value: Suspender, |}; type ResolvedResult = {| status: 1, value: Value, |}; type RejectedResult = {| status: 2, value: mixed, |}; type Result = PendingResult | ResolvedResult | RejectedResult; export type Resource = { clear(): void, invalidate(Key): void, read(Input): Value, preload(Input): void, write(Key, Value): void, }; const Pending = 0; const Resolved = 1; const Rejected = 2; const ReactCurrentDispatcher = (React: any) .__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher; function readContext(Context, observedBits) { const dispatcher = ReactCurrentDispatcher.current; if (dispatcher === null) { throw new Error( 'react-cache: read and preload may only be called from within a ' + "component's render. They are not supported in event handlers or " + 'lifecycle methods.', ); } return dispatcher.readContext(Context, observedBits); } const CacheContext = createContext(null); type Config = { useWeakMap?: boolean, }; const entries: Map< Resource, Map | WeakMap, > = new Map(); const resourceConfigs: Map, Config> = new Map(); function getEntriesForResource( resource: any, ): Map | WeakMap { let entriesForResource = ((entries.get(resource): any): Map); if (entriesForResource === undefined) { const config = resourceConfigs.get(resource); entriesForResource = config !== undefined && config.useWeakMap ? new WeakMap() : new Map(); entries.set(resource, entriesForResource); } return entriesForResource; } function accessResult( resource: any, fetch: Input => Thenable, input: Input, key: Key, ): Result { const entriesForResource = getEntriesForResource(resource); const entry = entriesForResource.get(key); if (entry === undefined) { const thenable = fetch(input); thenable.then( value => { if (newResult.status === Pending) { const resolvedResult: ResolvedResult = (newResult: any); resolvedResult.status = Resolved; resolvedResult.value = value; } }, error => { if (newResult.status === Pending) { const rejectedResult: RejectedResult = (newResult: any); rejectedResult.status = Rejected; rejectedResult.value = error; } }, ); const newResult: PendingResult = { status: Pending, value: thenable, }; entriesForResource.set(key, newResult); return newResult; } else { return entry; } } export function createResource( fetch: Input => Thenable, hashInput: Input => Key, config?: Config = {}, ): Resource { const resource = { clear(): void { entries.delete(resource); }, invalidate(key: Key): void { const entriesForResource = getEntriesForResource(resource); entriesForResource.delete(key); }, read(input: Input): Value { // Prevent access outside of render. readContext(CacheContext); const key = hashInput(input); const result: Result = accessResult(resource, fetch, input, key); switch (result.status) { case Pending: { const suspender = result.value; throw suspender; } case Resolved: { const value = result.value; return value; } case Rejected: { const error = result.value; throw error; } default: // Should be unreachable return (undefined: any); } }, preload(input: Input): void { // Prevent access outside of render. readContext(CacheContext); const key = hashInput(input); accessResult(resource, fetch, input, key); }, write(key: Key, value: Value): void { const entriesForResource = getEntriesForResource(resource); const resolvedResult: ResolvedResult = { status: Resolved, value, }; entriesForResource.set(key, resolvedResult); }, }; resourceConfigs.set(resource, config); return resource; } export function invalidateResources(): void { entries.clear(); }