diff --git a/packages/react-relay/__tests__/RelayResolverModel-test.js b/packages/react-relay/__tests__/RelayResolverModel-test.js
index 2d7a70e77fa3d..cc0c128fdc1c8 100644
--- a/packages/react-relay/__tests__/RelayResolverModel-test.js
+++ b/packages/react-relay/__tests__/RelayResolverModel-test.js
@@ -34,6 +34,11 @@ const {
completeTodo,
resetStore,
} = require('relay-runtime/store/__tests__/resolvers/ExampleTodoStore');
+const {
+ chargeBattery,
+ resetModels,
+ setIsHuman,
+} = require('relay-runtime/store/__tests__/resolvers/MutableModel');
const LiveResolverStore = require('relay-runtime/store/experimental-live-resolvers/LiveResolverStore.js');
const RelayModernEnvironment = require('relay-runtime/store/RelayModernEnvironment');
const RelayRecordSource = require('relay-runtime/store/RelayRecordSource');
@@ -494,4 +499,53 @@ describe.each([
},
});
});
+
+ test('should not mutate complex resolver values', () => {
+ resetModels();
+ // Do not deep freeze
+ jest.mock('relay-runtime/util/deepFreeze');
+
+ TestRenderer.act(() => {
+ setIsHuman(true);
+ });
+ function GetMutableEntity() {
+ const data = useClientQuery(
+ graphql`
+ query RelayResolverModelTestGetMutableEntityQuery {
+ mutable_entity
+ }
+ `,
+ {},
+ );
+ if (data.mutable_entity == null) {
+ return null;
+ }
+ return `${data.mutable_entity.type}:${data.mutable_entity.props.battery}`;
+ }
+
+ const renderer = TestRenderer.create(
+
+
+ ,
+ );
+ expect(renderer.toJSON()).toEqual('human:0');
+
+ TestRenderer.act(() => {
+ setIsHuman(false);
+ jest.runAllImmediates();
+ });
+ expect(renderer.toJSON()).toEqual('robot:0');
+
+ TestRenderer.act(() => {
+ chargeBattery();
+ setIsHuman(true);
+ jest.runAllImmediates();
+ });
+ // TODO: Should be 0. Relay should not mutate the value here.
+ expect(renderer.toJSON()).toEqual('human:100');
+
+ TestRenderer.act(() => {
+ renderer.unmount();
+ });
+ });
});
diff --git a/packages/react-relay/__tests__/__generated__/RelayResolverModelTestGetMutableEntityQuery.graphql.js b/packages/react-relay/__tests__/__generated__/RelayResolverModelTestGetMutableEntityQuery.graphql.js
new file mode 100644
index 0000000000000..df5db6c912ed3
--- /dev/null
+++ b/packages/react-relay/__tests__/__generated__/RelayResolverModelTestGetMutableEntityQuery.graphql.js
@@ -0,0 +1,99 @@
+/**
+ * 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<<66d0b3cabdba7cc580b3e2e2169ef5d8>>
+ * @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 {mutable_entity as queryMutableEntityResolverType} from "../../../relay-runtime/store/__tests__/resolvers/MutableModel.js";
+// Type assertion validating that `queryMutableEntityResolverType` resolver is correctly implemented.
+// A type error here indicates that the type signature of the resolver module is incorrect.
+(queryMutableEntityResolverType: () => LiveState);
+export type RelayResolverModelTestGetMutableEntityQuery$variables = {||};
+export type RelayResolverModelTestGetMutableEntityQuery$data = {|
+ +mutable_entity: ?ReturnType["read"]>,
+|};
+export type RelayResolverModelTestGetMutableEntityQuery = {|
+ response: RelayResolverModelTestGetMutableEntityQuery$data,
+ variables: RelayResolverModelTestGetMutableEntityQuery$variables,
+|};
+*/
+
+var node/*: ClientRequest*/ = {
+ "fragment": {
+ "argumentDefinitions": [],
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "RelayResolverModelTestGetMutableEntityQuery",
+ "selections": [
+ {
+ "kind": "ClientExtension",
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "fragment": null,
+ "kind": "RelayLiveResolver",
+ "name": "mutable_entity",
+ "resolverModule": require('./../../../relay-runtime/store/__tests__/resolvers/MutableModel').mutable_entity,
+ "path": "mutable_entity"
+ }
+ ]
+ }
+ ],
+ "type": "Query",
+ "abstractKey": null
+ },
+ "kind": "Request",
+ "operation": {
+ "argumentDefinitions": [],
+ "kind": "Operation",
+ "name": "RelayResolverModelTestGetMutableEntityQuery",
+ "selections": [
+ {
+ "kind": "ClientExtension",
+ "selections": [
+ {
+ "name": "mutable_entity",
+ "args": null,
+ "fragment": null,
+ "kind": "RelayResolver",
+ "storageKey": null,
+ "isOutputType": true
+ }
+ ]
+ }
+ ]
+ },
+ "params": {
+ "cacheID": "aa33fbf58d2c2c1640de7da7280d2f2e",
+ "id": null,
+ "metadata": {},
+ "name": "RelayResolverModelTestGetMutableEntityQuery",
+ "operationKind": "query",
+ "text": null
+ }
+};
+
+if (__DEV__) {
+ (node/*: any*/).hash = "bd6186904ff5b69591c6929ee7f72aa4";
+}
+
+module.exports = ((node/*: any*/)/*: ClientQuery<
+ RelayResolverModelTestGetMutableEntityQuery$variables,
+ RelayResolverModelTestGetMutableEntityQuery$data,
+>*/);
diff --git a/packages/relay-runtime/store/__tests__/resolvers/MutableModel.js b/packages/relay-runtime/store/__tests__/resolvers/MutableModel.js
new file mode 100644
index 0000000000000..0b8f74868ae4e
--- /dev/null
+++ b/packages/relay-runtime/store/__tests__/resolvers/MutableModel.js
@@ -0,0 +1,93 @@
+/**
+ * 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.
+ *
+ * @flow strict-local
+ * @format
+ * @oncall relay
+ */
+
+'use strict';
+
+import type {LiveState} from '../../experimental-live-resolvers/LiveResolverStore';
+
+type Entity = {
+ name: string,
+ type: string,
+ props: {
+ battery: string,
+ },
+};
+
+let HUMAN: Entity = {
+ name: 'Alice',
+ type: 'human',
+ props: {
+ battery: '0',
+ },
+};
+
+let ROBOT: Entity = {
+ name: 'Bob',
+ type: 'robot',
+ props: {
+ battery: '0',
+ },
+};
+
+const subscriptions: Array<() => void> = [];
+let isHuman: boolean = true;
+/**
+ * @RelayResolver Query.mutable_entity: RelayResolverValue
+ * @live
+
+ */
+function mutable_entity(): LiveState {
+ return {
+ read() {
+ return isHuman ? HUMAN : ROBOT;
+ },
+ subscribe(cb) {
+ subscriptions.push(cb);
+ return () => {
+ subscriptions.filter(x => x !== cb);
+ };
+ },
+ };
+}
+
+function setIsHuman(val: boolean): void {
+ isHuman = val;
+ subscriptions.forEach(x => x());
+}
+
+function chargeBattery(): void {
+ ROBOT.props.battery = '100';
+ subscriptions.forEach(x => x());
+}
+
+function resetModels(): void {
+ HUMAN = {
+ name: 'Alice',
+ type: 'human',
+ props: {
+ battery: '0',
+ },
+ };
+ ROBOT = {
+ name: 'Bob',
+ type: 'robot',
+ props: {
+ battery: '0',
+ },
+ };
+}
+
+module.exports = {
+ mutable_entity,
+ setIsHuman,
+ chargeBattery,
+ resetModels,
+};