Skip to content

Memoized mapping of observables instead of transformData in watchSingleFragment#13053

Merged
phryneas merged 9 commits intorelease-4.1from
pr/memoized-mapped-observables
Dec 18, 2025
Merged

Memoized mapping of observables instead of transformData in watchSingleFragment#13053
phryneas merged 9 commits intorelease-4.1from
pr/memoized-mapped-observables

Conversation

@phryneas
Copy link
Copy Markdown
Member

@phryneas phryneas commented Dec 15, 2025

This will memoize without a risk of memoization collisions if there are multiple code paths calling watchSingleFragment with the same fragment, but different transformData.

Follow-up to #13026

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Dec 15, 2025

🦋 Changeset detected

Latest commit: 7a60771

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@apollo-librarian
Copy link
Copy Markdown

apollo-librarian bot commented Dec 15, 2025

✅ Docs preview ready

The preview is ready to be viewed. View the preview

File Changes

0 new, 2 changed, 0 removed
* (developer-tools)/react/(latest)/data/fragments.mdx
* (developer-tools)/react/(latest)/data/mutations.mdx

Build ID: 6e8c97a701e0dcad48d3a2f8
Build Logs: View logs

URL: https://www.apollographql.com/docs/deploy-preview/6e8c97a701e0dcad48d3a2f8

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Dec 15, 2025

npm i https://pkg.pr.new/apollographql/apollo-client/@apollo/client@13053

commit: 4285d06

return Object.assign(
observable.pipe(
map(toMapped),
shareReplay({ bufferSize: 1, refCount: true })
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was missing on the current single-element track, and it's necessary to keep the observable active on synchronous unsubscribe/resubscribe.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this what this bit of code from watchSingleFragment does?

share({
  connector: () => new ReplaySubject(1),
  // debounce so a synchronous unsubscribe+resubscribe doesn't tear down the watch and create a new one
  resetOnRefCountZero: () => timer(0),
})

If I remove this shareReplay line, all tests continue to pass. Either the share from watchSingleFragment handles this correctly, or we have a missing test somewhere.

Copy link
Copy Markdown
Member Author

@phryneas phryneas Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes and no.

With a share in watchSingleFragment, but not in mapObservableFragment, this happens if you subscribe to the final Observable three times:

flowchart TD
    W[watchSingleFragmentObservable]
    W -->|subscribe1| O1[ObservableFragment]
    O1 -->|subscribe1| S1[Subscription]
    W -->|subscribe2| O1[ObservableFragment]
    O1 -->|subscribe2| S2[Subscription]
    W -->|subscribe3| O1[ObservableFragment]
    O1 -->|subscribe3| S3[Subscription]
Loading

Now, if you add share to mapObservableFragment, you get

flowchart TD
    W[watchSingleFragmentObservable]
    W -->|subscribe1, reused for 2,3| O1[ObservableFragment]
    O1 -->|subscribe1| S1[Subscription]
    O1 -->|subscribe2| S2[Subscription]
    O1 -->|subscribe3| S3[Subscription]
Loading

Now, if you look at the timeline with unsubscribe and immediately following subscribe, with resetOnRefCountZero of () => timer(0) in watchSingleFragment, but not mapObservableFragment:

sequenceDiagram
    Cache->>ObservableFragment: watch
    ObservableFragment->>watchSingleFragmentObservable: map
    User->>watchSingleFragmentObservable: subscribe
    watchSingleFragmentObservable->>ObservableFragment: subscribe
    ObservableFragment->>Cache: subscribe
    User->>watchSingleFragmentObservable: unsubscribe
    watchSingleFragmentObservable->>ObservableFragment: unsubscribe
    User->>watchSingleFragmentObservable: subscribe
    watchSingleFragmentObservable->>ObservableFragment: subscribe
Loading

And if both have that resetOnRefCountZero timer:

sequenceDiagram
    Cache->>ObservableFragment: watch
    ObservableFragment->>watchSingleFragmentObservable: map
    User->>watchSingleFragmentObservable: subscribe
    watchSingleFragmentObservable->>ObservableFragment: subscribe
    ObservableFragment->>Cache: subscribe
    User->>watchSingleFragmentObservable: unsubscribe
    User->>watchSingleFragmentObservable: subscribe
Loading

Tbh., I'm not sure if that really can be tested though.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm... something doesn't compute in my brain, so maybe you can help me understand where I'm misunderstanding.

Coming back to your original comment:

This was missing on the current single-element track, and it's necessary to keep the observable active on synchronous unsubscribe/resubscribe.

Wouldn't we have the same problem then if you called cache.watchFragment directly and subscribed multiple times and did the unsubscribe/subscribe synchronously yourself? The whole point of share with the ReplaySubject is so that values are multicast from the original watchSingleFragment and is only reset after you have no more subscriptions. That timer(0) obviously prevents it from resetting immediately. How is this different than using .pipe(...) where it subscribes to same the underlying observable multiple times?

Those first 2 charts illustrate this and this is also my understanding (1 subscription to the underlying watchSingleFragment vs many), but the 3rd chart is what seems wrong to me. Err... rather incomplete. What I think chart 3 and 4 don't show is when the underlying observable is reset (the ReplaySubject from share). Yes you get an unsubscribe in chart 3 to the ReplaySubject, but that subscription is still held until after the timer(0), so a synchronous resubscribe still maintains the original cache.watch watcher.

Am I missing something? Is it actually fully tearing down that cache.watch without this shareReplay here and I'm just missing it somehow?


By the way, I'm not entirely arguing against using shareReplay here (and sorry if it sounds like it). I'm mostly just trying to understand where my misunderstanding is here because in my mind the shareReplay doesn't have the affect that you're describing (restoring synchronous unsubscribe/subscribe behavior), but rather the only change it makes is demonstrated in the first 2 charts (1 subscription to the underlying observable vs many)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I mixed two of the steps in my 3rd and 4th graph 😓 ObservableFragment and watchSingleFragmentObservable need to switch places.

Yes you get an unsubscribe in chart 3 to the ReplaySubject, but that subscription is still held until after the timer(0), so a synchronous resubscribe still maintains the original cache.watch watcher.

It's more of a performance optimization I guess. Yes, the underlying Observable returned from watchSingleFragmentObservable will not unsubscribe, as it's still "hanging on".
But the "mapped" ObservableFragment will unsubscribe, resubscribe and call the mapFn again - where it could just also maintain the existing subscription for a moment longer.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the "mapped" ObservableFragment will unsubscribe, resubscribe and call the mapFn again - where it could just also maintain the existing subscription for a moment longer.

Got it, that makes more sense. Thanks!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown
Member

@jerelmiller jerelmiller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small question in regards to the shareReplay, but otherwise looks good.

return Object.assign(
observable.pipe(
map(toMapped),
shareReplay({ bufferSize: 1, refCount: true })
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this what this bit of code from watchSingleFragment does?

share({
  connector: () => new ReplaySubject(1),
  // debounce so a synchronous unsubscribe+resubscribe doesn't tear down the watch and create a new one
  resetOnRefCountZero: () => timer(0),
})

If I remove this shareReplay line, all tests continue to pass. Either the share from watchSingleFragment handles this correctly, or we have a missing test somewhere.

export { filterMap } from "./filterMap.js";
export { equalByQuery } from "./equalByQuery.js";
export { canonicalStringify } from "./canonicalStringify.js";
export { mapObservableFragmentMemoized } from "./mapObservableFragment.js";
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are your thoughts on exporting this as mapObservableFragment (or renaming the core function). Do we need the memoized as part of this "public" name?

Copy link
Copy Markdown
Member Author

@phryneas phryneas Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% sure if this is the "final form" we'll keep around, I can imagine a semi-public api with that name but potentially different api being added at some point, and either this variant will keep coexisting or swallowed up by that.
As with the api as it is, a memoization key is a big part of the api, I think I'd leave the name as it is right now - or at least avoid mapObservableFragment. If you have any alternative naming suggestions, I'm open :)

@github-actions github-actions bot added the auto-cleanup 🤖 label Dec 15, 2025
@phryneas phryneas merged commit 23ca0ba into release-4.1 Dec 18, 2025
32 of 33 checks passed
@phryneas phryneas deleted the pr/memoized-mapped-observables branch December 18, 2025 12:02
phryneas pushed a commit that referenced this pull request Dec 18, 2025
# Releases
## @apollo/[email protected]

### Patch Changes

- [#13053](#13053)
[`23ca0ba`](23ca0ba)
Thanks [@phryneas](https://github.com/phryneas)! - Use memoized
observable mapping when using `watchFragment`, `useFragment` or
`useSuspenseFragment`.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
jerelmiller pushed a commit that referenced this pull request Dec 18, 2025
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to release-4.1, this
PR will be updated.

⚠️⚠️⚠️⚠️⚠️⚠️

`release-4.1` is currently in **pre mode** so this branch has
prereleases rather than normal releases. If you want to exit
prereleases, run `changeset pre exit` on `release-4.1`.

⚠️⚠️⚠️⚠️⚠️⚠️

# Releases
## @apollo/[email protected]

### Minor Changes

- [#13056](#13056)
[`b224efc`](b224efc)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - `InMemoryCache`
no longer filters out explicitly returned `undefined` items from `read`
functions for array fields. This now makes it possible to create `read`
functions on array fields that return partial data and trigger a fetch
for the full list.

- [#13058](#13058)
[`121a2cb`](121a2cb)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Add an
`extensions` option to `cache.write`, `cache.writeQuery`, and
`client.writeQuery`. This makes `extensions` available in cache `merge`
functions which can be accessed with the other merge function options.

As a result of this change, any `extensions` returned in GraphQL
operations are now available in `merge` in the cache writes for these
operations.

### Patch Changes

- [#13053](#13053)
[`23ca0ba`](23ca0ba)
Thanks [@phryneas](https://github.com/phryneas)! - Use memoized
observable mapping when using `watchFragment`, `useFragment` or
`useSuspenseFragment`.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: apollogithubactionsbot[bot] <159198662+apollogithubactionsbot[bot]@users.noreply.github.com>
@github-actions github-actions bot mentioned this pull request Jan 15, 2026
jerelmiller pushed a commit that referenced this pull request Jan 15, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @apollo/[email protected]

### Minor Changes

- [#13043](#13043)
[`65e66ca`](65e66ca)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Support
`headers` transport for enhanced client awareness.

- [#12927](#12927)
[`785e223`](785e223)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - You can now
provide a callback function as the `context` option on the `mutate`
function returned by `useMutation`. The callback function is called with
the value of the `context` option provided to the `useMutation` hook.
This is useful if you'd like to merge the context object provided to the
`useMutation` hook with a value provided to the `mutate` function.

    ```ts
    function MyComponent() {
      const [mutate, result] = useMutation(MUTATION, {
        context: { foo: true },
      });

      async function runMutation() {
        await mutate({
          // sends context as { foo: true, bar: true }
          context: (hookContext) => ({ ...hookContext, bar: true }),
        });
      }

      // ...
    }
    ```

- [#12923](#12923)
[`94ea3e3`](94ea3e3)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Fix an issue
where deferred payloads that returned arrays with fewer items than the
original cached array would retain items from the cached array. This
change includes `@stream` arrays where stream arrays replace the cached
arrays.

- [#12927](#12927)
[`96b531f`](96b531f)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Don't set the
fallback value of a `@client` field to `null` when a `read` function is
defined. Instead the `read` function will be called with an `existing`
value of `undefined` to allow default arguments to be used to set the
returned value.

When a `read` function is not defined nor is there a defined resolver
for the field, warn and set the value to `null` only in that instance.

- [#12927](#12927)
[`45ebb52`](45ebb52)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Add support for
`from: null` in `client.watchFragment` and `cache.watchFragment`. When
`from` is `null`, the emitted result is:

    ```ts
    {
      data: null,
      dataState: "complete",
      complete: true,
    }
    ```

- [#12926](#12926)
[`2b7f2c1`](2b7f2c1)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Support the
newer incremental delivery format for the `@defer` directive implemented
in `[email protected]`. Import the `GraphQL17Alpha9Handler` to use
the newer incremental delivery format with `@defer`.

    ```ts
    import { GraphQL17Alpha9Handler } from "@apollo/client/incremental";

    const client = new ApolloClient({
      // ...
      incrementalHandler: new GraphQL17Alpha9Handler(),
    });
    ```

    > [!NOTE]
> In order to use the `GraphQL17Alpha9Handler`, the GraphQL server MUST
implement the newer incremental delivery format. You may see errors or
unusual behavior if you use the wrong handler. If you are using Apollo
Router, continue to use the `Defer20220824Handler` because Apollo Router
does not yet support the newer incremental delivery format.

- [#12927](#12927)
[`45ebb52`](45ebb52)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Add support for
arrays with `useFragment`, `useSuspenseFragment`, and
`client.watchFragment`. This allows the ability to use a fragment to
watch multiple entities in the cache. Passing an array to `from` will
return `data` as an array where each array index corresponds to the
index in the `from` array.

    ```ts
    function MyComponent() {
      const result = useFragment({
        fragment,
        from: [item1, item2, item3],
      });

      // `data` is an array with 3 items
console.log(result); // { data: [{...}, {...}, {...}], dataState:
"complete", complete: true }
    }
    ```

- [#12927](#12927)
[`45ebb52`](45ebb52)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Add a
`getCurrentResult` function to the observable returned by
`client.watchFragment` and `cache.watchFragment` that returns the
current value for the watched fragment.

    ```ts
    const observable = client.watchFragment({
      fragment,
      from: { __typename: "Item", id: 1 },
    });

    console.log(observable.getCurrentResult());
    // {
    //   data: {...},
    //   dataState: "complete",
    //   complete: true,
    // }
    ```

- [#13038](#13038)
[`109efe7`](109efe7)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Add the `from`
option to `readFragment`, `watchFragment`, and `updateFragment`.

- [#12918](#12918)
[`2e224b9`](2e224b9)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Add support for
the `@stream` directive on both the `Defer20220824Handler` and the
`GraphQL17Alpha2Handler`.

    > [!NOTE]
> The implementations of `@stream` differ in the delivery of incremental
results between the different GraphQL spec versions. If you upgrading
from the older format to the newer format, expect the timing of some
incremental results to change.

- [#13056](#13056)
[`b224efc`](b224efc)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - `InMemoryCache`
no longer filters out explicitly returned `undefined` items from `read`
functions for array fields. This now makes it possible to create `read`
functions on array fields that return partial data and trigger a fetch
for the full list.

- [#13058](#13058)
[`121a2cb`](121a2cb)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Add an
`extensions` option to `cache.write`, `cache.writeQuery`, and
`client.writeQuery`. This makes `extensions` available in cache `merge`
functions which can be accessed with the other merge function options.

As a result of this change, any `extensions` returned in GraphQL
operations are now available in `merge` in the cache writes for these
operations.

- [#12927](#12927)
[`96b531f`](96b531f)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Add an abstract
`resolvesClientField` function to `ApolloCache` that can be used by
caches to tell `LocalState` if it can resolve a `@client` field when a
local resolver is not defined.

`LocalState` will emit a warning and set a fallback value of `null` when
no local resolver is defined and `resolvesClientField` returns `false`,
or isn't defined. Returning `true` from `resolvesClientField` signals
that a mechanism in the cache will set the field value. In this case,
`LocalState` won't set the field value.

- [#13078](#13078)
[`bf1e0dc`](bf1e0dc)
Thanks [@phryneas](https://github.com/phryneas)! - Use the default
stream merge function for `@stream` fields only if stream info is
present. This change means that using the older `Defer20220824Handler`
will not use the default stream merge function and will instead truncate
the streamed array on the first chunk.

### Patch Changes

- [#12884](#12884)
[`d329790`](d329790)
Thanks [@phryneas](https://github.com/phryneas)! - Ensure that
`PreloadedQueryRef` instances are unsubscribed when garbage collected

- [#13086](#13086)
[`1a1d408`](1a1d408)
Thanks [@phryneas](https://github.com/phryneas)! - Change the returned
value from `null` to `{}` when all fields in a query were skipped.

This also fixes a bug where `useSuspenseQuery` would suspend
indefinitely when all fields were skipped.

- [#13010](#13010)
[`7627000`](7627000)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Fix an issue
where errors parsed from incremental chunks in `ErrorLink` might throw
when using the `GraphQL17Alpha9Handler`.

- [#12927](#12927)
[`45ebb52`](45ebb52)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Deduplicate
watches created by `useFragment`, `client.watchFragment`, and
`cache.watchFragment` that contain the same fragment, variables, and
identifier. This should improve performance in situations where a
`useFragment` or a `client.watchFragment` is used to watch the same
object in multiple places of an application.

- [#12927](#12927)
[`259ae9b`](259ae9b)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Allow
`FragmentType` not only to be called as `FragmentType<TData>`, but also
as `FragmentType<TypedDocumentNode>`.

- [#12925](#12925)
[`5851800`](5851800)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Fix an issue
where calling `fetchMore` with `@defer` or `@stream` would not rerender
incremental results as they were streamed.

- [#12927](#12927)
[`9e55188`](9e55188)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Truncate
`@stream` arrays only on last chunk by default.

- [#13083](#13083)
[`f3c2be1`](f3c2be1)
Thanks [@phryneas](https://github.com/phryneas)! - Expose the
`ExtensionsWithStreamInfo` type for `extensions` in `Cache.writeQuery`,
`Cache.write` and `Cache.update` so other cache implementations also can
correctly access them.

- [#12923](#12923)
[`94ea3e3`](94ea3e3)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Improve the
cache data loss warning message when `existing` or `incoming` is an
array.

- [#12927](#12927)
[`4631175`](4631175)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Ignore
top-level `data` values on subsequent chunks in incremental responses.

- [#12927](#12927)
[`2be8de2`](2be8de2)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Create
mechanism to add experimental features to Apollo Client

- [#12927](#12927)
[`96b531f`](96b531f)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Ensure
`LocalState` doesn't try to read from the cache when using a `no-cache`
fetch policy.

- [#12927](#12927)
[`bb8ed7b`](bb8ed7b)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Ensure an error
is thrown when `@stream` is detected and an `incrementalDelivery`
handler is not configured.

- [#13053](#13053)
[`23ca0ba`](23ca0ba)
Thanks [@phryneas](https://github.com/phryneas)! - Use memoized
observable mapping when using `watchFragment`, `useFragment` or
`useSuspenseFragment`.

- [#12927](#12927)
[`44706a2`](44706a2)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Add helper type
`QueryRef.ForQuery<TypedDocumentNode>`

- [#13082](#13082)
[`c257418`](c257418)
Thanks [@phryneas](https://github.com/phryneas)! - Pass `streamInfo`
through result extensions as a `WeakRef`.

- [#12927](#12927)
[`4631175`](4631175)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Fix the
`Defer20220824Handler.SubsequentResult` type to match the
`FormattedSubsequentIncrementalExecutionResult` type in
`[email protected]`.

- [#12927](#12927)
[`96b531f`](96b531f)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Warn when using
a `no-cache` fetch policy without a local resolver defined. `no-cache`
queries do not read or write to the cache which meant `no-cache` queries
are silently incomplete when the `@client` field value was handled by a
cache `read` function.

- [#12927](#12927)
[`5776ea0`](5776ea0)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Update the
`accept` header used with the `GraphQL17Alpha9Handler` to
`multipart/mixed;incrementalSpec=v0.2` to ensure the newest incremental
delivery format is requested.

- [#12927](#12927)
[`45ebb52`](45ebb52)
Thanks [@jerelmiller](https://github.com/jerelmiller)! -
`DeepPartial<Array<TData>>` now returns `Array<DeepPartial<TData>>`
instead of `Array<DeepPartial<TData | undefined>>`.

- [#13071](#13071)
[`99ffe9a`](99ffe9a)
Thanks [@phryneas](https://github.com/phryneas)! - `prerenderStatic`:
Expose return value of `renderFunction` to userland, fix `aborted`
property.

    This enables usage of `resumeAndPrerender` with React 19.2.

- [#13026](#13026)
[`05eee67`](05eee67)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Reduce the
number of observables created by `watchFragment` by reusing existing
observables as much as possible. This should improve performance when
watching the same item in the cache multiple times after a cache update
occurs.

- [#13010](#13010)
[`7627000`](7627000)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Handle
`@stream` payloads that send multiple items in the same chunk when using
the `Defer20220824Handler`.

- [#13010](#13010)
[`7627000`](7627000)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Handle an edge
case with the `Defer20220824Handler` where an error for a `@stream` item
that bubbles to the `@stream` boundary (such as an item returning `null`
for a non-null array item) would write items from future chunks to the
wrong array index. In these cases, the `@stream` field is no longer
processed and future updates to the field are ignored. This prevents
runtime errors that TypeScript would otherwise not be able to catch.

- [#13081](#13081)
[`1e06ad7`](1e06ad7)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Avoid calling
`merge` functions more than once for the same incremental chunk.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jan 18, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants