Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7f784a8
fix(fetchium): optional store hooks for acknowledged, merged and chec…
abedell-phantom Oct 6, 2026
c9b4dfe
fix(fetchium): close the snapshot, hydration and event-persistence re…
abedell-phantom Oct 6, 2026
cb4a211
perf(fetchium): skip the per-snapshot key-cache lookup and an event's…
abedell-phantom Oct 6, 2026
f67c516
perf(fetchium): take the extra store work back out of streamed events
abedell-phantom Oct 6, 2026
e68c875
perf(fetchium): stop re-running a shared entity's consumers, copying …
abedell-phantom Oct 6, 2026
e583e75
chore(fetchium): major changeset listing the behavior changes an upgr…
abedell-phantom Oct 6, 2026
4897103
fix(fetchium): keep merging an event-built entity whose events carrie…
abedell-phantom Oct 6, 2026
94a754e
chore(fetchium): shorten this change's changesets for app developers
abedell-phantom Oct 6, 2026
8be1a63
chore(fetchium): rewrite this change's changesets for React developers
abedell-phantom Oct 6, 2026
cc302e1
Merge branch 'perf/fast-delivery' into fix/perf-stack-regressions
abedell-phantom Oct 6, 2026
281e428
test(fetchium): cover the async write order of a created child and it…
abedell-phantom Oct 6, 2026
3213594
chore(fetchium): tighten comments
abedell-phantom Oct 7, 2026
ebab339
Merge branch 'perf/fast-delivery' into fix/perf-stack-regressions
abedell-phantom Oct 7, 2026
f412cb9
chore(fetchium): drop PR numbers from a test header
abedell-phantom Oct 7, 2026
2aba512
Merge branch 'perf/fast-delivery' into fix/perf-stack-regressions
abedell-phantom Oct 7, 2026
99982a5
fix(fetchium): keep store bookkeeping right after a failed write or a…
abedell-phantom Oct 7, 2026
feeacdc
chore(fetchium): cut and shorten comments
abedell-phantom Oct 7, 2026
d468448
Merge branch 'perf/fast-delivery' into fix/perf-stack-regressions
abedell-phantom Oct 7, 2026
96a5200
fix(fetchium): keep event writes and store listeners safe when the st…
abedell-phantom Oct 7, 2026
1ae384d
Merge branch 'perf/fast-delivery' into fix/perf-stack-regressions
abedell-phantom Oct 7, 2026
208a412
Merge branch 'perf/fast-delivery' into fix/perf-stack-regressions
abedell-phantom Oct 7, 2026
1bf0674
Merge branch 'perf/fast-delivery' into fix/perf-stack-regressions
abedell-phantom Oct 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/skip-write-and-snapshot-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'fetchium': patch
---

Fixed stale data in components when a second query loads entities that are already in memory, and streamed updates that were lost on restart.

- Development builds freeze query results, so mutating one (such as calling `sort()` on a list in render) throws a `TypeError`. Copy the data first, and treat production results as read-only too.
- In development, the stale-data check logs an error instead of throwing and blocking other components' updates.
- Custom stores can add the new optional `QueryStore.mergeEntity`, so an entity built from streamed updates doesn't overwrite stored fields those updates didn't include. Without it, writes work as before.
19 changes: 19 additions & 0 deletions .changeset/upgrade-behavior-changes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'fetchium': major
---

Breaking changes. Most apps need no code changes, but tests that depend on loading frames, render counts or timers may.

- **Results are frozen in development.** Mutating query data throws a `TypeError`; copy it first (`[...data.items].sort()`).
- **Cached data shows on the first render** with `SyncQueryStore`. Update tests that wait for a loading state or count renders.
- **Fetches start sooner.** The first fetch and refetches without `debounce` start on a microtask, not after `setTimeout(0)`; update tests that advance timers to see them.
- **Identical data doesn't re-render.** A refetch, poll or streamed update with the same data skips the render; read `isFetching` instead of counting renders.
- **Nested values change identity only when they change.** `t.object`, `t.record` and live-collection values are new objects after a change, so memoized children re-render; drop workarounds that forced it.
- **`t.record` and `t.result` fields apply updates** instead of keeping their old value.
- **Changing a Signal param mid-fetch aborts that fetch** and fetches the new params; the old response is dropped.
- **`QueryClient.destroy()` aborts fetches in flight.** Code that awaits a query during teardown gets an `AbortError`; catch it.
- **Awaiting a query can wait for a remount.** If the last component using a query unmounts before its first data arrives, `await fetchQuery(...)` waits for the next mount, `gcTime` collection or `destroy()` instead of rejecting with an `AbortError`.
- **New reserved `QueryClientConfig` names:** `shouldRetry`, `reactivationGraceMs`, `reactivationStaggerMs`, `activity` and `pollResumeJitterMs` are read as options; rename custom context values that use them.
- **`retry: {}` and `retry: { retryDelay }` now retry** like `retry: true`; use `retry: false` for no retries.
- **`AsyncQueryStore` honors `cache.maxCount`.** Check the value if you set one.
- **Custom `QueryStore`s** get new optional methods (`mergeEntity`, `onDelete`, `onPersisted`, `isSettled`, `hasQueuedDeletes`, `hasEntity`); without them nothing changes.
2 changes: 1 addition & 1 deletion docs/src/app/api/fetchium-react/page.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ const SearchResults = component(() => {

- `useQuery` is a thin wrapper around Signalium v3's `useReactive`, which is deep-by-default. It returns a **structurally-shared snapshot** of the query result, so memoized children that receive subtrees as props keep stable references when the underlying data is unchanged.
- Fetchium registers a custom snapshot for entity proxies so the snapshot walks into entities (instead of returning them by reference), giving you correct re-rendering on entity field changes.
- The snapshot is a plain-object copy of the query result to prevent accidental mutation of the entity cache. Use `draft()` from `fetchium` if you need a mutable copy for mutations.
- The snapshot is a plain-object copy of the query result to prevent accidental mutation of the entity cache. Treat it as read-only: in development builds the snapshot and every nested object and array in it are frozen, so an in-place `sort()`, a `push()` or an assignment in render throws a `TypeError` at that line. Production snapshots are not frozen, and a mutation there is carried forward until the entity changes. Sort a copy (`[...items].sort()`) or use `draft()` from `fetchium` if you need a mutable copy for mutations.
- `useQuery` never suspends. Reading `.value` while pending returns `undefined`. Use [`useSuspenseQuery`](#usesuspensequery) to suspend on a cold miss.

---
Expand Down
26 changes: 13 additions & 13 deletions docs/src/app/api/fetchium/page.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,14 +241,14 @@ new QueryClient(config: QueryClientConfig)

#### Methods

| Method | Signature | Description |
| -------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `getContext` | `(): QueryContext` | Returns the `QueryContext` passed at construction. |
| `applyMutationEvent` | `(event: MutationEvent): void` | Applies an external mutation event (create/update/delete) to the entity store. |
| `invalidateQueries` | `(targets: ReadonlyArray<InvalidateTarget>): void` | Marks matching query instances as stale. Accepts query classes and optional param subsets for filtering. |
| `prefetch` | `(QueryClass, params?, { ttl? }?): () => void` | Starts a query now and keeps it active for `ttl` ms (default `DEFAULT_PREFETCH_TTL`, 10 s). See below. |
| `retain` | `(fn: () => unknown, { ttl? }?): () => void` | Keeps the queries `fn` reads active until the returned `release` is called, or for `ttl` ms. See below. |
| `destroy` | `(): void` | Tears down the GC manager, network manager, and all caches, and releases outstanding leases. |
| Method | Signature | Description |
| -------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `getContext` | `(): QueryContext` | Returns the `QueryContext` passed at construction. |
| `applyMutationEvent` | `(event: MutationEvent): void` | Applies an external mutation event (create/update/delete) to the entity store. |
| `invalidateQueries` | `(targets: ReadonlyArray<InvalidateTarget>): void` | Marks matching query instances as stale. Accepts query classes and optional param subsets for filtering. |
| `prefetch` | `(QueryClass, params?, { ttl? }?): () => void` | Starts a query now and keeps it active for `ttl` ms (default `DEFAULT_PREFETCH_TTL`, 10 s). See below. |
| `retain` | `(fn: () => unknown, { ttl? }?): () => void` | Keeps the queries `fn` reads active until the returned `release` is called, or for `ttl` ms. See below. |
| `destroy` | `(): void` | Tears down the GC manager, network manager, and all caches, releases outstanding leases, and unsubscribes the store listeners it registered. |

#### `prefetch` and `retain`

Expand Down Expand Up @@ -710,8 +710,8 @@ The `t` object provides a declarative type definition DSL for describing query p

### Store types

| Type | Definition | Description |
| -------------------- | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `QueryStore` | Interface | Storage backend interface. See [stores/sync](/api/stores-sync) and [stores/async](/api/stores-async). |
| `CachedQuery` | `{ value: unknown; refIds: Set<number> \| undefined; updatedAt: number; preloadedEntities?: PreloadedEntityMap }` | Cached query data returned by `QueryStore.loadQuery()`. |
| `PreloadedEntityMap` | `Map<number, Record<string, unknown>>` | Pre-loaded entity data for hydration. |
| Type | Definition | Description |
| -------------------- | ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `QueryStore` | Interface | Storage backend interface. See [stores/sync](/api/stores-sync) and [stores/async](/api/stores-async). Besides the required methods, a store may implement `onDelete` (the client then skips writing entities whose records are current, and writes one again once the store reports its record dropped), `onPersisted` (for a store that applies writes later), `isSettled`, `hasEntity` (lets a streamed event refresh the record of an entity that is not in memory) and `mergeEntity` (merges the fields streamed events supplied for an entity built from them over its record; without it, such an entity is written whole through `saveEntity`). A store without `onDelete` is written on every apply, as before 0.6; when `hasEntity` is missing or answers `undefined`, an event for an entity that is not in memory and that no live array retains is written only as a `mergeEntity` call with `ifStored`, which writes nothing when the store holds no record, and not at all without `mergeEntity`. |
| `CachedQuery` | `{ value: unknown; refIds: Set<number> \| undefined; updatedAt: number; preloadedEntities?: PreloadedEntityMap }` | Cached query data returned by `QueryStore.loadQuery()`. |
| `PreloadedEntityMap` | `Map<number, Record<string, unknown>>` | Pre-loaded entity data for hydration. |
Loading
Loading