From 9ce48174b60ac29d2839d28812530b81732118a3 Mon Sep 17 00:00:00 2001 From: Iha Shin Date: Mon, 6 Jan 2025 15:41:34 -0800 Subject: [PATCH] Add plural fragment support to `observeFragment()` (#4862) Summary: This PR adds plural fragment support to `observeFragment()` (and `waitForFragmentData()` which uses `observeFragment()` under the hood) It should be noted that the Relay environment currently doesn't support subscribing to plural fragment updates, so the implementation is done by subscribing to each selector and merging the states. Since the current implementation doesn't do any batching of notifications that are made from each selector subscription, it can be somewhat inefficient if the consumer doesn't batch on their own. Pull Request resolved: https://github.com/facebook/relay/pull/4862 Reviewed By: tyao1 Differential Revision: D67148721 Pulled By: captbaritone fbshipit-source-id: 1c63178c78ece3588ac86b8814d9b8fbf2ce057b --- ...eFragmentTestListUpdateFragment.graphql.js | 61 +++++ ...erveFragmentTestListUpdateQuery.graphql.js | 137 +++++++++++ ...stMissingRequiredPluralFragment.graphql.js | 65 +++++ ...tTestMissingRequiredPluralQuery.graphql.js | 137 +++++++++++ ...serveFragmentTestPluralFragment.graphql.js | 61 +++++ .../observeFragmentTestPluralQuery.graphql.js | 137 +++++++++++ ...PluralThrowOnFieldErrorFragment.graphql.js | 62 +++++ ...estPluralThrowOnFieldErrorQuery.graphql.js | 137 +++++++++++ ...PluralThrowOnFieldErrorFragment.graphql.js | 78 ++++++ ...ithPluralThrowOnFieldErrorQuery.graphql.js | 146 +++++++++++ ...ragmentDataTestOkPluralFragment.graphql.js | 61 +++++ ...orFragmentDataTestOkPluralQuery.graphql.js | 137 +++++++++++ .../store/__tests__/observeFragment-test.js | 232 ++++++++++++++++++ .../__tests__/waitForFragmentData-test.js | 33 +++ .../store/observeFragmentExperimental.js | 94 +++++-- .../store/waitForFragmentExperimental.js | 7 +- .../relay-runtime/observe-fragment.md | 4 + 17 files changed, 1566 insertions(+), 23 deletions(-) create mode 100644 packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestListUpdateFragment.graphql.js create mode 100644 packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestListUpdateQuery.graphql.js create mode 100644 packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestMissingRequiredPluralFragment.graphql.js create mode 100644 packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestMissingRequiredPluralQuery.graphql.js create mode 100644 packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestPluralFragment.graphql.js create mode 100644 packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestPluralQuery.graphql.js create mode 100644 packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestPluralThrowOnFieldErrorFragment.graphql.js create mode 100644 packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestPluralThrowOnFieldErrorQuery.graphql.js create mode 100644 packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorFragment.graphql.js create mode 100644 packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorQuery.graphql.js create mode 100644 packages/relay-runtime/store/__tests__/__generated__/waitForFragmentDataTestOkPluralFragment.graphql.js create mode 100644 packages/relay-runtime/store/__tests__/__generated__/waitForFragmentDataTestOkPluralQuery.graphql.js diff --git a/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestListUpdateFragment.graphql.js b/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestListUpdateFragment.graphql.js new file mode 100644 index 0000000000000..dd62e819b9696 --- /dev/null +++ b/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestListUpdateFragment.graphql.js @@ -0,0 +1,61 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @oncall relay + * + * @generated SignedSource<> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { Fragment, ReaderFragment } from 'relay-runtime'; +import type { FragmentType } from "relay-runtime"; +declare export opaque type observeFragmentTestListUpdateFragment$fragmentType: FragmentType; +export type observeFragmentTestListUpdateFragment$data = $ReadOnlyArray<{| + +name: ?string, + +$fragmentType: observeFragmentTestListUpdateFragment$fragmentType, +|}>; +export type observeFragmentTestListUpdateFragment$key = $ReadOnlyArray<{ + +$data?: observeFragmentTestListUpdateFragment$data, + +$fragmentSpreads: observeFragmentTestListUpdateFragment$fragmentType, + ... +}>; +*/ + +var node/*: ReaderFragment*/ = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": { + "plural": true + }, + "name": "observeFragmentTestListUpdateFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "type": "User", + "abstractKey": null +}; + +if (__DEV__) { + (node/*: any*/).hash = "30d272ba4e5c5a9eb1d9a79015a69ec3"; +} + +module.exports = ((node/*: any*/)/*: Fragment< + observeFragmentTestListUpdateFragment$fragmentType, + observeFragmentTestListUpdateFragment$data, +>*/); diff --git a/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestListUpdateQuery.graphql.js b/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestListUpdateQuery.graphql.js new file mode 100644 index 0000000000000..70cac22b20f6e --- /dev/null +++ b/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestListUpdateQuery.graphql.js @@ -0,0 +1,137 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @oncall relay + * + * @generated SignedSource<<4337e2999734809a390206fb499f653e>> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ConcreteRequest, Query } from 'relay-runtime'; +import type { observeFragmentTestListUpdateFragment$fragmentType } from "./observeFragmentTestListUpdateFragment.graphql"; +export type observeFragmentTestListUpdateQuery$variables = {||}; +export type observeFragmentTestListUpdateQuery$data = {| + +nodes: ?$ReadOnlyArray, +|}; +export type observeFragmentTestListUpdateQuery = {| + response: observeFragmentTestListUpdateQuery$data, + variables: observeFragmentTestListUpdateQuery$variables, +|}; +*/ + +var node/*: ConcreteRequest*/ = (function(){ +var v0 = [ + { + "kind": "Literal", + "name": "ids", + "value": [ + "1", + "2" + ] + } +]; +return { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "observeFragmentTestListUpdateQuery", + "selections": [ + { + "alias": null, + "args": (v0/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "nodes", + "plural": true, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "observeFragmentTestListUpdateFragment" + } + ], + "storageKey": "nodes(ids:[\"1\",\"2\"])" + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "observeFragmentTestListUpdateQuery", + "selections": [ + { + "alias": null, + "args": (v0/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "nodes", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "type": "User", + "abstractKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "storageKey": "nodes(ids:[\"1\",\"2\"])" + } + ] + }, + "params": { + "cacheID": "17a222c7e13bc4a3b9c0c108377514da", + "id": null, + "metadata": {}, + "name": "observeFragmentTestListUpdateQuery", + "operationKind": "query", + "text": "query observeFragmentTestListUpdateQuery {\n nodes(ids: [\"1\", \"2\"]) {\n __typename\n ...observeFragmentTestListUpdateFragment\n id\n }\n}\n\nfragment observeFragmentTestListUpdateFragment on User {\n name\n}\n" + } +}; +})(); + +if (__DEV__) { + (node/*: any*/).hash = "493ccdbc127bfccc347fc16107f21b79"; +} + +module.exports = ((node/*: any*/)/*: Query< + observeFragmentTestListUpdateQuery$variables, + observeFragmentTestListUpdateQuery$data, +>*/); diff --git a/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestMissingRequiredPluralFragment.graphql.js b/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestMissingRequiredPluralFragment.graphql.js new file mode 100644 index 0000000000000..ba317d7cf63f9 --- /dev/null +++ b/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestMissingRequiredPluralFragment.graphql.js @@ -0,0 +1,65 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @oncall relay + * + * @generated SignedSource<<80e54ca26f89d2d71c3bb618efa15d49>> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { Fragment, ReaderFragment } from 'relay-runtime'; +import type { FragmentType } from "relay-runtime"; +declare export opaque type observeFragmentTestMissingRequiredPluralFragment$fragmentType: FragmentType; +export type observeFragmentTestMissingRequiredPluralFragment$data = $ReadOnlyArray<{| + +name: string, + +$fragmentType: observeFragmentTestMissingRequiredPluralFragment$fragmentType, +|}>; +export type observeFragmentTestMissingRequiredPluralFragment$key = $ReadOnlyArray<{ + +$data?: observeFragmentTestMissingRequiredPluralFragment$data, + +$fragmentSpreads: observeFragmentTestMissingRequiredPluralFragment$fragmentType, + ... +}>; +*/ + +var node/*: ReaderFragment*/ = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": { + "plural": true + }, + "name": "observeFragmentTestMissingRequiredPluralFragment", + "selections": [ + { + "kind": "RequiredField", + "field": { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + "action": "THROW" + } + ], + "type": "User", + "abstractKey": null +}; + +if (__DEV__) { + (node/*: any*/).hash = "0a62ca17583bf06a225f706e103dc11e"; +} + +module.exports = ((node/*: any*/)/*: Fragment< + observeFragmentTestMissingRequiredPluralFragment$fragmentType, + observeFragmentTestMissingRequiredPluralFragment$data, +>*/); diff --git a/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestMissingRequiredPluralQuery.graphql.js b/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestMissingRequiredPluralQuery.graphql.js new file mode 100644 index 0000000000000..d14055391be61 --- /dev/null +++ b/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestMissingRequiredPluralQuery.graphql.js @@ -0,0 +1,137 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @oncall relay + * + * @generated SignedSource<<0ee8f9cd4d1a4269af40172b0c55884d>> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ConcreteRequest, Query } from 'relay-runtime'; +import type { observeFragmentTestMissingRequiredPluralFragment$fragmentType } from "./observeFragmentTestMissingRequiredPluralFragment.graphql"; +export type observeFragmentTestMissingRequiredPluralQuery$variables = {||}; +export type observeFragmentTestMissingRequiredPluralQuery$data = {| + +nodes: ?$ReadOnlyArray, +|}; +export type observeFragmentTestMissingRequiredPluralQuery = {| + response: observeFragmentTestMissingRequiredPluralQuery$data, + variables: observeFragmentTestMissingRequiredPluralQuery$variables, +|}; +*/ + +var node/*: ConcreteRequest*/ = (function(){ +var v0 = [ + { + "kind": "Literal", + "name": "ids", + "value": [ + "1", + "2" + ] + } +]; +return { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "observeFragmentTestMissingRequiredPluralQuery", + "selections": [ + { + "alias": null, + "args": (v0/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "nodes", + "plural": true, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "observeFragmentTestMissingRequiredPluralFragment" + } + ], + "storageKey": "nodes(ids:[\"1\",\"2\"])" + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "observeFragmentTestMissingRequiredPluralQuery", + "selections": [ + { + "alias": null, + "args": (v0/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "nodes", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "type": "User", + "abstractKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "storageKey": "nodes(ids:[\"1\",\"2\"])" + } + ] + }, + "params": { + "cacheID": "d1213ecc01790c61de1e12d97a40c349", + "id": null, + "metadata": {}, + "name": "observeFragmentTestMissingRequiredPluralQuery", + "operationKind": "query", + "text": "query observeFragmentTestMissingRequiredPluralQuery {\n nodes(ids: [\"1\", \"2\"]) {\n __typename\n ...observeFragmentTestMissingRequiredPluralFragment\n id\n }\n}\n\nfragment observeFragmentTestMissingRequiredPluralFragment on User {\n name\n}\n" + } +}; +})(); + +if (__DEV__) { + (node/*: any*/).hash = "1eb9c5256c37c4d0e28695bb4dd64fa8"; +} + +module.exports = ((node/*: any*/)/*: Query< + observeFragmentTestMissingRequiredPluralQuery$variables, + observeFragmentTestMissingRequiredPluralQuery$data, +>*/); diff --git a/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestPluralFragment.graphql.js b/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestPluralFragment.graphql.js new file mode 100644 index 0000000000000..f5d80766c2a15 --- /dev/null +++ b/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestPluralFragment.graphql.js @@ -0,0 +1,61 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @oncall relay + * + * @generated SignedSource<> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { Fragment, ReaderFragment } from 'relay-runtime'; +import type { FragmentType } from "relay-runtime"; +declare export opaque type observeFragmentTestPluralFragment$fragmentType: FragmentType; +export type observeFragmentTestPluralFragment$data = $ReadOnlyArray<{| + +name: ?string, + +$fragmentType: observeFragmentTestPluralFragment$fragmentType, +|}>; +export type observeFragmentTestPluralFragment$key = $ReadOnlyArray<{ + +$data?: observeFragmentTestPluralFragment$data, + +$fragmentSpreads: observeFragmentTestPluralFragment$fragmentType, + ... +}>; +*/ + +var node/*: ReaderFragment*/ = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": { + "plural": true + }, + "name": "observeFragmentTestPluralFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "type": "User", + "abstractKey": null +}; + +if (__DEV__) { + (node/*: any*/).hash = "3b473578ee9b2f35ed7214e714f68334"; +} + +module.exports = ((node/*: any*/)/*: Fragment< + observeFragmentTestPluralFragment$fragmentType, + observeFragmentTestPluralFragment$data, +>*/); diff --git a/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestPluralQuery.graphql.js b/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestPluralQuery.graphql.js new file mode 100644 index 0000000000000..d6ed1fc180fc6 --- /dev/null +++ b/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestPluralQuery.graphql.js @@ -0,0 +1,137 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @oncall relay + * + * @generated SignedSource<<8ed3e238203c49288429f434ff0ef253>> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ConcreteRequest, Query } from 'relay-runtime'; +import type { observeFragmentTestPluralFragment$fragmentType } from "./observeFragmentTestPluralFragment.graphql"; +export type observeFragmentTestPluralQuery$variables = {||}; +export type observeFragmentTestPluralQuery$data = {| + +nodes: ?$ReadOnlyArray, +|}; +export type observeFragmentTestPluralQuery = {| + response: observeFragmentTestPluralQuery$data, + variables: observeFragmentTestPluralQuery$variables, +|}; +*/ + +var node/*: ConcreteRequest*/ = (function(){ +var v0 = [ + { + "kind": "Literal", + "name": "ids", + "value": [ + "1", + "2" + ] + } +]; +return { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "observeFragmentTestPluralQuery", + "selections": [ + { + "alias": null, + "args": (v0/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "nodes", + "plural": true, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "observeFragmentTestPluralFragment" + } + ], + "storageKey": "nodes(ids:[\"1\",\"2\"])" + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "observeFragmentTestPluralQuery", + "selections": [ + { + "alias": null, + "args": (v0/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "nodes", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "type": "User", + "abstractKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "storageKey": "nodes(ids:[\"1\",\"2\"])" + } + ] + }, + "params": { + "cacheID": "ccff1412490c5e62d70031d41db0c7e4", + "id": null, + "metadata": {}, + "name": "observeFragmentTestPluralQuery", + "operationKind": "query", + "text": "query observeFragmentTestPluralQuery {\n nodes(ids: [\"1\", \"2\"]) {\n __typename\n ...observeFragmentTestPluralFragment\n id\n }\n}\n\nfragment observeFragmentTestPluralFragment on User {\n name\n}\n" + } +}; +})(); + +if (__DEV__) { + (node/*: any*/).hash = "c1a2f68df2ec25bc00b077d6cdecdce4"; +} + +module.exports = ((node/*: any*/)/*: Query< + observeFragmentTestPluralQuery$variables, + observeFragmentTestPluralQuery$data, +>*/); diff --git a/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestPluralThrowOnFieldErrorFragment.graphql.js b/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestPluralThrowOnFieldErrorFragment.graphql.js new file mode 100644 index 0000000000000..122853d5c2de7 --- /dev/null +++ b/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestPluralThrowOnFieldErrorFragment.graphql.js @@ -0,0 +1,62 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @oncall relay + * + * @generated SignedSource<<73dfa993cd14eb071971ec8ae446eea0>> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { Fragment, ReaderFragment } from 'relay-runtime'; +import type { FragmentType } from "relay-runtime"; +declare export opaque type observeFragmentTestPluralThrowOnFieldErrorFragment$fragmentType: FragmentType; +export type observeFragmentTestPluralThrowOnFieldErrorFragment$data = $ReadOnlyArray<{| + +name: ?string, + +$fragmentType: observeFragmentTestPluralThrowOnFieldErrorFragment$fragmentType, +|}>; +export type observeFragmentTestPluralThrowOnFieldErrorFragment$key = $ReadOnlyArray<{ + +$data?: observeFragmentTestPluralThrowOnFieldErrorFragment$data, + +$fragmentSpreads: observeFragmentTestPluralThrowOnFieldErrorFragment$fragmentType, + ... +}>; +*/ + +var node/*: ReaderFragment*/ = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": { + "plural": true, + "throwOnFieldError": true + }, + "name": "observeFragmentTestPluralThrowOnFieldErrorFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "type": "User", + "abstractKey": null +}; + +if (__DEV__) { + (node/*: any*/).hash = "531291e1335ff8e4ffacf60c7a6064ed"; +} + +module.exports = ((node/*: any*/)/*: Fragment< + observeFragmentTestPluralThrowOnFieldErrorFragment$fragmentType, + observeFragmentTestPluralThrowOnFieldErrorFragment$data, +>*/); diff --git a/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestPluralThrowOnFieldErrorQuery.graphql.js b/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestPluralThrowOnFieldErrorQuery.graphql.js new file mode 100644 index 0000000000000..a7a23d3f32ffe --- /dev/null +++ b/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestPluralThrowOnFieldErrorQuery.graphql.js @@ -0,0 +1,137 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @oncall relay + * + * @generated SignedSource<> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ConcreteRequest, Query } from 'relay-runtime'; +import type { observeFragmentTestPluralThrowOnFieldErrorFragment$fragmentType } from "./observeFragmentTestPluralThrowOnFieldErrorFragment.graphql"; +export type observeFragmentTestPluralThrowOnFieldErrorQuery$variables = {||}; +export type observeFragmentTestPluralThrowOnFieldErrorQuery$data = {| + +nodes: ?$ReadOnlyArray, +|}; +export type observeFragmentTestPluralThrowOnFieldErrorQuery = {| + response: observeFragmentTestPluralThrowOnFieldErrorQuery$data, + variables: observeFragmentTestPluralThrowOnFieldErrorQuery$variables, +|}; +*/ + +var node/*: ConcreteRequest*/ = (function(){ +var v0 = [ + { + "kind": "Literal", + "name": "ids", + "value": [ + "1", + "2" + ] + } +]; +return { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "observeFragmentTestPluralThrowOnFieldErrorQuery", + "selections": [ + { + "alias": null, + "args": (v0/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "nodes", + "plural": true, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "observeFragmentTestPluralThrowOnFieldErrorFragment" + } + ], + "storageKey": "nodes(ids:[\"1\",\"2\"])" + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "observeFragmentTestPluralThrowOnFieldErrorQuery", + "selections": [ + { + "alias": null, + "args": (v0/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "nodes", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "type": "User", + "abstractKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "storageKey": "nodes(ids:[\"1\",\"2\"])" + } + ] + }, + "params": { + "cacheID": "1e83abfe97b66be09a6642b3332e4d09", + "id": null, + "metadata": {}, + "name": "observeFragmentTestPluralThrowOnFieldErrorQuery", + "operationKind": "query", + "text": "query observeFragmentTestPluralThrowOnFieldErrorQuery {\n nodes(ids: [\"1\", \"2\"]) {\n __typename\n ...observeFragmentTestPluralThrowOnFieldErrorFragment\n id\n }\n}\n\nfragment observeFragmentTestPluralThrowOnFieldErrorFragment on User {\n name\n}\n" + } +}; +})(); + +if (__DEV__) { + (node/*: any*/).hash = "36cd146fd2db4ac80dfe226a3e20dd3e"; +} + +module.exports = ((node/*: any*/)/*: Query< + observeFragmentTestPluralThrowOnFieldErrorQuery$variables, + observeFragmentTestPluralThrowOnFieldErrorQuery$data, +>*/); diff --git a/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorFragment.graphql.js b/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorFragment.graphql.js new file mode 100644 index 0000000000000..3bd37816c2c5f --- /dev/null +++ b/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorFragment.graphql.js @@ -0,0 +1,78 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @oncall relay + * + * @generated SignedSource<> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { Fragment, ReaderFragment } from 'relay-runtime'; +import type { UserAlwaysThrowsResolver$key } from "./../resolvers/__generated__/UserAlwaysThrowsResolver.graphql"; +import type { FragmentType } from "relay-runtime"; +import {always_throws as userAlwaysThrowsResolverType} from "../resolvers/UserAlwaysThrowsResolver.js"; +import type { TestResolverContextType } from "../../../mutations/__tests__/TestResolverContextType"; +// Type assertion validating that `userAlwaysThrowsResolverType` resolver is correctly implemented. +// A type error here indicates that the type signature of the resolver module is incorrect. +(userAlwaysThrowsResolverType: ( + rootKey: UserAlwaysThrowsResolver$key, + args: void, + context: TestResolverContextType, +) => ?string); +declare export opaque type observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorFragment$fragmentType: FragmentType; +export type observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorFragment$data = $ReadOnlyArray<{| + +always_throws: ?string, + +$fragmentType: observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorFragment$fragmentType, +|}>; +export type observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorFragment$key = $ReadOnlyArray<{ + +$data?: observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorFragment$data, + +$fragmentSpreads: observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorFragment$fragmentType, + ... +}>; +*/ + +var node/*: ReaderFragment*/ = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": { + "plural": true, + "throwOnFieldError": true + }, + "name": "observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorFragment", + "selections": [ + { + "alias": null, + "args": null, + "fragment": { + "args": null, + "kind": "FragmentSpread", + "name": "UserAlwaysThrowsResolver" + }, + "kind": "RelayResolver", + "name": "always_throws", + "resolverModule": require('./../resolvers/UserAlwaysThrowsResolver').always_throws, + "path": "always_throws" + } + ], + "type": "User", + "abstractKey": null +}; + +if (__DEV__) { + (node/*: any*/).hash = "59a6f5ddf61e54affd5726b8cf322183"; +} + +module.exports = ((node/*: any*/)/*: Fragment< + observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorFragment$fragmentType, + observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorFragment$data, +>*/); diff --git a/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorQuery.graphql.js b/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorQuery.graphql.js new file mode 100644 index 0000000000000..924a7c4f2ce5a --- /dev/null +++ b/packages/relay-runtime/store/__tests__/__generated__/observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorQuery.graphql.js @@ -0,0 +1,146 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @oncall relay + * + * @generated SignedSource<<88f631a8a727158afc72c5ed15b804a4>> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ConcreteRequest, Query } from 'relay-runtime'; +import type { observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorFragment$fragmentType } from "./observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorFragment.graphql"; +export type observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorQuery$variables = {||}; +export type observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorQuery$data = {| + +nodes: ?$ReadOnlyArray, +|}; +export type observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorQuery = {| + response: observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorQuery$data, + variables: observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorQuery$variables, +|}; +*/ + +var node/*: ConcreteRequest*/ = (function(){ +var v0 = [ + { + "kind": "Literal", + "name": "ids", + "value": [ + "7", + "8" + ] + } +], +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorQuery", + "selections": [ + { + "alias": null, + "args": (v0/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "nodes", + "plural": true, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorFragment" + } + ], + "storageKey": "nodes(ids:[\"7\",\"8\"])" + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorQuery", + "selections": [ + { + "alias": null, + "args": (v0/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "nodes", + "plural": true, + "selections": [ + (v1/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "name": "always_throws", + "args": null, + "fragment": { + "kind": "InlineFragment", + "selections": [ + (v1/*: any*/) + ], + "type": "User", + "abstractKey": null + }, + "kind": "RelayResolver", + "storageKey": null, + "isOutputType": true + } + ], + "type": "User", + "abstractKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "storageKey": "nodes(ids:[\"7\",\"8\"])" + } + ] + }, + "params": { + "cacheID": "cda0054b75d8a0bc0abaacbb14186b4a", + "id": null, + "metadata": {}, + "name": "observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorQuery", + "operationKind": "query", + "text": "query observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorQuery {\n nodes(ids: [\"7\", \"8\"]) {\n __typename\n ...observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorFragment\n id\n }\n}\n\nfragment UserAlwaysThrowsResolver on User {\n __typename\n}\n\nfragment observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorFragment on User {\n ...UserAlwaysThrowsResolver\n}\n" + } +}; +})(); + +if (__DEV__) { + (node/*: any*/).hash = "412492582875c8c7b44e67794ed55763"; +} + +module.exports = ((node/*: any*/)/*: Query< + observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorQuery$variables, + observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorQuery$data, +>*/); diff --git a/packages/relay-runtime/store/__tests__/__generated__/waitForFragmentDataTestOkPluralFragment.graphql.js b/packages/relay-runtime/store/__tests__/__generated__/waitForFragmentDataTestOkPluralFragment.graphql.js new file mode 100644 index 0000000000000..ecd0f3164f631 --- /dev/null +++ b/packages/relay-runtime/store/__tests__/__generated__/waitForFragmentDataTestOkPluralFragment.graphql.js @@ -0,0 +1,61 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @oncall relay + * + * @generated SignedSource<<2ed8051023394e48547a26c640b8e13c>> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { Fragment, ReaderFragment } from 'relay-runtime'; +import type { FragmentType } from "relay-runtime"; +declare export opaque type waitForFragmentDataTestOkPluralFragment$fragmentType: FragmentType; +export type waitForFragmentDataTestOkPluralFragment$data = $ReadOnlyArray<{| + +name: ?string, + +$fragmentType: waitForFragmentDataTestOkPluralFragment$fragmentType, +|}>; +export type waitForFragmentDataTestOkPluralFragment$key = $ReadOnlyArray<{ + +$data?: waitForFragmentDataTestOkPluralFragment$data, + +$fragmentSpreads: waitForFragmentDataTestOkPluralFragment$fragmentType, + ... +}>; +*/ + +var node/*: ReaderFragment*/ = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": { + "plural": true + }, + "name": "waitForFragmentDataTestOkPluralFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "type": "User", + "abstractKey": null +}; + +if (__DEV__) { + (node/*: any*/).hash = "27b1b1a834949ad358eaf6d1396d3f9d"; +} + +module.exports = ((node/*: any*/)/*: Fragment< + waitForFragmentDataTestOkPluralFragment$fragmentType, + waitForFragmentDataTestOkPluralFragment$data, +>*/); diff --git a/packages/relay-runtime/store/__tests__/__generated__/waitForFragmentDataTestOkPluralQuery.graphql.js b/packages/relay-runtime/store/__tests__/__generated__/waitForFragmentDataTestOkPluralQuery.graphql.js new file mode 100644 index 0000000000000..c1f1dc35eef47 --- /dev/null +++ b/packages/relay-runtime/store/__tests__/__generated__/waitForFragmentDataTestOkPluralQuery.graphql.js @@ -0,0 +1,137 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @oncall relay + * + * @generated SignedSource<<5eca2a10e99142f6f4e5d99b02421ad1>> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ConcreteRequest, Query } from 'relay-runtime'; +import type { waitForFragmentDataTestOkPluralFragment$fragmentType } from "./waitForFragmentDataTestOkPluralFragment.graphql"; +export type waitForFragmentDataTestOkPluralQuery$variables = {||}; +export type waitForFragmentDataTestOkPluralQuery$data = {| + +nodes: ?$ReadOnlyArray, +|}; +export type waitForFragmentDataTestOkPluralQuery = {| + response: waitForFragmentDataTestOkPluralQuery$data, + variables: waitForFragmentDataTestOkPluralQuery$variables, +|}; +*/ + +var node/*: ConcreteRequest*/ = (function(){ +var v0 = [ + { + "kind": "Literal", + "name": "ids", + "value": [ + "1", + "2" + ] + } +]; +return { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "waitForFragmentDataTestOkPluralQuery", + "selections": [ + { + "alias": null, + "args": (v0/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "nodes", + "plural": true, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "waitForFragmentDataTestOkPluralFragment" + } + ], + "storageKey": "nodes(ids:[\"1\",\"2\"])" + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "waitForFragmentDataTestOkPluralQuery", + "selections": [ + { + "alias": null, + "args": (v0/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "nodes", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "type": "User", + "abstractKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "storageKey": "nodes(ids:[\"1\",\"2\"])" + } + ] + }, + "params": { + "cacheID": "91c66e0b45b06f9cc7a66477f8156418", + "id": null, + "metadata": {}, + "name": "waitForFragmentDataTestOkPluralQuery", + "operationKind": "query", + "text": "query waitForFragmentDataTestOkPluralQuery {\n nodes(ids: [\"1\", \"2\"]) {\n __typename\n ...waitForFragmentDataTestOkPluralFragment\n id\n }\n}\n\nfragment waitForFragmentDataTestOkPluralFragment on User {\n name\n}\n" + } +}; +})(); + +if (__DEV__) { + (node/*: any*/).hash = "139b32cebe816906461147bcb6b1db45"; +} + +module.exports = ((node/*: any*/)/*: Query< + waitForFragmentDataTestOkPluralQuery$variables, + waitForFragmentDataTestOkPluralQuery$data, +>*/); diff --git a/packages/relay-runtime/store/__tests__/observeFragment-test.js b/packages/relay-runtime/store/__tests__/observeFragment-test.js index 484a74009c32c..8d0e2874d2549 100644 --- a/packages/relay-runtime/store/__tests__/observeFragment-test.js +++ b/packages/relay-runtime/store/__tests__/observeFragment-test.js @@ -337,6 +337,238 @@ test('read deferred fragment', async () => { }); }); +test('observes a plural fragment', async () => { + const query = graphql` + query observeFragmentTestPluralQuery { + nodes(ids: ["1", "2"]) { + ...observeFragmentTestPluralFragment + } + } + `; + + const fragment = graphql` + fragment observeFragmentTestPluralFragment on User @relay(plural: true) { + name + } + `; + + const environment = createMockEnvironment({ + store: new LiveResolverStore(new RelayRecordSource()), + }); + const variables = {}; + const operation = createOperationDescriptor(query, variables); + environment.commitPayload(operation, { + nodes: [ + {id: '1', __typename: 'User', name: 'Alice'}, + {id: '2', __typename: 'User', name: 'Bob'}, + ], + }); + const {data} = environment.lookup(operation.fragment); + // $FlowFixMe Data is untyped + const observable = observeFragment(environment, fragment, data.nodes); + const result = await observable.toPromise(); + expect(result).toEqual({ + state: 'ok', + value: [{name: 'Alice'}, {name: 'Bob'}], + }); +}); + +test('Missing required data on plural fragment', async () => { + const query = graphql` + query observeFragmentTestMissingRequiredPluralQuery { + nodes(ids: ["1", "2"]) { + ...observeFragmentTestMissingRequiredPluralFragment + } + } + `; + + const fragment = graphql` + fragment observeFragmentTestMissingRequiredPluralFragment on User + @relay(plural: true) { + name @required(action: THROW) + } + `; + + const environment = createMockEnvironment(); + const variables = {}; + const operation = createOperationDescriptor(query, variables); + environment.commitPayload(operation, { + nodes: [ + // Name is null despite being required + {id: '1', __typename: 'User', name: null}, + {id: '2', __typename: 'User', name: 'Bob'}, + ], + }); + + const {data} = environment.lookup(operation.fragment); + // $FlowFixMe Data is untyped + const observable = observeFragment(environment, fragment, data.nodes); + withObservableValues(observable, results => { + expect(results).toEqual([ + { + error: new Error( + "Relay: Missing @required value at path 'name' in 'observeFragmentTestMissingRequiredPluralFragment'.", + ), + state: 'error', + }, + ]); + }); +}); + +test('Field error with @relay(plural: true) @throwOnFieldError', async () => { + const query = graphql` + query observeFragmentTestPluralThrowOnFieldErrorQuery { + nodes(ids: ["1", "2"]) { + ...observeFragmentTestPluralThrowOnFieldErrorFragment + } + } + `; + + const fragment = graphql` + fragment observeFragmentTestPluralThrowOnFieldErrorFragment on User + @relay(plural: true) + @throwOnFieldError { + name + } + `; + + let dataSource: Sink; + const fetch = ( + _query: RequestParameters, + _variables: Variables, + _cacheConfig: CacheConfig, + ) => { + // $FlowFixMe[missing-local-annot] Error found while enabling LTI on this file + return RelayObservable.create(sink => { + dataSource = sink; + }); + }; + + const environment = createMockEnvironment({ + network: RelayNetwork.create(fetch), + }); + const variables = {}; + const operation = createOperationDescriptor(query, variables); + + environment.execute({operation}).subscribe({}); + invariant(dataSource != null, 'Expected data source to be set'); + dataSource.next({ + data: { + nodes: [ + {id: '1', __typename: 'User', name: null}, + {id: '2', __typename: 'User', name: 'Bob'}, + ], + }, + errors: [{message: 'error', path: ['nodes', 0, 'name']}], + }); + + const {data} = environment.lookup(operation.fragment); + // $FlowFixMe Data is untyped + const observable = observeFragment(environment, fragment, data.nodes); + withObservableValues(observable, results => { + expect(results).toEqual([ + { + error: new Error( + 'Relay: Unexpected response payload - check server logs for details.', + ), + state: 'error', + }, + ]); + }); +}); + +test('Resolver error with @relay(plural: true) @throwOnFieldError', async () => { + const query = graphql` + query observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorQuery { + nodes(ids: ["7", "8"]) { + ...observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorFragment + } + } + `; + + const fragment = graphql` + fragment observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorFragment on User + @relay(plural: true) + @throwOnFieldError { + always_throws + } + `; + + const environment = createMockEnvironment({ + store: new LiveResolverStore(new RelayRecordSource()), + }); + const variables = {}; + const operation = createOperationDescriptor(query, variables); + environment.commitPayload(operation, { + nodes: [ + {id: '7', __typename: 'User'}, + {id: '8', __typename: 'User'}, + ], + }); + const {data} = environment.lookup(operation.fragment); + // $FlowFixMe Data is untyped + const observable = observeFragment(environment, fragment, data.nodes); + withObservableValues(observable, results => { + expect(results).toEqual([ + { + error: new Error( + "Relay: Resolver error at path 'always_throws' in 'observeFragmentTestResolverErrorWithPluralThrowOnFieldErrorFragment'.", + ), + state: 'error', + }, + ]); + }); +}); + +test('Store update across list items notifies multiple times', async () => { + const query = graphql` + query observeFragmentTestListUpdateQuery { + nodes(ids: ["1", "2"]) { + ...observeFragmentTestListUpdateFragment + } + } + `; + + const fragment = graphql` + fragment observeFragmentTestListUpdateFragment on User + @relay(plural: true) { + name + } + `; + + const environment = createMockEnvironment({ + store: new LiveResolverStore(new RelayRecordSource()), + }); + const variables = {}; + const operation = createOperationDescriptor(query, variables); + environment.commitPayload(operation, { + nodes: [ + {id: '1', __typename: 'User', name: 'Alice'}, + {id: '2', __typename: 'User', name: 'Bob'}, + ], + }); + const {data} = environment.lookup(operation.fragment); + // $FlowFixMe Data is untyped + const observable = observeFragment(environment, fragment, data.nodes); + withObservableValues(observable, results => { + expect(results).toEqual([ + {state: 'ok', value: [{name: 'Alice'}, {name: 'Bob'}]}, + ]); + + environment.commitPayload(operation, { + nodes: [ + {id: '1', __typename: 'User', name: 'Alice updated'}, + {id: '2', __typename: 'User', name: 'Bob updated'}, + ], + }); + expect(results).toEqual([ + {state: 'ok', value: [{name: 'Alice'}, {name: 'Bob'}]}, + {state: 'ok', value: [{name: 'Alice updated'}, {name: 'Bob'}]}, + {state: 'ok', value: [{name: 'Alice updated'}, {name: 'Bob updated'}]}, + ]); + }); +}); + test('data goes missing due to unrelated query response', async () => { const query = graphql` query observeFragmentTestMissingDataQuery { diff --git a/packages/relay-runtime/store/__tests__/waitForFragmentData-test.js b/packages/relay-runtime/store/__tests__/waitForFragmentData-test.js index ec64390dd1a92..b2cb899a5d283 100644 --- a/packages/relay-runtime/store/__tests__/waitForFragmentData-test.js +++ b/packages/relay-runtime/store/__tests__/waitForFragmentData-test.js @@ -52,6 +52,39 @@ test('data ok', async () => { expect(result).toEqual({name: 'Elizabeth'}); }); +test('data ok with plural fragment', async () => { + const query = graphql` + query waitForFragmentDataTestOkPluralQuery { + nodes(ids: ["1", "2"]) { + ...waitForFragmentDataTestOkPluralFragment + } + } + `; + + const fragment = graphql` + fragment waitForFragmentDataTestOkPluralFragment on User + @relay(plural: true) { + name + } + `; + + const environment = createMockEnvironment({ + store: new LiveResolverStore(new RelayRecordSource()), + }); + const variables = {}; + const operation = createOperationDescriptor(query, variables); + environment.commitPayload(operation, { + nodes: [ + {id: '1', __typename: 'User', name: 'Alice'}, + {id: '2', __typename: 'User', name: 'Bob'}, + ], + }); + const {data} = environment.lookup(operation.fragment); + // $FlowFixMe - data is untyped + const result = await waitForFragmentData(environment, fragment, data.nodes); + expect(result).toEqual([{name: 'Alice'}, {name: 'Bob'}]); +}); + test('Promise rejects with @throwOnFieldError', async () => { const query = graphql` query waitForFragmentDataTestThrowOnFieldErrorQuery { diff --git a/packages/relay-runtime/store/observeFragmentExperimental.js b/packages/relay-runtime/store/observeFragmentExperimental.js index e84a00855c944..424ef28a3254c 100644 --- a/packages/relay-runtime/store/observeFragmentExperimental.js +++ b/packages/relay-runtime/store/observeFragmentExperimental.js @@ -9,7 +9,7 @@ * @oncall relay */ -import type {RequestDescriptor} from './RelayStoreTypes'; +import type {PluralReaderSelector, RequestDescriptor} from './RelayStoreTypes'; import type { Fragment, FragmentType, @@ -47,8 +47,7 @@ export type HasSpread = { /** * EXPERIMENTAL: This API is experimental and does not yet support all Relay - * features. Notably, it does not correectly handle plural fragments or some - * features of Relay Resolvers. + * features. Notably, it does not correctly handle some features of Relay Resolvers. * * Given a fragment and a fragment reference, returns a promise that resolves * once the fragment data is available, or rejects if the fragment has an error. @@ -63,7 +62,9 @@ export type HasSpread = { async function waitForFragmentData( environment: IEnvironment, fragment: Fragment, - fragmentRef: HasSpread, + fragmentRef: + | HasSpread + | $ReadOnlyArray>, ): Promise { let subscription: ?Subscription; @@ -94,13 +95,14 @@ async function waitForFragmentData( declare function observeFragment( environment: IEnvironment, fragment: Fragment, - fragmentRef: HasSpread, + fragmentRef: + | HasSpread + | $ReadOnlyArray>, ): Observable>; /** * EXPERIMENTAL: This API is experimental and does not yet support all Relay - * features. Notably, it does not correectly handle plural fragments or some - * features of Relay Resolvers. + * features. Notably, it does not correctly handle some features of Relay Resolvers. * * Given a fragment and a fragment reference, returns an observable that emits * the state of the fragment over time. The observable will emit the following @@ -114,7 +116,7 @@ function observeFragment( environment: IEnvironment, fragment: Fragment, fragmentRef: mixed, -): Observable> { +): mixed { const fragmentNode = getFragment(fragment); const fragmentSelector = getSelector(fragmentNode, fragmentRef); invariant( @@ -124,24 +126,19 @@ function observeFragment( invariant(fragmentSelector != null, 'Expected a selector, got null.'); switch (fragmentSelector.kind) { case 'SingularReaderSelector': - return observeSelector(environment, fragment, fragmentSelector); + return observeSingularSelector(environment, fragment, fragmentSelector); case 'PluralReaderSelector': { - // TODO: We could use something like this RXJS's combineLatest to create - // an observable for each selector and merge them. - // https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/observable/combineLatest.ts - // - // Note that this problem is a bit tricky because Relay currently only - // lets you subscribe at a singular fragment granularity. This makes it - // hard to batch updates such that when a store update causes multiple - // fragments to change, we can only publish a single update to the - // fragment owner. - invariant(false, 'Plural fragments are not supported'); + return observePluralSelector( + environment, + (fragment: $FlowFixMe), + fragmentSelector, + ); } } invariant(false, 'Unsupported fragment selector kind'); } -function observeSelector( +function observeSingularSelector( environment: IEnvironment, fragmentNode: Fragment, fragmentSelector: SingularReaderSelector, @@ -173,6 +170,49 @@ function observeSelector( }); } +function observePluralSelector< + TFragmentType: FragmentType, + TData: Array, +>( + environment: IEnvironment, + fragmentNode: Fragment, + fragmentSelector: PluralReaderSelector, +): Observable> { + const snapshots = fragmentSelector.selectors.map(selector => + environment.lookup(selector), + ); + + return Observable.create(sink => { + // This array is mutable since each subscription updates the array in place. + const states = snapshots.map((snapshot, index) => + snapshotToFragmentState( + environment, + fragmentNode, + fragmentSelector.selectors[index].owner, + snapshot, + ), + ); + + sink.next((mergeFragmentStates(states): $FlowFixMe)); + + const subscriptions = snapshots.map((snapshot, index) => + environment.subscribe(snapshot, latestSnapshot => { + states[index] = snapshotToFragmentState( + environment, + fragmentNode, + fragmentSelector.selectors[index].owner, + latestSnapshot, + ); + // This doesn't batch updates, so it will notify the subscriber multiple times + // if a store update impacting multiple items in the list is published. + sink.next((mergeFragmentStates(states): $FlowFixMe)); + }), + ); + + return () => subscriptions.forEach(subscription => subscription.dispose()); + }); +} + function snapshotToFragmentState( environment: IEnvironment, fragmentNode: Fragment, @@ -229,6 +269,20 @@ function snapshotToFragmentState( return {state: 'ok', value: (snapshot.data: $FlowFixMe)}; } +function mergeFragmentStates( + states: $ReadOnlyArray>, +): FragmentState> { + const value = []; + for (const state of states) { + if (state.state === 'ok') { + value.push(state.value); + } else { + return state; + } + } + return {state: 'ok', value}; +} + module.exports = { observeFragment, waitForFragmentData, diff --git a/packages/relay-runtime/store/waitForFragmentExperimental.js b/packages/relay-runtime/store/waitForFragmentExperimental.js index 53ba061e85ec5..e1ad597178d86 100644 --- a/packages/relay-runtime/store/waitForFragmentExperimental.js +++ b/packages/relay-runtime/store/waitForFragmentExperimental.js @@ -21,8 +21,7 @@ const {observeFragment} = require('./observeFragmentExperimental'); /** * EXPERIMENTAL: This API is experimental and does not yet support all Relay - * features. Notably, it does not correectly handle plural fragments or some - * features of Relay Resolvers. + * features. Notably, it does not correctly handle some features of Relay Resolvers. * * Given a fragment and a fragment reference, returns a promise that resolves * once the fragment data is available, or rejects if the fragment has an error. @@ -37,7 +36,9 @@ const {observeFragment} = require('./observeFragmentExperimental'); async function waitForFragmentData( environment: IEnvironment, fragment: Fragment, - fragmentRef: HasSpread, + fragmentRef: + | HasSpread + | $ReadOnlyArray>, ): Promise { let subscription: ?Subscription; diff --git a/website/docs/api-reference/relay-runtime/observe-fragment.md b/website/docs/api-reference/relay-runtime/observe-fragment.md index abf6b830f481f..1acd6efe29ed1 100644 --- a/website/docs/api-reference/relay-runtime/observe-fragment.md +++ b/website/docs/api-reference/relay-runtime/observe-fragment.md @@ -20,6 +20,10 @@ In some cases it can be useful to define data that you wish to read using a Grap To read a fragment's data just once, see [`waitForFragmentData`](./wait-for-fragment-data.md). +:::caution +When using `observeFragment` with a plural fragment, the current implementation notifies the subscription multiple times if a store update impacting multiple list items gets published. Since the notifications happen synchronously, it is advised to debounce for a tick and only use the last payload for batching. +::: + ### Example ```ts