From 1ac160a048ec920ed02dd33f78f1eea207a77ece Mon Sep 17 00:00:00 2001 From: Pavel Glac Date: Thu, 26 Mar 2026 19:02:15 +0000 Subject: [PATCH 1/7] observer --- packages/apollo-forest-run/src/ForestRun.ts | 57 ++++++++++++++++++++- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/packages/apollo-forest-run/src/ForestRun.ts b/packages/apollo-forest-run/src/ForestRun.ts index 9811acf07..99cd57bff 100644 --- a/packages/apollo-forest-run/src/ForestRun.ts +++ b/packages/apollo-forest-run/src/ForestRun.ts @@ -345,7 +345,7 @@ export class ForestRun< return this.env.optimizeFragmentReads && isFragmentDocument(watch.query) && (watch.id || watch.rootId) - ? this.watchFragment(watch) + ? this.watchFragmentInternal(watch) : this.watchOperation(watch); } @@ -370,7 +370,7 @@ export class ForestRun< }; } - private watchFragment(watch: Cache.WatchOptions) { + private watchFragmentInternal(watch: Cache.WatchOptions) { const id = watch.id ?? watch.rootId; assert(id !== undefined); accumulate(this.store.fragmentWatches, id, watch); @@ -380,6 +380,59 @@ export class ForestRun< }; } + public watchFragment(options: { + fragment: DocumentNode; + fragmentName?: string; + from: string | StoreObject; + optimistic?: boolean; + }) { + const { fragment, fragmentName, from, optimistic = true, ...otherOptions } = + options; + const query = this["getFragmentDoc"](fragment, fragmentName); + const id = typeof from === "string" ? from : this.identify(from); + + return { + subscribe: ( + observerOrNext: + | ((result: any) => void) + | { next?: (result: any) => void } + | null + | undefined, + ) => { + let latestDiff: Cache.DiffResult | undefined; + const unsubscribe = this.watch({ + ...otherOptions, + returnPartialData: true, + id, + query, + optimistic, + immediate: true, + callback: (diff: Cache.DiffResult) => { + let data = diff.result; + if (data === null) data = {}; + if (latestDiff && equal(latestDiff.result, data)) return; + const result = { + data, + dataState: diff.complete ? "complete" : "partial", + complete: !!diff.complete, + missing: diff.missing, + }; + latestDiff = { + ...diff, + result: data, + }; + const next = + typeof observerOrNext === "function" + ? observerOrNext + : observerOrNext?.next; + next?.(result); + }, + }); + return { unsubscribe }; + }, + }; + } + // Compatibility with InMemoryCache for Apollo dev tools get data() { const extract = this.extract.bind(this); From 1a92b71df9973ae84cd10967f826e5a3d2dbd8d6 Mon Sep 17 00:00:00 2001 From: Pavel Glac Date: Thu, 26 Mar 2026 19:02:37 +0000 Subject: [PATCH 2/7] Change files --- ...lo-forest-run-c0098f3e-15b9-4b8c-9f7e-cc97f866a63f.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@graphitation-apollo-forest-run-c0098f3e-15b9-4b8c-9f7e-cc97f866a63f.json diff --git a/change/@graphitation-apollo-forest-run-c0098f3e-15b9-4b8c-9f7e-cc97f866a63f.json b/change/@graphitation-apollo-forest-run-c0098f3e-15b9-4b8c-9f7e-cc97f866a63f.json new file mode 100644 index 000000000..517f0b8a8 --- /dev/null +++ b/change/@graphitation-apollo-forest-run-c0098f3e-15b9-4b8c-9f7e-cc97f866a63f.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "observer", + "packageName": "@graphitation/apollo-forest-run", + "email": "pavelglac@gmail.com", + "dependentChangeType": "patch" +} From 22650dee2818bed09067fd62d45811e803fad7d2 Mon Sep 17 00:00:00 2001 From: Pavel Glac Date: Thu, 26 Mar 2026 19:08:21 +0000 Subject: [PATCH 3/7] fix formatting --- packages/apollo-forest-run/src/ForestRun.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/apollo-forest-run/src/ForestRun.ts b/packages/apollo-forest-run/src/ForestRun.ts index 99cd57bff..a2c7e99f8 100644 --- a/packages/apollo-forest-run/src/ForestRun.ts +++ b/packages/apollo-forest-run/src/ForestRun.ts @@ -386,8 +386,13 @@ export class ForestRun< from: string | StoreObject; optimistic?: boolean; }) { - const { fragment, fragmentName, from, optimistic = true, ...otherOptions } = - options; + const { + fragment, + fragmentName, + from, + optimistic = true, + ...otherOptions + } = options; const query = this["getFragmentDoc"](fragment, fragmentName); const id = typeof from === "string" ? from : this.identify(from); From 8e437c63c8e2c692105c31ea0318c447f292132a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Mar 2026 12:16:01 +0000 Subject: [PATCH 4/7] Fix watchFragment to handle undefined `from` gracefully Agent-Logs-Url: https://github.com/microsoft/graphitation/sessions/1188cddb-5227-4dbe-8a69-1283c9aeebd2 Co-authored-by: pavelglac <42679661+pavelglac@users.noreply.github.com> --- packages/apollo-forest-run/src/ForestRun.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/apollo-forest-run/src/ForestRun.ts b/packages/apollo-forest-run/src/ForestRun.ts index a2c7e99f8..5a32700d8 100644 --- a/packages/apollo-forest-run/src/ForestRun.ts +++ b/packages/apollo-forest-run/src/ForestRun.ts @@ -394,7 +394,10 @@ export class ForestRun< ...otherOptions } = options; const query = this["getFragmentDoc"](fragment, fragmentName); - const id = typeof from === "string" ? from : this.identify(from); + const id = + typeof from === "undefined" || typeof from === "string" ? + from + : this.identify(from); return { subscribe: ( From 44d5137b6845a0a80f63d22927e33df7f2f1f56b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:16:20 +0000 Subject: [PATCH 5/7] Align watchFragment with Apollo Client 3.13.9 ApolloCache.watchFragment Agent-Logs-Url: https://github.com/microsoft/graphitation/sessions/67058345-f43e-42f4-9747-050ad6c70880 Co-authored-by: pavelglac <42679661+pavelglac@users.noreply.github.com> --- .../compat/package-lock.json | 22 +++++ packages/apollo-forest-run/src/ForestRun.ts | 99 ++++++++++--------- 2 files changed, 76 insertions(+), 45 deletions(-) diff --git a/packages/apollo-forest-run/compat/package-lock.json b/packages/apollo-forest-run/compat/package-lock.json index 0caa720a2..2aa163cb8 100644 --- a/packages/apollo-forest-run/compat/package-lock.json +++ b/packages/apollo-forest-run/compat/package-lock.json @@ -2318,6 +2318,21 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -7201,6 +7216,13 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, "function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", diff --git a/packages/apollo-forest-run/src/ForestRun.ts b/packages/apollo-forest-run/src/ForestRun.ts index 5a32700d8..5ebc3aec2 100644 --- a/packages/apollo-forest-run/src/ForestRun.ts +++ b/packages/apollo-forest-run/src/ForestRun.ts @@ -19,6 +19,7 @@ import type { Transaction, } from "./cache/types"; import { ApolloCache } from "@apollo/client"; +import { Observable } from "@apollo/client/utilities"; import { assert } from "./jsutils/assert"; import { accumulate, deleteAccumulated } from "./jsutils/map"; import { read } from "./cache/read"; @@ -380,12 +381,17 @@ export class ForestRun< }; } - public watchFragment(options: { + public watchFragment(options: { fragment: DocumentNode; fragmentName?: string; - from: string | StoreObject; + from: StoreObject | Reference | string; optimistic?: boolean; - }) { + variables?: TVars; + }): Observable<{ + data: TData; + complete: boolean; + missing?: any; + }> { const { fragment, fragmentName, @@ -394,51 +400,54 @@ export class ForestRun< ...otherOptions } = options; const query = this["getFragmentDoc"](fragment, fragmentName); + // While our TypeScript types do not allow for `undefined` as a valid + // `from`, its possible `useFragment` gives us an `undefined` since it + // calls `cache.identify` and provides that value to `from`. We are + // adding this fix here however to ensure those using plain JavaScript + // and using `cache.identify` themselves will avoid seeing the obscure + // warning. const id = - typeof from === "undefined" || typeof from === "string" ? - from - : this.identify(from); + typeof from === "undefined" || typeof from === "string" + ? from + : this.identify(from); - return { - subscribe: ( - observerOrNext: - | ((result: any) => void) - | { next?: (result: any) => void } - | null - | undefined, - ) => { - let latestDiff: Cache.DiffResult | undefined; - const unsubscribe = this.watch({ - ...otherOptions, - returnPartialData: true, - id, - query, - optimistic, - immediate: true, - callback: (diff: Cache.DiffResult) => { - let data = diff.result; - if (data === null) data = {}; - if (latestDiff && equal(latestDiff.result, data)) return; - const result = { - data, - dataState: diff.complete ? "complete" : "partial", - complete: !!diff.complete, - missing: diff.missing, - }; - latestDiff = { - ...diff, - result: data, - }; - const next = - typeof observerOrNext === "function" - ? observerOrNext - : observerOrNext?.next; - next?.(result); - }, - }); - return { unsubscribe }; - }, + const diffOptions: Cache.DiffOptions = { + ...otherOptions, + returnPartialData: true, + id, + query, + optimistic, }; + + let latestDiff: Cache.DiffResult | undefined; + + return new Observable((observer) => { + return this.watch({ + ...diffOptions, + immediate: true, + callback: (diff: Cache.DiffResult) => { + const data = + diff.result == null ? ({} as TData) : diff.result; + + if (latestDiff && equal(latestDiff.result, data)) { + return; + } + + const result = { + data, + complete: !!diff.complete, + missing: diff.missing, + }; + + latestDiff = { + ...diff, + result: data, + }; + + observer.next(result); + }, + }); + }); } // Compatibility with InMemoryCache for Apollo dev tools From dbeb0991f861a41353f88498b0e5199d1f469c31 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 10:15:17 +0000 Subject: [PATCH 6/7] Fix watchFragmentInternal immediate mode and add watchFragment tests Agent-Logs-Url: https://github.com/microsoft/graphitation/sessions/6e241c01-f93d-4cff-a0fd-b99cd56d337a Co-authored-by: pavelglac <42679661+pavelglac@users.noreply.github.com> --- packages/apollo-forest-run/src/ForestRun.ts | 8 + .../src/__tests__/watchFragment.test.ts | 401 ++++++++++++++++++ 2 files changed, 409 insertions(+) create mode 100644 packages/apollo-forest-run/src/__tests__/watchFragment.test.ts diff --git a/packages/apollo-forest-run/src/ForestRun.ts b/packages/apollo-forest-run/src/ForestRun.ts index 5ebc3aec2..97de7e938 100644 --- a/packages/apollo-forest-run/src/ForestRun.ts +++ b/packages/apollo-forest-run/src/ForestRun.ts @@ -374,6 +374,14 @@ export class ForestRun< private watchFragmentInternal(watch: Cache.WatchOptions) { const id = watch.id ?? watch.rootId; assert(id !== undefined); + + if (watch.immediate) { + const diff = this.diff(watch); + if (this.shouldNotifyWatch(watch, diff)) { + this.notifyWatch(watch, diff); + } + } + accumulate(this.store.fragmentWatches, id, watch); return () => { diff --git a/packages/apollo-forest-run/src/__tests__/watchFragment.test.ts b/packages/apollo-forest-run/src/__tests__/watchFragment.test.ts new file mode 100644 index 000000000..24e921c70 --- /dev/null +++ b/packages/apollo-forest-run/src/__tests__/watchFragment.test.ts @@ -0,0 +1,401 @@ +import { DocumentNode } from "graphql"; +import { ForestRun } from "../ForestRun"; +import { gql } from "./helpers/descriptor"; + +function newCache(optimizeFragmentReads = false) { + return new ForestRun({ optimizeFragmentReads }); +} + +const QUERY = gql` + query ($id: Int!) { + item(id: $id) { + id + foo + } + } +`; + +const FRAGMENT: DocumentNode = { + kind: "Document", + definitions: [ + { + kind: "FragmentDefinition", + name: { kind: "Name", value: "ItemFragment" }, + typeCondition: { + kind: "NamedType", + name: { kind: "Name", value: "Foo" }, + }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "id" } }, + { kind: "Field", name: { kind: "Name", value: "foo" } }, + ], + }, + }, + ], +}; + +// zen-observable delivers values via Promise.resolve().then(...) +function waitForNextTick() { + return new Promise((resolve) => setTimeout(resolve, 0)); +} + +describe("watchFragment", () => { + test("returns an Observable with subscribe method", () => { + const cache = newCache(); + const observable = cache.watchFragment({ + fragment: FRAGMENT, + from: { __typename: "Foo", id: 1 }, + }); + expect(observable).toBeDefined(); + expect(typeof observable.subscribe).toBe("function"); + }); + + test("delivers initial result on subscribe with string id", async () => { + const cache = newCache(); + const foo1 = { __typename: "Foo", id: 1, foo: "bar" }; + + cache.write({ + dataId: "ROOT_QUERY", + query: QUERY, + result: { item: foo1 }, + variables: { id: 1 }, + }); + + const results: any[] = []; + const sub = cache + .watchFragment({ + fragment: FRAGMENT, + from: "Foo:1", + }) + .subscribe((result) => { + results.push(result); + }); + + await waitForNextTick(); + + expect(results.length).toBe(1); + expect(results[0].data).toEqual(foo1); + expect(results[0].complete).toBe(true); + + sub.unsubscribe(); + }); + + test("delivers initial result on subscribe with StoreObject", async () => { + const cache = newCache(); + const foo1 = { __typename: "Foo", id: 1, foo: "bar" }; + + cache.write({ + dataId: "ROOT_QUERY", + query: QUERY, + result: { item: foo1 }, + variables: { id: 1 }, + }); + + const results: any[] = []; + const sub = cache + .watchFragment({ + fragment: FRAGMENT, + from: { __typename: "Foo", id: 1 }, + }) + .subscribe((result) => { + results.push(result); + }); + + await waitForNextTick(); + + expect(results.length).toBe(1); + expect(results[0].data).toEqual(foo1); + expect(results[0].complete).toBe(true); + + sub.unsubscribe(); + }); + + test("delivers initial result on subscribe with Reference", async () => { + const cache = newCache(); + const foo1 = { __typename: "Foo", id: 1, foo: "bar" }; + + cache.write({ + dataId: "ROOT_QUERY", + query: QUERY, + result: { item: foo1 }, + variables: { id: 1 }, + }); + + const results: any[] = []; + const sub = cache + .watchFragment({ + fragment: FRAGMENT, + from: { __ref: "Foo:1" }, + }) + .subscribe((result) => { + results.push(result); + }); + + await waitForNextTick(); + + expect(results.length).toBe(1); + expect(results[0].data).toEqual(foo1); + expect(results[0].complete).toBe(true); + + sub.unsubscribe(); + }); + + test("notifies subscriber when fragment data changes", async () => { + const cache = newCache(); + const foo1 = { __typename: "Foo", id: 1, foo: "before" }; + + cache.write({ + dataId: "ROOT_QUERY", + query: QUERY, + result: { item: foo1 }, + variables: { id: 1 }, + }); + + const results: any[] = []; + const sub = cache + .watchFragment({ + fragment: FRAGMENT, + from: "Foo:1", + }) + .subscribe((result) => { + results.push(result); + }); + + await waitForNextTick(); + expect(results.length).toBe(1); + expect(results[0].data.foo).toBe("before"); + + const foo1changed = { __typename: "Foo", id: 1, foo: "after" }; + cache.write({ + dataId: "ROOT_QUERY", + query: QUERY, + result: { item: foo1changed }, + variables: { id: 1 }, + }); + + await waitForNextTick(); + expect(results.length).toBe(2); + expect(results[1].data.foo).toBe("after"); + expect(results[1].complete).toBe(true); + + sub.unsubscribe(); + }); + + test("does not notify when data is unchanged (deduplication)", async () => { + const cache = newCache(); + const foo1 = { __typename: "Foo", id: 1, foo: "same" }; + + cache.write({ + dataId: "ROOT_QUERY", + query: QUERY, + result: { item: foo1 }, + variables: { id: 1 }, + }); + + const results: any[] = []; + const sub = cache + .watchFragment({ + fragment: FRAGMENT, + from: "Foo:1", + }) + .subscribe((result) => { + results.push(result); + }); + + await waitForNextTick(); + expect(results.length).toBe(1); + + // Write the same data again + cache.write({ + dataId: "ROOT_QUERY", + query: QUERY, + result: { item: { ...foo1 } }, + variables: { id: 1 }, + }); + + await waitForNextTick(); + // Should still be 1 because data didn't change + expect(results.length).toBe(1); + + sub.unsubscribe(); + }); + + test("stops notifying after unsubscribe", async () => { + const cache = newCache(); + const foo1 = { __typename: "Foo", id: 1, foo: "before" }; + + cache.write({ + dataId: "ROOT_QUERY", + query: QUERY, + result: { item: foo1 }, + variables: { id: 1 }, + }); + + const results: any[] = []; + const sub = cache + .watchFragment({ + fragment: FRAGMENT, + from: "Foo:1", + }) + .subscribe((result) => { + results.push(result); + }); + + await waitForNextTick(); + expect(results.length).toBe(1); + sub.unsubscribe(); + + cache.write({ + dataId: "ROOT_QUERY", + query: QUERY, + result: { item: { __typename: "Foo", id: 1, foo: "after" } }, + variables: { id: 1 }, + }); + + await waitForNextTick(); + // Should still be 1 - no notification after unsubscribe + expect(results.length).toBe(1); + }); + + test("result has complete: false when data is missing", async () => { + const cache = newCache(); + + const results: any[] = []; + const sub = cache + .watchFragment({ + fragment: FRAGMENT, + from: "Foo:1", + }) + .subscribe((result) => { + results.push(result); + }); + + await waitForNextTick(); + expect(results.length).toBe(1); + expect(results[0].complete).toBe(false); + + sub.unsubscribe(); + }); + + test("handles from === undefined gracefully", async () => { + const cache = newCache(); + + const results: any[] = []; + // TypeScript wouldn't normally allow this but JS callers might pass undefined + const sub = cache + .watchFragment({ + fragment: FRAGMENT, + from: undefined as any, + }) + .subscribe((result) => { + results.push(result); + }); + + await waitForNextTick(); + // Should not throw, should deliver a result + expect(results.length).toBe(1); + + sub.unsubscribe(); + }); + + test("works with optimizeFragmentReads enabled", async () => { + const cache = newCache(true); + const foo1 = { __typename: "Foo", id: 1, foo: "bar" }; + + cache.write({ + dataId: "ROOT_QUERY", + query: QUERY, + result: { item: foo1 }, + variables: { id: 1 }, + }); + + const results: any[] = []; + const sub = cache + .watchFragment({ + fragment: FRAGMENT, + from: "Foo:1", + }) + .subscribe((result) => { + results.push(result); + }); + + await waitForNextTick(); + // Should deliver initial result even with optimizeFragmentReads + expect(results.length).toBe(1); + expect(results[0].data).toEqual(foo1); + expect(results[0].complete).toBe(true); + + sub.unsubscribe(); + }); + + test("delivers updates with optimizeFragmentReads enabled", async () => { + const cache = newCache(true); + const foo1 = { __typename: "Foo", id: 1, foo: "before" }; + + cache.write({ + dataId: "ROOT_QUERY", + query: QUERY, + result: { item: foo1 }, + variables: { id: 1 }, + }); + + const results: any[] = []; + const sub = cache + .watchFragment({ + fragment: FRAGMENT, + from: "Foo:1", + }) + .subscribe((result) => { + results.push(result); + }); + + await waitForNextTick(); + expect(results.length).toBe(1); + + const foo1changed = { __typename: "Foo", id: 1, foo: "after" }; + cache.write({ + dataId: "ROOT_QUERY", + query: QUERY, + result: { item: foo1changed }, + variables: { id: 1 }, + }); + + await waitForNextTick(); + expect(results.length).toBe(2); + expect(results[1].data.foo).toBe("after"); + + sub.unsubscribe(); + }); + + test("result does not contain dataState field", async () => { + const cache = newCache(); + const foo1 = { __typename: "Foo", id: 1, foo: "bar" }; + + cache.write({ + dataId: "ROOT_QUERY", + query: QUERY, + result: { item: foo1 }, + variables: { id: 1 }, + }); + + const results: any[] = []; + const sub = cache + .watchFragment({ + fragment: FRAGMENT, + from: "Foo:1", + }) + .subscribe((result) => { + results.push(result); + }); + + await waitForNextTick(); + expect(results.length).toBe(1); + expect(results[0]).not.toHaveProperty("dataState"); + expect(results[0]).toHaveProperty("data"); + expect(results[0]).toHaveProperty("complete"); + + sub.unsubscribe(); + }); +}); From fa6a230669a5eb1212f21784042905a5f1baf1c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:43:21 +0000 Subject: [PATCH 7/7] Refactor: merge watchFragmentInternal optimization directly into watchFragment Agent-Logs-Url: https://github.com/microsoft/graphitation/sessions/f84e3233-0a71-432d-962b-8a7f4d44a82c Co-authored-by: pavelglac <42679661+pavelglac@users.noreply.github.com> --- packages/apollo-forest-run/src/ForestRun.ts | 91 +++++++++++---------- 1 file changed, 49 insertions(+), 42 deletions(-) diff --git a/packages/apollo-forest-run/src/ForestRun.ts b/packages/apollo-forest-run/src/ForestRun.ts index 97de7e938..41d0d9907 100644 --- a/packages/apollo-forest-run/src/ForestRun.ts +++ b/packages/apollo-forest-run/src/ForestRun.ts @@ -343,11 +343,18 @@ export class ForestRun< public watch( watch: Cache.WatchOptions, ): () => void { - return this.env.optimizeFragmentReads && + if ( + this.env.optimizeFragmentReads && isFragmentDocument(watch.query) && (watch.id || watch.rootId) - ? this.watchFragmentInternal(watch) - : this.watchOperation(watch); + ) { + const id = watch.id ?? watch.rootId; + accumulate(this.store.fragmentWatches, id, watch); + return () => { + deleteAccumulated(this.store.fragmentWatches, id, watch); + }; + } + return this.watchOperation(watch); } private watchOperation(watch: Cache.WatchOptions) { @@ -371,24 +378,6 @@ export class ForestRun< }; } - private watchFragmentInternal(watch: Cache.WatchOptions) { - const id = watch.id ?? watch.rootId; - assert(id !== undefined); - - if (watch.immediate) { - const diff = this.diff(watch); - if (this.shouldNotifyWatch(watch, diff)) { - this.notifyWatch(watch, diff); - } - } - - accumulate(this.store.fragmentWatches, id, watch); - - return () => { - deleteAccumulated(this.store.fragmentWatches, id, watch); - }; - } - public watchFragment(options: { fragment: DocumentNode; fragmentName?: string; @@ -430,31 +419,49 @@ export class ForestRun< let latestDiff: Cache.DiffResult | undefined; return new Observable((observer) => { - return this.watch({ - ...diffOptions, - immediate: true, - callback: (diff: Cache.DiffResult) => { - const data = - diff.result == null ? ({} as TData) : diff.result; + const callback = (diff: Cache.DiffResult) => { + const data = diff.result == null ? ({} as TData) : diff.result; - if (latestDiff && equal(latestDiff.result, data)) { - return; - } + if (latestDiff && equal(latestDiff.result, data)) { + return; + } - const result = { - data, - complete: !!diff.complete, - missing: diff.missing, - }; + const result = { + data, + complete: !!diff.complete, + missing: diff.missing, + }; - latestDiff = { - ...diff, - result: data, - }; + latestDiff = { + ...diff, + result: data, + }; - observer.next(result); - }, - }); + observer.next(result); + }; + + const watchOptions: Cache.WatchOptions = { + ...diffOptions, + immediate: true, + callback, + }; + + // Use ForestRun fragment optimization: register directly in + // fragmentWatches keyed by entity ID for efficient lookups + // via collectAffectedWatches / transaction.affectedNodes + if (this.env.optimizeFragmentReads && id) { + const initialDiff = this.diff(watchOptions); + if (this.shouldNotifyWatch(watchOptions, initialDiff)) { + this.notifyWatch(watchOptions, initialDiff); + } + accumulate(this.store.fragmentWatches, id, watchOptions); + return () => { + deleteAccumulated(this.store.fragmentWatches, id, watchOptions); + }; + } + + // Fall back to standard operation watching + return this.watchOperation(watchOptions); }); }