diff --git a/packages/react-relay/__tests__/LiveResolvers-test.js b/packages/react-relay/__tests__/LiveResolvers-test.js index 77359a26a7d05..7d630a181a73d 100644 --- a/packages/react-relay/__tests__/LiveResolvers-test.js +++ b/packages/react-relay/__tests__/LiveResolvers-test.js @@ -1193,3 +1193,63 @@ describe.each([ expect(renderer.toJSON()).toBe('Loading...'); }); }); + +test('Errors when reading a @live resolver that does not return a LiveState object', () => { + const source = RelayRecordSource.create({ + 'client:root': { + __id: 'client:root', + __typename: '__Root', + }, + }); + const FooQuery = graphql` + query LiveResolversTest16Query { + live_resolver_with_bad_return_value + } + `; + + const operation = createOperationDescriptor(FooQuery, {}); + const store = new LiveResolverStore(source, { + gcReleaseBufferSize: 0, + }); + + const environment = new RelayModernEnvironment({ + network: RelayNetwork.create(jest.fn()), + store, + }); + + expect(() => { + environment.lookup(operation.fragment); + }).toThrow( + 'Expected the @live Relay Resolver backing the field "live_resolver_with_bad_return_value" to return a value that implements LiveState. Did you mean to remove the @live annotation on this resolver?', + ); +}); + +test('Errors when reading a non-@live resolver that returns a LiveState object', () => { + const source = RelayRecordSource.create({ + 'client:root': { + __id: 'client:root', + __typename: '__Root', + }, + }); + const FooQuery = graphql` + query LiveResolversTest17Query { + non_live_resolver_with_live_return_value + } + `; + + const operation = createOperationDescriptor(FooQuery, {}); + const store = new LiveResolverStore(source, { + gcReleaseBufferSize: 0, + }); + + const environment = new RelayModernEnvironment({ + network: RelayNetwork.create(jest.fn()), + store, + }); + + expect(() => { + environment.lookup(operation.fragment); + }).toThrow( + 'Unexpected LiveState value retuned from the non-@live Relay Resolver backing the field "non_live_resolver_with_live_return_value". Did you intend to add @live to this resolver?.', + ); +}); diff --git a/packages/react-relay/__tests__/__generated__/LiveResolversTest16Query.graphql.js b/packages/react-relay/__tests__/__generated__/LiveResolversTest16Query.graphql.js new file mode 100644 index 0000000000000..5b096d87e7c7b --- /dev/null +++ b/packages/react-relay/__tests__/__generated__/LiveResolversTest16Query.graphql.js @@ -0,0 +1,96 @@ +/** + * 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. + * + * @generated SignedSource<> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ClientRequest, ClientQuery } from 'relay-runtime'; +import type { LiveState } from "relay-runtime/store/experimental-live-resolvers/LiveResolverStore"; +import queryLiveResolverWithBadReturnValueResolver from "../../../relay-runtime/store/__tests__/resolvers/QueryLiveResolverWithBadReturnValue.js"; +// Type assertion validating that `queryLiveResolverWithBadReturnValueResolver` resolver is correctly implemented. +// A type error here indicates that the type signature of the resolver module is incorrect. +(queryLiveResolverWithBadReturnValueResolver: () => LiveState); +export type LiveResolversTest16Query$variables = {||}; +export type LiveResolversTest16Query$data = {| + +live_resolver_with_bad_return_value: ?$Call<$Call<((...empty[]) => R) => R, typeof queryLiveResolverWithBadReturnValueResolver>["read"]>, +|}; +export type LiveResolversTest16Query = {| + response: LiveResolversTest16Query$data, + variables: LiveResolversTest16Query$variables, +|}; +*/ + +var node/*: ClientRequest*/ = { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "LiveResolversTest16Query", + "selections": [ + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "fragment": null, + "kind": "RelayLiveResolver", + "name": "live_resolver_with_bad_return_value", + "resolverModule": require('./../../../relay-runtime/store/__tests__/resolvers/QueryLiveResolverWithBadReturnValue.js'), + "path": "live_resolver_with_bad_return_value" + } + ] + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "LiveResolversTest16Query", + "selections": [ + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] + } + ] + }, + "params": { + "cacheID": "f255cb56b711b4d192aacc8e6a11c390", + "id": null, + "metadata": {}, + "name": "LiveResolversTest16Query", + "operationKind": "query", + "text": null + } +}; + +if (__DEV__) { + (node/*: any*/).hash = "e055bf3c3b415e5cd631e4011760e3f1"; +} + +module.exports = ((node/*: any*/)/*: ClientQuery< + LiveResolversTest16Query$variables, + LiveResolversTest16Query$data, +>*/); diff --git a/packages/react-relay/__tests__/__generated__/LiveResolversTest17Query.graphql.js b/packages/react-relay/__tests__/__generated__/LiveResolversTest17Query.graphql.js new file mode 100644 index 0000000000000..9ca9c9fa28457 --- /dev/null +++ b/packages/react-relay/__tests__/__generated__/LiveResolversTest17Query.graphql.js @@ -0,0 +1,95 @@ +/** + * 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. + * + * @generated SignedSource<<1c2724cec85b330379b0c9dc91d508a5>> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ClientRequest, ClientQuery } from 'relay-runtime'; +import queryNonLiveResolverWithLiveReturnValueResolver from "../../../relay-runtime/store/__tests__/resolvers/QueryNonLiveResolverWithLiveReturnValue.js"; +// Type assertion validating that `queryNonLiveResolverWithLiveReturnValueResolver` resolver is correctly implemented. +// A type error here indicates that the type signature of the resolver module is incorrect. +(queryNonLiveResolverWithLiveReturnValueResolver: () => mixed); +export type LiveResolversTest17Query$variables = {||}; +export type LiveResolversTest17Query$data = {| + +non_live_resolver_with_live_return_value: ?$Call<((...empty[]) => R) => R, typeof queryNonLiveResolverWithLiveReturnValueResolver>, +|}; +export type LiveResolversTest17Query = {| + response: LiveResolversTest17Query$data, + variables: LiveResolversTest17Query$variables, +|}; +*/ + +var node/*: ClientRequest*/ = { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "LiveResolversTest17Query", + "selections": [ + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "fragment": null, + "kind": "RelayResolver", + "name": "non_live_resolver_with_live_return_value", + "resolverModule": require('./../../../relay-runtime/store/__tests__/resolvers/QueryNonLiveResolverWithLiveReturnValue.js'), + "path": "non_live_resolver_with_live_return_value" + } + ] + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "LiveResolversTest17Query", + "selections": [ + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] + } + ] + }, + "params": { + "cacheID": "45aad32a1b278b9adda1ecfa295978e3", + "id": null, + "metadata": {}, + "name": "LiveResolversTest17Query", + "operationKind": "query", + "text": null + } +}; + +if (__DEV__) { + (node/*: any*/).hash = "c77a95b61ab9f2b3662fa17bf4001bfc"; +} + +module.exports = ((node/*: any*/)/*: ClientQuery< + LiveResolversTest17Query$variables, + LiveResolversTest17Query$data, +>*/); diff --git a/packages/relay-runtime/store/__tests__/resolvers/QueryLiveResolverWithBadReturnValue.js b/packages/relay-runtime/store/__tests__/resolvers/QueryLiveResolverWithBadReturnValue.js new file mode 100644 index 0000000000000..dc6f6652ab6dd --- /dev/null +++ b/packages/relay-runtime/store/__tests__/resolvers/QueryLiveResolverWithBadReturnValue.js @@ -0,0 +1,29 @@ +/** + * 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. + * + * @format + * @flow strict-local + * @emails oncall+relay + */ + +'use strict'; + +/** + * @RelayResolver + * @fieldName live_resolver_with_bad_return_value + * @onType Query + * @live + * + * A @live resolver that does not return a LiveObject + */ +import type {LiveState} from '../../experimental-live-resolvers/LiveResolverStore'; + +function liveResolverWithBadReturnValue(): LiveState { + // $FlowFixMe The purpose of this resolver is to test a bad return value. + return 'Oops!'; +} + +module.exports = liveResolverWithBadReturnValue; diff --git a/packages/relay-runtime/store/__tests__/resolvers/QueryNonLiveResolverWithLiveReturnValue.js b/packages/relay-runtime/store/__tests__/resolvers/QueryNonLiveResolverWithLiveReturnValue.js new file mode 100644 index 0000000000000..c6339cb2f5193 --- /dev/null +++ b/packages/relay-runtime/store/__tests__/resolvers/QueryNonLiveResolverWithLiveReturnValue.js @@ -0,0 +1,34 @@ +/** + * 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. + * + * @format + * @flow strict-local + * @emails oncall+relay + */ + +'use strict'; + +/** + * @RelayResolver + * @fieldName non_live_resolver_with_live_return_value + * @onType Query + * + * A non-@live resolver that returns a LiveObject + */ +import type {LiveState} from '../../experimental-live-resolvers/LiveResolverStore'; + +function nonLiveResolverWithLiveReturnValue(): LiveState { + return { + read() { + return 'Oops!'; + }, + subscribe(cb) { + return () => {}; + }, + }; +} + +module.exports = nonLiveResolverWithLiveReturnValue; diff --git a/packages/relay-runtime/store/experimental-live-resolvers/LiveResolverCache.js b/packages/relay-runtime/store/experimental-live-resolvers/LiveResolverCache.js index b0b662dbbd115..cd6d67a5bec9e 100644 --- a/packages/relay-runtime/store/experimental-live-resolvers/LiveResolverCache.js +++ b/packages/relay-runtime/store/experimental-live-resolvers/LiveResolverCache.js @@ -27,6 +27,7 @@ import type { GetDataForResolverFragmentFn, ResolverCache, } from '../ResolverCache'; +import type LiveResolverStore from './LiveResolverStore'; import type {LiveState} from './LiveResolverStore'; const recycleNodesInto = require('../../util/recycleNodesInto'); @@ -41,7 +42,6 @@ const { RELAY_RESOLVER_VALUE_KEY, getStorageKey, } = require('../RelayStoreUtils'); -const LiveResolverStore = require('./LiveResolverStore'); const {isSuspenseSentinel} = require('./LiveResolverSuspenseSentinel'); const invariant = require('invariant'); const warning = require('warning'); @@ -129,7 +129,9 @@ class LiveResolverCache implements ResolverCache { if (__DEV__) { invariant( isLiveStateValue(evaluationResult.resolverResult), - 'Expected a @live Relay Resolver to return a value that implements LiveState.', + 'Expected the @live Relay Resolver backing the field "%s" to return a value ' + + 'that implements LiveState. Did you mean to remove the @live annotation on this resolver?', + field.path, ); } const liveState: LiveState = @@ -138,6 +140,13 @@ class LiveResolverCache implements ResolverCache { this._setLiveStateValue(linkedRecord, linkedID, liveState); } } else { + if (__DEV__) { + invariant( + !isLiveStateValue(evaluationResult.resolverResult), + 'Unexpected LiveState value retuned from the non-@live Relay Resolver backing the field "%s". Did you intend to add @live to this resolver?.', + field.path, + ); + } RelayModernRecord.setValue( linkedRecord, RELAY_RESOLVER_VALUE_KEY,