react-relay 0.0.0-main-960835bc → 0.0.0-main-b925743b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ReactRelayContext.js +1 -1
- package/hooks.js +1 -1
- package/index.js +1 -1
- package/legacy.js +1 -1
- package/lib/relay-hooks/FragmentResource.js +241 -45
- package/lib/relay-hooks/QueryResource.js +81 -2
- package/lib/relay-hooks/SuspenseResource.js +130 -0
- package/package.json +2 -2
- package/react-relay-hooks.js +2 -2
- package/react-relay-hooks.min.js +2 -2
- package/react-relay-legacy.js +1 -1
- package/react-relay-legacy.min.js +1 -1
- package/react-relay.js +2 -2
- package/react-relay.min.js +2 -2
- package/relay-hooks/FragmentResource.js.flow +202 -14
- package/relay-hooks/QueryResource.js.flow +110 -4
- package/relay-hooks/SuspenseResource.js.flow +115 -0
| @@ -14,7 +14,10 @@ | |
| 14 14 | 
             
            'use strict';
         | 
| 15 15 |  | 
| 16 16 | 
             
            import type {Cache} from './LRUCache';
         | 
| 17 | 
            +
            import type {QueryResource, QueryResult} from './QueryResource';
         | 
| 17 18 | 
             
            import type {
         | 
| 19 | 
            +
              ConcreteRequest,
         | 
| 20 | 
            +
              DataID,
         | 
| 18 21 | 
             
              Disposable,
         | 
| 19 22 | 
             
              IEnvironment,
         | 
| 20 23 | 
             
              ReaderFragment,
         | 
| @@ -23,11 +26,17 @@ import type { | |
| 23 26 | 
             
            } from 'relay-runtime';
         | 
| 24 27 |  | 
| 25 28 | 
             
            const LRUCache = require('./LRUCache');
         | 
| 29 | 
            +
            const {getQueryResourceForEnvironment} = require('./QueryResource');
         | 
| 30 | 
            +
            const SuspenseResource = require('./SuspenseResource');
         | 
| 26 31 | 
             
            const invariant = require('invariant');
         | 
| 27 32 | 
             
            const {
         | 
| 33 | 
            +
              RelayFeatureFlags,
         | 
| 34 | 
            +
              __internal: {fetchQuery, getPromiseForActiveRequest},
         | 
| 35 | 
            +
              createOperationDescriptor,
         | 
| 28 36 | 
             
              getFragmentIdentifier,
         | 
| 29 37 | 
             
              getPendingOperationsForFragment,
         | 
| 30 38 | 
             
              getSelector,
         | 
| 39 | 
            +
              getVariablesFromFragment,
         | 
| 31 40 | 
             
              isPromise,
         | 
| 32 41 | 
             
              recycleNodesInto,
         | 
| 33 42 | 
             
              reportMissingRequiredFields,
         | 
| @@ -76,6 +85,24 @@ function isMissingData(snapshot: SingularOrPluralSnapshot): boolean { | |
| 76 85 | 
             
              return snapshot.isMissingData;
         | 
| 77 86 | 
             
            }
         | 
| 78 87 |  | 
| 88 | 
            +
            function hasMissingClientEdges(snapshot: SingularOrPluralSnapshot): boolean {
         | 
| 89 | 
            +
              if (Array.isArray(snapshot)) {
         | 
| 90 | 
            +
                return snapshot.some(s => (s.missingClientEdges?.length ?? 0) > 0);
         | 
| 91 | 
            +
              }
         | 
| 92 | 
            +
              return (snapshot.missingClientEdges?.length ?? 0) > 0;
         | 
| 93 | 
            +
            }
         | 
| 94 | 
            +
             | 
| 95 | 
            +
            function singularOrPluralForEach(
         | 
| 96 | 
            +
              snapshot: SingularOrPluralSnapshot,
         | 
| 97 | 
            +
              f: Snapshot => void,
         | 
| 98 | 
            +
            ): void {
         | 
| 99 | 
            +
              if (Array.isArray(snapshot)) {
         | 
| 100 | 
            +
                snapshot.forEach(f);
         | 
| 101 | 
            +
              } else {
         | 
| 102 | 
            +
                f(snapshot);
         | 
| 103 | 
            +
              }
         | 
| 104 | 
            +
            }
         | 
| 105 | 
            +
             | 
| 79 106 | 
             
            function getFragmentResult(
         | 
| 80 107 | 
             
              cacheKey: string,
         | 
| 81 108 | 
             
              snapshot: SingularOrPluralSnapshot,
         | 
| @@ -99,13 +126,75 @@ function getFragmentResult( | |
| 99 126 | 
             
              };
         | 
| 100 127 | 
             
            }
         | 
| 101 128 |  | 
| 129 | 
            +
            /**
         | 
| 130 | 
            +
             * The purpose of this cache is to allow information to be passed from an
         | 
| 131 | 
            +
             * initial read which suspends through to the commit that follows a subsequent
         | 
| 132 | 
            +
             * successful read. Specifically, the QueryResource result for the data fetch
         | 
| 133 | 
            +
             * is passed through so that that query can be retained on commit.
         | 
| 134 | 
            +
             */
         | 
| 135 | 
            +
            class ClientEdgeQueryResultsCache {
         | 
| 136 | 
            +
              _cache: Map<string, [Array<QueryResult>, SuspenseResource]> = new Map();
         | 
| 137 | 
            +
              _retainCounts: Map<string, number> = new Map();
         | 
| 138 | 
            +
              _environment: IEnvironment;
         | 
| 139 | 
            +
             | 
| 140 | 
            +
              constructor(environment: IEnvironment) {
         | 
| 141 | 
            +
                this._environment = environment;
         | 
| 142 | 
            +
              }
         | 
| 143 | 
            +
             | 
| 144 | 
            +
              get(fragmentIdentifier: string): void | Array<QueryResult> {
         | 
| 145 | 
            +
                return this._cache.get(fragmentIdentifier)?.[0] ?? undefined;
         | 
| 146 | 
            +
              }
         | 
| 147 | 
            +
             | 
| 148 | 
            +
              recordQueryResults(
         | 
| 149 | 
            +
                fragmentIdentifier: string,
         | 
| 150 | 
            +
                value: Array<QueryResult>, // may be mutated after being passed here
         | 
| 151 | 
            +
              ): void {
         | 
| 152 | 
            +
                const existing = this._cache.get(fragmentIdentifier);
         | 
| 153 | 
            +
                if (!existing) {
         | 
| 154 | 
            +
                  const suspenseResource = new SuspenseResource(() =>
         | 
| 155 | 
            +
                    this._retain(fragmentIdentifier),
         | 
| 156 | 
            +
                  );
         | 
| 157 | 
            +
                  this._cache.set(fragmentIdentifier, [value, suspenseResource]);
         | 
| 158 | 
            +
                  suspenseResource.temporaryRetain(this._environment);
         | 
| 159 | 
            +
                } else {
         | 
| 160 | 
            +
                  const [existingResults, suspenseResource] = existing;
         | 
| 161 | 
            +
                  value.forEach(queryResult => {
         | 
| 162 | 
            +
                    existingResults.push(queryResult);
         | 
| 163 | 
            +
                  });
         | 
| 164 | 
            +
                  suspenseResource.temporaryRetain(this._environment);
         | 
| 165 | 
            +
                }
         | 
| 166 | 
            +
              }
         | 
| 167 | 
            +
             | 
| 168 | 
            +
              _retain(id) {
         | 
| 169 | 
            +
                const retainCount = (this._retainCounts.get(id) ?? 0) + 1;
         | 
| 170 | 
            +
                this._retainCounts.set(id, retainCount);
         | 
| 171 | 
            +
                return {
         | 
| 172 | 
            +
                  dispose: () => {
         | 
| 173 | 
            +
                    const newRetainCount = (this._retainCounts.get(id) ?? 0) - 1;
         | 
| 174 | 
            +
                    if (newRetainCount > 0) {
         | 
| 175 | 
            +
                      this._retainCounts.set(id, newRetainCount);
         | 
| 176 | 
            +
                    } else {
         | 
| 177 | 
            +
                      this._retainCounts.delete(id);
         | 
| 178 | 
            +
                      this._cache.delete(id);
         | 
| 179 | 
            +
                    }
         | 
| 180 | 
            +
                  },
         | 
| 181 | 
            +
                };
         | 
| 182 | 
            +
              }
         | 
| 183 | 
            +
            }
         | 
| 184 | 
            +
             | 
| 102 185 | 
             
            class FragmentResourceImpl {
         | 
| 103 186 | 
             
              _environment: IEnvironment;
         | 
| 104 187 | 
             
              _cache: FragmentResourceCache;
         | 
| 188 | 
            +
              _clientEdgeQueryResultsCache: void | ClientEdgeQueryResultsCache;
         | 
| 105 189 |  | 
| 106 190 | 
             
              constructor(environment: IEnvironment) {
         | 
| 107 191 | 
             
                this._environment = environment;
         | 
| 108 192 | 
             
                this._cache = LRUCache.create(CACHE_CAPACITY);
         | 
| 193 | 
            +
                if (RelayFeatureFlags.ENABLE_CLIENT_EDGES) {
         | 
| 194 | 
            +
                  this._clientEdgeQueryResultsCache = new ClientEdgeQueryResultsCache(
         | 
| 195 | 
            +
                    environment,
         | 
| 196 | 
            +
                  );
         | 
| 197 | 
            +
                }
         | 
| 109 198 | 
             
              }
         | 
| 110 199 |  | 
| 111 200 | 
             
              /**
         | 
| @@ -250,25 +339,75 @@ class FragmentResourceImpl { | |
| 250 339 | 
             
                  return fragmentResult;
         | 
| 251 340 | 
             
                }
         | 
| 252 341 |  | 
| 253 | 
            -
                // 3. If we don't have data in the store,  | 
| 254 | 
            -
                //  | 
| 255 | 
            -
                //  | 
| 256 | 
            -
                //  | 
| 257 | 
            -
                //  | 
| 342 | 
            +
                // 3. If we don't have data in the store, there's two cases where we should
         | 
| 343 | 
            +
                // suspend to await the data: First if any client edges were traversed where
         | 
| 344 | 
            +
                // the destination record was missing data; in that case we initiate a query
         | 
| 345 | 
            +
                // here to fetch the missing data. Second, there may already be a request
         | 
| 346 | 
            +
                // in flight for the fragment's parent query, or for another operation that
         | 
| 347 | 
            +
                //  may affect the parent's query data, such as a mutation or subscription.
         | 
| 348 | 
            +
                // For any of these cases we can get a promise, which we will cache and
         | 
| 349 | 
            +
                // suspend on.
         | 
| 350 | 
            +
             | 
| 351 | 
            +
                // First, initiate a query for any client edges that were missing data:
         | 
| 352 | 
            +
                let clientEdgeRequests: ?Array<RequestDescriptor> = null;
         | 
| 353 | 
            +
                if (
         | 
| 354 | 
            +
                  RelayFeatureFlags.ENABLE_CLIENT_EDGES &&
         | 
| 355 | 
            +
                  hasMissingClientEdges(snapshot)
         | 
| 356 | 
            +
                ) {
         | 
| 357 | 
            +
                  clientEdgeRequests = [];
         | 
| 358 | 
            +
                  const queryResource = getQueryResourceForEnvironment(this._environment);
         | 
| 359 | 
            +
                  const queryResults = [];
         | 
| 360 | 
            +
                  singularOrPluralForEach(snapshot, snap => {
         | 
| 361 | 
            +
                    snap.missingClientEdges?.forEach(
         | 
| 362 | 
            +
                      ({request, clientEdgeDestinationID}) => {
         | 
| 363 | 
            +
                        const {queryResult, requestDescriptor} =
         | 
| 364 | 
            +
                          this._performClientEdgeQuery(
         | 
| 365 | 
            +
                            queryResource,
         | 
| 366 | 
            +
                            fragmentNode,
         | 
| 367 | 
            +
                            fragmentRef,
         | 
| 368 | 
            +
                            request,
         | 
| 369 | 
            +
                            clientEdgeDestinationID,
         | 
| 370 | 
            +
                          );
         | 
| 371 | 
            +
                        queryResults.push(queryResult);
         | 
| 372 | 
            +
                        clientEdgeRequests?.push(requestDescriptor);
         | 
| 373 | 
            +
                      },
         | 
| 374 | 
            +
                    );
         | 
| 375 | 
            +
                  });
         | 
| 376 | 
            +
                  // Store the query so that it can be retained when our own fragment is
         | 
| 377 | 
            +
                  // subscribed to. This merges with any existing query results:
         | 
| 378 | 
            +
                  invariant(
         | 
| 379 | 
            +
                    this._clientEdgeQueryResultsCache != null,
         | 
| 380 | 
            +
                    'Client edge query result cache should exist when ENABLE_CLIENT_EDGES is on.',
         | 
| 381 | 
            +
                  );
         | 
| 382 | 
            +
                  this._clientEdgeQueryResultsCache.recordQueryResults(
         | 
| 383 | 
            +
                    fragmentIdentifier,
         | 
| 384 | 
            +
                    queryResults,
         | 
| 385 | 
            +
                  );
         | 
| 386 | 
            +
                }
         | 
| 387 | 
            +
                let clientEdgePromises = null;
         | 
| 388 | 
            +
                if (RelayFeatureFlags.ENABLE_CLIENT_EDGES && clientEdgeRequests) {
         | 
| 389 | 
            +
                  clientEdgePromises = clientEdgeRequests
         | 
| 390 | 
            +
                    .map(request => getPromiseForActiveRequest(this._environment, request))
         | 
| 391 | 
            +
                    .filter(p => p != null);
         | 
| 392 | 
            +
                }
         | 
| 393 | 
            +
             | 
| 394 | 
            +
                // Finally look for operations in flight for our parent query:
         | 
| 258 395 | 
             
                const fragmentOwner =
         | 
| 259 396 | 
             
                  fragmentSelector.kind === 'PluralReaderSelector'
         | 
| 260 397 | 
             
                    ? fragmentSelector.selectors[0].owner
         | 
| 261 398 | 
             
                    : fragmentSelector.owner;
         | 
| 262 | 
            -
                const  | 
| 399 | 
            +
                const parentQueryPromiseResult =
         | 
| 263 400 | 
             
                  this._getAndSavePromiseForFragmentRequestInFlight(
         | 
| 264 401 | 
             
                    fragmentIdentifier,
         | 
| 265 402 | 
             
                    fragmentNode,
         | 
| 266 403 | 
             
                    fragmentOwner,
         | 
| 267 404 | 
             
                    fragmentResult,
         | 
| 268 405 | 
             
                  );
         | 
| 406 | 
            +
                const parentQueryPromiseResultPromise = parentQueryPromiseResult?.promise; // for refinement
         | 
| 407 | 
            +
             | 
| 269 408 | 
             
                if (
         | 
| 270 | 
            -
                   | 
| 271 | 
            -
                  isPromise( | 
| 409 | 
            +
                  clientEdgePromises?.length ||
         | 
| 410 | 
            +
                  isPromise(parentQueryPromiseResultPromise)
         | 
| 272 411 | 
             
                ) {
         | 
| 273 412 | 
             
                  environment.__log({
         | 
| 274 413 | 
             
                    name: 'suspense.fragment',
         | 
| @@ -277,15 +416,52 @@ class FragmentResourceImpl { | |
| 277 416 | 
             
                    isRelayHooks: true,
         | 
| 278 417 | 
             
                    isPromiseCached: false,
         | 
| 279 418 | 
             
                    isMissingData: fragmentResult.isMissingData,
         | 
| 280 | 
            -
                    pendingOperations:  | 
| 419 | 
            +
                    pendingOperations: [
         | 
| 420 | 
            +
                      ...(parentQueryPromiseResult?.pendingOperations ?? []),
         | 
| 421 | 
            +
                      ...(clientEdgeRequests ?? []),
         | 
| 422 | 
            +
                    ],
         | 
| 281 423 | 
             
                  });
         | 
| 282 | 
            -
                  throw  | 
| 424 | 
            +
                  throw clientEdgePromises?.length
         | 
| 425 | 
            +
                    ? Promise.all([parentQueryPromiseResultPromise, ...clientEdgePromises])
         | 
| 426 | 
            +
                    : parentQueryPromiseResultPromise;
         | 
| 283 427 | 
             
                }
         | 
| 284 428 |  | 
| 285 429 | 
             
                this._reportMissingRequiredFieldsInSnapshot(snapshot);
         | 
| 286 430 | 
             
                return getFragmentResult(fragmentIdentifier, snapshot, storeEpoch);
         | 
| 287 431 | 
             
              }
         | 
| 288 432 |  | 
| 433 | 
            +
              _performClientEdgeQuery(
         | 
| 434 | 
            +
                queryResource: QueryResource,
         | 
| 435 | 
            +
                fragmentNode: ReaderFragment,
         | 
| 436 | 
            +
                fragmentRef: mixed,
         | 
| 437 | 
            +
                request: ConcreteRequest,
         | 
| 438 | 
            +
                clientEdgeDestinationID: DataID,
         | 
| 439 | 
            +
              ) {
         | 
| 440 | 
            +
                const originalVariables = getVariablesFromFragment(
         | 
| 441 | 
            +
                  fragmentNode,
         | 
| 442 | 
            +
                  fragmentRef,
         | 
| 443 | 
            +
                );
         | 
| 444 | 
            +
                const variables = {
         | 
| 445 | 
            +
                  ...originalVariables,
         | 
| 446 | 
            +
                  id: clientEdgeDestinationID, // TODO should be a reserved name
         | 
| 447 | 
            +
                };
         | 
| 448 | 
            +
                const operation = createOperationDescriptor(
         | 
| 449 | 
            +
                  request,
         | 
| 450 | 
            +
                  variables,
         | 
| 451 | 
            +
                  {}, //  TODO cacheConfig should probably inherent from parent operation
         | 
| 452 | 
            +
                );
         | 
| 453 | 
            +
                const fetchObservable = fetchQuery(this._environment, operation);
         | 
| 454 | 
            +
                const queryResult = queryResource.prepare(
         | 
| 455 | 
            +
                  operation,
         | 
| 456 | 
            +
                  fetchObservable,
         | 
| 457 | 
            +
                  // TODO should inherent render policy etc. from parent operation
         | 
| 458 | 
            +
                );
         | 
| 459 | 
            +
                return {
         | 
| 460 | 
            +
                  requestDescriptor: operation.request,
         | 
| 461 | 
            +
                  queryResult,
         | 
| 462 | 
            +
                };
         | 
| 463 | 
            +
              }
         | 
| 464 | 
            +
             | 
| 289 465 | 
             
              _reportMissingRequiredFieldsInSnapshot(snapshot: SingularOrPluralSnapshot) {
         | 
| 290 466 | 
             
                if (Array.isArray(snapshot)) {
         | 
| 291 467 | 
             
                  snapshot.forEach(s => {
         | 
| @@ -343,7 +519,7 @@ class FragmentResourceImpl { | |
| 343 519 | 
             
                }
         | 
| 344 520 |  | 
| 345 521 | 
             
                // 3. Establish subscriptions on the snapshot(s)
         | 
| 346 | 
            -
                const  | 
| 522 | 
            +
                const disposables = [];
         | 
| 347 523 | 
             
                if (Array.isArray(renderedSnapshot)) {
         | 
| 348 524 | 
             
                  invariant(
         | 
| 349 525 | 
             
                    Array.isArray(currentSnapshot),
         | 
| @@ -351,7 +527,7 @@ class FragmentResourceImpl { | |
| 351 527 | 
             
                      "If you're seeing this, this is likely a bug in Relay.",
         | 
| 352 528 | 
             
                  );
         | 
| 353 529 | 
             
                  currentSnapshot.forEach((snapshot, idx) => {
         | 
| 354 | 
            -
                     | 
| 530 | 
            +
                    disposables.push(
         | 
| 355 531 | 
             
                      environment.subscribe(snapshot, latestSnapshot => {
         | 
| 356 532 | 
             
                        const storeEpoch = environment.getStore().getEpoch();
         | 
| 357 533 | 
             
                        this._updatePluralSnapshot(
         | 
| @@ -371,7 +547,7 @@ class FragmentResourceImpl { | |
| 371 547 | 
             
                    'Relay: Expected snapshot to be singular. ' +
         | 
| 372 548 | 
             
                      "If you're seeing this, this is likely a bug in Relay.",
         | 
| 373 549 | 
             
                  );
         | 
| 374 | 
            -
                   | 
| 550 | 
            +
                  disposables.push(
         | 
| 375 551 | 
             
                    environment.subscribe(currentSnapshot, latestSnapshot => {
         | 
| 376 552 | 
             
                      const storeEpoch = environment.getStore().getEpoch();
         | 
| 377 553 | 
             
                      this._cache.set(cacheKey, {
         | 
| @@ -383,9 +559,20 @@ class FragmentResourceImpl { | |
| 383 559 | 
             
                  );
         | 
| 384 560 | 
             
                }
         | 
| 385 561 |  | 
| 562 | 
            +
                if (RelayFeatureFlags.ENABLE_CLIENT_EDGES) {
         | 
| 563 | 
            +
                  const clientEdgeQueryResults =
         | 
| 564 | 
            +
                    this._clientEdgeQueryResultsCache?.get(cacheKey) ?? undefined;
         | 
| 565 | 
            +
                  if (clientEdgeQueryResults?.length) {
         | 
| 566 | 
            +
                    const queryResource = getQueryResourceForEnvironment(this._environment);
         | 
| 567 | 
            +
                    clientEdgeQueryResults.forEach(queryResult => {
         | 
| 568 | 
            +
                      disposables.push(queryResource.retain(queryResult));
         | 
| 569 | 
            +
                    });
         | 
| 570 | 
            +
                  }
         | 
| 571 | 
            +
                }
         | 
| 572 | 
            +
             | 
| 386 573 | 
             
                return {
         | 
| 387 574 | 
             
                  dispose: () => {
         | 
| 388 | 
            -
                     | 
| 575 | 
            +
                    disposables.forEach(s => s.dispose());
         | 
| 389 576 | 
             
                    this._cache.delete(cacheKey);
         | 
| 390 577 | 
             
                  },
         | 
| 391 578 | 
             
                };
         | 
| @@ -456,6 +643,7 @@ class FragmentResourceImpl { | |
| 456 643 | 
             
                const updatedCurrentSnapshot: Snapshot = {
         | 
| 457 644 | 
             
                  data: updatedData,
         | 
| 458 645 | 
             
                  isMissingData: currentSnapshot.isMissingData,
         | 
| 646 | 
            +
                  missingClientEdges: currentSnapshot.missingClientEdges,
         | 
| 459 647 | 
             
                  seenRecords: currentSnapshot.seenRecords,
         | 
| 460 648 | 
             
                  selector: currentSnapshot.selector,
         | 
| 461 649 | 
             
                  missingRequiredFields: currentSnapshot.missingRequiredFields,
         | 
| @@ -30,13 +30,13 @@ import type { | |
| 30 30 | 
             
            } from 'relay-runtime';
         | 
| 31 31 |  | 
| 32 32 | 
             
            const LRUCache = require('./LRUCache');
         | 
| 33 | 
            +
            const SuspenseResource = require('./SuspenseResource');
         | 
| 33 34 | 
             
            const invariant = require('invariant');
         | 
| 34 | 
            -
            const {isPromise} = require('relay-runtime');
         | 
| 35 | 
            +
            const {RelayFeatureFlags, isPromise} = require('relay-runtime');
         | 
| 35 36 | 
             
            const warning = require('warning');
         | 
| 36 37 |  | 
| 37 38 | 
             
            const CACHE_CAPACITY = 1000;
         | 
| 38 39 | 
             
            const DEFAULT_FETCH_POLICY = 'store-or-network';
         | 
| 39 | 
            -
            const DATA_RETENTION_TIMEOUT = 5 * 60 * 1000;
         | 
| 40 40 |  | 
| 41 41 | 
             
            export type QueryResource = QueryResourceImpl;
         | 
| 42 42 |  | 
| @@ -59,7 +59,7 @@ type QueryResourceCacheEntry = {| | |
| 59 59 | 
             
              permanentRetain(environment: IEnvironment): Disposable,
         | 
| 60 60 | 
             
              releaseTemporaryRetain(): void,
         | 
| 61 61 | 
             
            |};
         | 
| 62 | 
            -
            opaque type QueryResult: {
         | 
| 62 | 
            +
            export opaque type QueryResult: {
         | 
| 63 63 | 
             
              fragmentNode: ReaderFragment,
         | 
| 64 64 | 
             
              fragmentRef: mixed,
         | 
| 65 65 | 
             
              ...
         | 
| @@ -125,6 +125,106 @@ function createCacheEntry( | |
| 125 125 | 
             
              value: Error | Promise<void> | QueryResult,
         | 
| 126 126 | 
             
              networkSubscription: ?Subscription,
         | 
| 127 127 | 
             
              onDispose: QueryResourceCacheEntry => void,
         | 
| 128 | 
            +
            ): QueryResourceCacheEntry {
         | 
| 129 | 
            +
              // There should be no behavior difference between createCacheEntry_new and
         | 
| 130 | 
            +
              // createCacheEntry_old, and it doesn't directly relate to Client Edges.
         | 
| 131 | 
            +
              // It was just a refactoring that was needed for Client Edges but that
         | 
| 132 | 
            +
              // is behind the feature flag just in case there is any accidental breakage.
         | 
| 133 | 
            +
              if (RelayFeatureFlags.REFACTOR_SUSPENSE_RESOURCE) {
         | 
| 134 | 
            +
                return createCacheEntry_new(
         | 
| 135 | 
            +
                  cacheIdentifier,
         | 
| 136 | 
            +
                  operation,
         | 
| 137 | 
            +
                  operationAvailability,
         | 
| 138 | 
            +
                  value,
         | 
| 139 | 
            +
                  networkSubscription,
         | 
| 140 | 
            +
                  onDispose,
         | 
| 141 | 
            +
                );
         | 
| 142 | 
            +
              } else {
         | 
| 143 | 
            +
                return createCacheEntry_old(
         | 
| 144 | 
            +
                  cacheIdentifier,
         | 
| 145 | 
            +
                  operation,
         | 
| 146 | 
            +
                  operationAvailability,
         | 
| 147 | 
            +
                  value,
         | 
| 148 | 
            +
                  networkSubscription,
         | 
| 149 | 
            +
                  onDispose,
         | 
| 150 | 
            +
                );
         | 
| 151 | 
            +
              }
         | 
| 152 | 
            +
            }
         | 
| 153 | 
            +
             | 
| 154 | 
            +
            function createCacheEntry_new(
         | 
| 155 | 
            +
              cacheIdentifier: string,
         | 
| 156 | 
            +
              operation: OperationDescriptor,
         | 
| 157 | 
            +
              operationAvailability: ?OperationAvailability,
         | 
| 158 | 
            +
              value: Error | Promise<void> | QueryResult,
         | 
| 159 | 
            +
              networkSubscription: ?Subscription,
         | 
| 160 | 
            +
              onDispose: QueryResourceCacheEntry => void,
         | 
| 161 | 
            +
            ): QueryResourceCacheEntry {
         | 
| 162 | 
            +
              const isLiveQuery = operationIsLiveQuery(operation);
         | 
| 163 | 
            +
             | 
| 164 | 
            +
              let currentValue: Error | Promise<void> | QueryResult = value;
         | 
| 165 | 
            +
              let currentNetworkSubscription: ?Subscription = networkSubscription;
         | 
| 166 | 
            +
             | 
| 167 | 
            +
              const suspenseResource = new SuspenseResource(environment => {
         | 
| 168 | 
            +
                const retention = environment.retain(operation);
         | 
| 169 | 
            +
                return {
         | 
| 170 | 
            +
                  dispose: () => {
         | 
| 171 | 
            +
                    // Normally if this entry never commits, the request would've ended by the
         | 
| 172 | 
            +
                    // time this timeout expires and the temporary retain is released. However,
         | 
| 173 | 
            +
                    // we need to do this for live queries which remain open indefinitely.
         | 
| 174 | 
            +
                    if (isLiveQuery && currentNetworkSubscription != null) {
         | 
| 175 | 
            +
                      currentNetworkSubscription.unsubscribe();
         | 
| 176 | 
            +
                    }
         | 
| 177 | 
            +
                    retention.dispose();
         | 
| 178 | 
            +
                    onDispose(cacheEntry);
         | 
| 179 | 
            +
                  },
         | 
| 180 | 
            +
                };
         | 
| 181 | 
            +
              });
         | 
| 182 | 
            +
             | 
| 183 | 
            +
              const cacheEntry = {
         | 
| 184 | 
            +
                cacheIdentifier,
         | 
| 185 | 
            +
                id: nextID++,
         | 
| 186 | 
            +
                processedPayloadsCount: 0,
         | 
| 187 | 
            +
                operationAvailability,
         | 
| 188 | 
            +
                getValue() {
         | 
| 189 | 
            +
                  return currentValue;
         | 
| 190 | 
            +
                },
         | 
| 191 | 
            +
                setValue(val: QueryResult | Promise<void> | Error) {
         | 
| 192 | 
            +
                  currentValue = val;
         | 
| 193 | 
            +
                },
         | 
| 194 | 
            +
                getRetainCount() {
         | 
| 195 | 
            +
                  return suspenseResource.getRetainCount();
         | 
| 196 | 
            +
                },
         | 
| 197 | 
            +
                getNetworkSubscription() {
         | 
| 198 | 
            +
                  return currentNetworkSubscription;
         | 
| 199 | 
            +
                },
         | 
| 200 | 
            +
                setNetworkSubscription(subscription: ?Subscription) {
         | 
| 201 | 
            +
                  if (isLiveQuery && currentNetworkSubscription != null) {
         | 
| 202 | 
            +
                    currentNetworkSubscription.unsubscribe();
         | 
| 203 | 
            +
                  }
         | 
| 204 | 
            +
                  currentNetworkSubscription = subscription;
         | 
| 205 | 
            +
                },
         | 
| 206 | 
            +
                temporaryRetain(environment: IEnvironment): Disposable {
         | 
| 207 | 
            +
                  return suspenseResource.temporaryRetain(environment);
         | 
| 208 | 
            +
                },
         | 
| 209 | 
            +
                permanentRetain(environment: IEnvironment): Disposable {
         | 
| 210 | 
            +
                  return suspenseResource.permanentRetain(environment);
         | 
| 211 | 
            +
                },
         | 
| 212 | 
            +
                releaseTemporaryRetain() {
         | 
| 213 | 
            +
                  suspenseResource.releaseTemporaryRetain();
         | 
| 214 | 
            +
                },
         | 
| 215 | 
            +
              };
         | 
| 216 | 
            +
             | 
| 217 | 
            +
              return cacheEntry;
         | 
| 218 | 
            +
            }
         | 
| 219 | 
            +
             | 
| 220 | 
            +
            const DATA_RETENTION_TIMEOUT = 5 * 60 * 1000;
         | 
| 221 | 
            +
            function createCacheEntry_old(
         | 
| 222 | 
            +
              cacheIdentifier: string,
         | 
| 223 | 
            +
              operation: OperationDescriptor,
         | 
| 224 | 
            +
              operationAvailability: ?OperationAvailability,
         | 
| 225 | 
            +
              value: Error | Promise<void> | QueryResult,
         | 
| 226 | 
            +
              networkSubscription: ?Subscription,
         | 
| 227 | 
            +
              onDispose: QueryResourceCacheEntry => void,
         | 
| 128 228 | 
             
            ): QueryResourceCacheEntry {
         | 
| 129 229 | 
             
              const isLiveQuery = operationIsLiveQuery(operation);
         | 
| 130 230 |  | 
| @@ -432,8 +532,14 @@ class QueryResourceImpl { | |
| 432 532 | 
             
              }
         | 
| 433 533 |  | 
| 434 534 | 
             
              _clearCacheEntry = (cacheEntry: QueryResourceCacheEntry): void => {
         | 
| 435 | 
            -
                 | 
| 535 | 
            +
                // The new code does this retainCount <= 0 check within SuspenseResource
         | 
| 536 | 
            +
                // before calling _clearCacheEntry, whereas with the old code we do it here.
         | 
| 537 | 
            +
                if (RelayFeatureFlags.REFACTOR_SUSPENSE_RESOURCE) {
         | 
| 436 538 | 
             
                  this._cache.delete(cacheEntry.cacheIdentifier);
         | 
| 539 | 
            +
                } else {
         | 
| 540 | 
            +
                  if (cacheEntry.getRetainCount() <= 0) {
         | 
| 541 | 
            +
                    this._cache.delete(cacheEntry.cacheIdentifier);
         | 
| 542 | 
            +
                  }
         | 
| 437 543 | 
             
                }
         | 
| 438 544 | 
             
              };
         | 
| 439 545 |  | 
| @@ -0,0 +1,115 @@ | |
| 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 strict-local
         | 
| 8 | 
            +
             * @emails oncall+relay
         | 
| 9 | 
            +
             * @format
         | 
| 10 | 
            +
             */
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            // flowlint ambiguous-object-type:error
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            'use strict';
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            import type {Disposable, IEnvironment} from 'relay-runtime';
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            const invariant = require('invariant');
         | 
| 19 | 
            +
             | 
| 20 | 
            +
            const TEMPORARY_RETAIN_DURATION_MS = 5 * 60 * 1000;
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            /**
         | 
| 23 | 
            +
             * Allows you to retain a resource as part of a component lifecycle accounting
         | 
| 24 | 
            +
             * for Suspense. You temporarily retain the resource during render, then
         | 
| 25 | 
            +
             * permanently retain it during commit and release it during unmount.
         | 
| 26 | 
            +
             */
         | 
| 27 | 
            +
            class SuspenseResource {
         | 
| 28 | 
            +
              _retainCount = 0;
         | 
| 29 | 
            +
              _retainDisposable: ?Disposable = null;
         | 
| 30 | 
            +
              _releaseTemporaryRetain: ?() => void = null;
         | 
| 31 | 
            +
              _retain: IEnvironment => Disposable;
         | 
| 32 | 
            +
             | 
| 33 | 
            +
              constructor(retain: (environment: IEnvironment) => Disposable) {
         | 
| 34 | 
            +
                this._retain = (environment: IEnvironment): Disposable => {
         | 
| 35 | 
            +
                  this._retainCount++;
         | 
| 36 | 
            +
                  if (this._retainCount === 1) {
         | 
| 37 | 
            +
                    this._retainDisposable = retain(environment);
         | 
| 38 | 
            +
                  }
         | 
| 39 | 
            +
                  return {
         | 
| 40 | 
            +
                    dispose: () => {
         | 
| 41 | 
            +
                      this._retainCount = Math.max(0, this._retainCount - 1);
         | 
| 42 | 
            +
                      if (this._retainCount === 0) {
         | 
| 43 | 
            +
                        invariant(
         | 
| 44 | 
            +
                          this._retainDisposable != null,
         | 
| 45 | 
            +
                          'Relay: Expected disposable to release query to be defined.' +
         | 
| 46 | 
            +
                            "If you're seeing this, this is likely a bug in Relay.",
         | 
| 47 | 
            +
                        );
         | 
| 48 | 
            +
                        this._retainDisposable.dispose();
         | 
| 49 | 
            +
                        this._retainDisposable = null;
         | 
| 50 | 
            +
                      }
         | 
| 51 | 
            +
                    },
         | 
| 52 | 
            +
                  };
         | 
| 53 | 
            +
                };
         | 
| 54 | 
            +
              }
         | 
| 55 | 
            +
             | 
| 56 | 
            +
              temporaryRetain(environment: IEnvironment): Disposable {
         | 
| 57 | 
            +
                // If we're executing in a server environment, there's no need
         | 
| 58 | 
            +
                // to create temporary retains, since the component will never commit.
         | 
| 59 | 
            +
                if (environment.isServer()) {
         | 
| 60 | 
            +
                  return {dispose: () => {}};
         | 
| 61 | 
            +
                }
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                // temporaryRetain is called during the render phase. However,
         | 
| 64 | 
            +
                // given that we can't tell if this render will eventually commit or not,
         | 
| 65 | 
            +
                // we create a timer to autodispose of this retain in case the associated
         | 
| 66 | 
            +
                // component never commits.
         | 
| 67 | 
            +
                // If the component /does/ commit, permanentRetain will clear this timeout
         | 
| 68 | 
            +
                // and permanently retain the data.
         | 
| 69 | 
            +
                const retention = this._retain(environment);
         | 
| 70 | 
            +
                let releaseQueryTimeout = null;
         | 
| 71 | 
            +
                const releaseTemporaryRetain = () => {
         | 
| 72 | 
            +
                  clearTimeout(releaseQueryTimeout);
         | 
| 73 | 
            +
                  releaseQueryTimeout = null;
         | 
| 74 | 
            +
                  this._releaseTemporaryRetain = null;
         | 
| 75 | 
            +
                  retention.dispose();
         | 
| 76 | 
            +
                };
         | 
| 77 | 
            +
                releaseQueryTimeout = setTimeout(
         | 
| 78 | 
            +
                  releaseTemporaryRetain,
         | 
| 79 | 
            +
                  TEMPORARY_RETAIN_DURATION_MS,
         | 
| 80 | 
            +
                );
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                // NOTE: Since temporaryRetain can be called multiple times, we release
         | 
| 83 | 
            +
                // the previous temporary retain after we re-establish a new one, since
         | 
| 84 | 
            +
                // we only ever need a single temporary retain until the permanent retain is
         | 
| 85 | 
            +
                // established.
         | 
| 86 | 
            +
                // temporaryRetain may be called multiple times by React during the render
         | 
| 87 | 
            +
                // phase, as well as multiple times by other query components that are
         | 
| 88 | 
            +
                // rendering the same query/variables.
         | 
| 89 | 
            +
                this._releaseTemporaryRetain?.();
         | 
| 90 | 
            +
                this._releaseTemporaryRetain = releaseTemporaryRetain;
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                return {
         | 
| 93 | 
            +
                  dispose: () => {
         | 
| 94 | 
            +
                    this._releaseTemporaryRetain?.();
         | 
| 95 | 
            +
                  },
         | 
| 96 | 
            +
                };
         | 
| 97 | 
            +
              }
         | 
| 98 | 
            +
             | 
| 99 | 
            +
              permanentRetain(environment: IEnvironment): Disposable {
         | 
| 100 | 
            +
                const disposable = this._retain(environment);
         | 
| 101 | 
            +
                this.releaseTemporaryRetain();
         | 
| 102 | 
            +
                return disposable;
         | 
| 103 | 
            +
              }
         | 
| 104 | 
            +
             | 
| 105 | 
            +
              releaseTemporaryRetain(): void {
         | 
| 106 | 
            +
                this._releaseTemporaryRetain?.();
         | 
| 107 | 
            +
                this._releaseTemporaryRetain = null;
         | 
| 108 | 
            +
              }
         | 
| 109 | 
            +
             | 
| 110 | 
            +
              getRetainCount(): number {
         | 
| 111 | 
            +
                return this._retainCount;
         | 
| 112 | 
            +
              }
         | 
| 113 | 
            +
            }
         | 
| 114 | 
            +
             | 
| 115 | 
            +
            module.exports = SuspenseResource;
         |