From 2f38c3527d1ee318041e652350e2c094d7a45a09 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 18:07:17 +0000 Subject: [PATCH 1/2] docs(blog): Show v0.19 changes with diffs, a live demo and a migration guide - Batch set(): Before/After DiffEditor, live HooksPlayground timing demo, perf table and speedup chart (the old chart had unlabeled overlapping bars) - Entity pk() args: TypeScriptEditor example and DiffEditor for overrides - Add migration guide for the TypeScript 4.0 minimum, link it from the summary - Add the Vue useFetch() Ref type fix (#4114) to Other Improvements Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01BFGrVRphxPNNsZaH1qjaK2 --- website/blog/2026-10-03-v0.19-batch-set.md | 217 +++++++++++++++++---- 1 file changed, 176 insertions(+), 41 deletions(-) diff --git a/website/blog/2026-10-03-v0.19-batch-set.md b/website/blog/2026-10-03-v0.19-batch-set.md index 8bd47213dd73..eeca11698817 100644 --- a/website/blog/2026-10-03-v0.19-batch-set.md +++ b/website/blog/2026-10-03-v0.19-batch-set.md @@ -7,7 +7,12 @@ draft: true --- import DiffEditor from '@site/src/components/DiffEditor'; +import HooksPlayground from '@site/src/components/HooksPlayground'; import StackBlitz from '@site/src/components/StackBlitz'; +import TypeScriptEditor from '@site/src/components/TypeScriptEditor'; + +v0.19 lets a [Manager](/docs/concepts/managers) write a whole batch of streamed rows in one store update, and fixes a +round of TypeScript and Vue issues. **New APIs:** @@ -27,74 +32,141 @@ import StackBlitz from '@site/src/components/StackBlitz'; - [Entity](/rest/api/Entity) classes can be used where an `EntityInterface` is expected ([#4149](https://github.com/reactive/data-client/pull/4149)); [prepare `pk()` overrides](/blog/2026/10/03/v0.19-batch-set#entity-pk-args) for a future release - [useCache()](/docs/api/useCache) and [useDLE()](/docs/api/useDLE) return `undefined` for a deleted entity whose refetch failed, instead of a truthy `Symbol` that slipped past `if (!data)` checks; [remove any workarounds](/blog/2026/10/03/v0.19-batch-set#deleted-entity-undefined) ([#4150](https://github.com/reactive/data-client/pull/4150)) - Vue [useFetch()](/vue/api/useFetch) keeps its data from being garbage collected while mounted, like [useSuspense()](/vue/api/useSuspense), so a configured `gcPolicy` no longer evicts prefetched data that components read later ([#4152](https://github.com/reactive/data-client/pull/4152)) +- Vue [useFetch()](/vue/api/useFetch) is typed as the read-only `Ref` it returns, so `promise.resolved` (always `undefined`) is now a TypeScript error; read `promise.value.resolved` instead ([#4114](https://github.com/reactive/data-client/pull/4114)) -**Breaking Changes:** +**[Breaking Changes:](/blog/2026/10/03/v0.19-batch-set#migration-guide)** -- TypeScript 4.0 or later is required; the TypeScript 3.x declarations are removed ([#4151](https://github.com/reactive/data-client/pull/4151)) +- [TypeScript 4.0 or later is required](/blog/2026/10/03/v0.19-batch-set#typescript-4) - the TypeScript 3.x declarations are removed ([#4151](https://github.com/reactive/data-client/pull/4151)) {/* truncate */} ## Batch Controller.set() {#batch-set} -[Controller.set()](/docs/api/Controller#set-array) now accepts an [Array](/rest/api/Array) schema, so a list of rows -is normalized in one store update. Each row merges with its stored entity, and entities not in the list are untouched. +A [Manager](/docs/concepts/managers) that receives a stream of rows (like price tickers over a websocket) can now write +them all with one [Controller.set()](/docs/api/Controller#set-array) by passing an [Array](/rest/api/Array) schema. +Before v0.19, `[Ticker]` was a TypeScript error, so the usual workaround was a `set()` per row: -```ts -// Before: TypeScript error on [Ticker], so batches became one set() per row -for (const row of rows) { - ctrl.set(Ticker, { product_id: row.product_id }, row); -} + -// After: one store update -ctrl.set([Ticker], rows); +```ts title="Before" +ws.onmessage = event => { + const rows = JSON.parse(event.data); + for (const row of rows) { + ctrl.set(Ticker, { product_id: row.product_id }, row); + } +}; +``` + +```ts title="After" +ws.onmessage = event => { + const rows = JSON.parse(event.data); + ctrl.set([Ticker], rows); +}; ``` -This works at runtime before v0.19, but only typechecked for single entities, which pushed code toward a `set()` call -per row or a push-only endpoint with `setResponse()`. Array schemas take no `args` and no updater function. + + +Each row merges with its stored entity, and entities not in the list are untouched. Array schemas take no `args` and +no updater function. To batch mixed Entity types, deletes, or rows keyed by id, pass a [Union](/rest/api/Union), +[Invalidate](/rest/api/Invalidate#batch-invalidation), or [Values](/rest/api/Values) schema; see +[Controller.set()](/docs/api/Controller#set-array) for examples. [#4103](https://github.com/reactive/data-client/pull/4103) -Rows are typed by the Entity's fields. Use a [Union](/rest/api/Union) for lists that mix Entity types, -[Invalidate](/rest/api/Invalidate#batch-invalidation) to delete many entities at once, and [Values](/rest/api/Values) -for rows keyed by id. +Try both buttons below. Each writes 500 new prices; the timer shows how long until the store finished updating. -```ts -const Message = new schema.Union({ ticker: Ticker, trade: Trade }, 'type'); -ctrl.set([Message], messages); -ctrl.set([new schema.Invalidate(Ticker)], [{ product_id: 'BTC-USD' }]); -ctrl.set(new schema.Values(Ticker), { 'BTC-USD': row }); + + +```ts title="Ticker" collapsed +import { Entity } from '@data-client/rest'; + +export class Ticker extends Entity { + product_id = ''; + price = 0; + + pk() { + return this.product_id; + } + static key = 'Ticker'; +} + +export const newPrices = () => + Array.from({ length: 500 }, (_, i) => ({ + product_id: `COIN-${i}`, + price: Math.round(Math.random() * 10000) / 100, + })); ``` +```tsx title="PriceStream" +import { useController, useQuery } from '@data-client/react'; +import { Ticker, newPrices } from './Ticker'; + +function PriceStream() { + const ctrl = useController(); + const [timing, setTiming] = React.useState(''); + const first = useQuery(Ticker, { product_id: 'COIN-0' }); + + const time = async (label: string, write: () => Promise) => { + const start = performance.now(); + await write(); + setTiming(`${label}: ${(performance.now() - start).toFixed(1)} ms`); + }; + const perRow = () => + time('500 set() calls', () => + Promise.all( + newPrices().map(row => + ctrl.set(Ticker, { product_id: row.product_id }, row), + ), + ), + ); + // highlight-next-line + const batch = () => time('1 batch set()', () => ctrl.set([Ticker], newPrices())); + + return ( +
+ {' '} + +

COIN-0: {first ? `$${first.price}` : 'no data yet'}

+

{timing}

+
+ ); +} +render(); +``` + +
+ ### Performance {#batch-set-performance} Each `set()` is a separate store update, and every store update copies that entity type's table. Writing rows one at a time repeats that copy for every row, so the cost grows with both the batch size and the store size. A batch -pays it once: **20x** faster for 50 rows and **95x** faster for 500 rows, into a store holding 500 entities. +pays it once, and notifies subscribers once instead of once per row. [#4103](https://github.com/reactive/data-client/pull/4103) +| Rows written into a 500-entity store | One `set()` per row | `set([Ticker], rows)` | Speedup | +| ------------------------------------ | ------------------- | --------------------- | ------- | +| 50 | 10.8 ms | 0.54 ms | **20x** | +| 500 | 103 ms | 1.08 ms | **95x** | +
```mermaid xychart-beta - title "Writing rows into a 500-entity store" + title "Batch set() speedup vs one set() per row" x-axis ["50 rows", "500 rows"] - y-axis "Milliseconds (lower is better)" 0 --> 105 - bar [10.8, 103] - bar [0.54, 1.08] + y-axis "Times faster" 0 --> 100 + bar [20, 95] ```
-One `set()` per row vs one `set([Ticker], rows)`. This measures the store update only; a batch also notifies -subscribers once instead of once per row. - [Benchmarks over time](https://reactive.github.io/data-client/dev/bench/) | [View benchmark](https://github.com/reactive/data-client/tree/master/examples/benchmark)
-This is most useful in [Managers](/docs/concepts/managers) that receive high-frequency streams or large snapshots. -Buffer incoming messages and flush each batch with one `set()`, as described in +To use this in a [Manager](/docs/concepts/managers) that receives high-frequency streams or large snapshots, buffer +incoming messages and flush each batch with one `set()`, as described in [Batching high-frequency updates](/docs/concepts/managers#batching). The coin app's `StreamManager` now flushes Coinbase ticker messages this way. @@ -109,32 +181,54 @@ is the Array schema, so match `action.schema[0]` for `[Ticker]` rather than the ## Other improvements -### Entity pk() args {#entity-pk-args} +### Entity classes typecheck as EntityInterface {#entity-pk-args} + +Helpers typed to accept any Entity with `EntityInterface` rejected Entity classes, because +[Entity.pk()](/rest/api/Entity#pk) typed its `args` as a mutable array. It is now `readonly any[]`, so this +typechecks ([#4149](https://github.com/reactive/data-client/pull/4149)): -[Entity.pk()](/rest/api/Entity#pk) now types its `args` parameter as `readonly any[]`, so Entity classes can be passed -where an `EntityInterface` is expected ([#4149](https://github.com/reactive/data-client/pull/4149)). + ```ts +import { Entity } from '@data-client/rest'; import type { EntityInterface } from '@data-client/react'; -const schema: EntityInterface = User; +class User extends Entity { + id = ''; + name = ''; +} + +function entityName(schema: EntityInterface) { + return schema.key; +} + +entityName(User); ``` -:::tip + -Overrides of `static pk()` that annotate `args` as a mutable array still compile, but a future breaking release -will require `readonly`. Update them now: +If you override `static pk()` and type `args` as a mutable array, it still compiles, but a future breaking release +will require `readonly`. Update it now: -```ts + + +```ts title="Before" +class User extends Entity { + static pk(value: any, parent?: any, key?: string, args?: any[]) { + return `${value.id}-${args?.[0]?.org}`; + } +} +``` + +```ts title="After" class User extends Entity { - // highlight-next-line static pk(value: any, parent?: any, key?: string, args?: readonly any[]) { return `${value.id}-${args?.[0]?.org}`; } } ``` -::: + ### Deleted entities read as undefined {#deleted-entity-undefined} @@ -180,3 +274,44 @@ function TodoDetail({ id }: { id: number }) { To show something specific when the refetch failed (for example a 404 after deletion), read `error` from [useDLE()](/docs/api/useDLE) rather than inspecting `data`. + +## Migration guide + +import PkgTabs from '@site/src/components/PkgTabs'; + +This upgrade requires updating all package versions simultaneously. + + + +### TypeScript 4.0 or later {#typescript-4} + +Skip this section if you already use TypeScript 4.0 or later. + +The TypeScript 3.x declarations are removed. They no longer typechecked on any TypeScript 3.x version, and +`@data-client/rest` already required TypeScript 4.0. Upgrade TypeScript: + + + +```json title="Before" +{ + "devDependencies": { + "typescript": "^3.9.0" + } +} +``` + +```json title="After" +{ + "devDependencies": { + "typescript": "^4.0.0" + } +} +``` + + + +[#4151](https://github.com/reactive/data-client/pull/4151) + +### Upgrade support + +As usual, if you have any troubles or questions, feel free to join our [![Chat](https://img.shields.io/discord/768254430381735967.svg?style=flat-square&colorB=758ED3)](https://discord.gg/wXGV27xm6t) or [file a bug](https://github.com/reactive/data-client/issues/new/choose) From 1e3c6eb8281ea08884e159dc86853291e34336b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 18:08:06 +0000 Subject: [PATCH 2/2] docs(blog): Trim v0.19 perf table and TypeScript migration diff Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01BFGrVRphxPNNsZaH1qjaK2 --- website/blog/2026-10-03-v0.19-batch-set.md | 37 +++------------------- 1 file changed, 5 insertions(+), 32 deletions(-) diff --git a/website/blog/2026-10-03-v0.19-batch-set.md b/website/blog/2026-10-03-v0.19-batch-set.md index eeca11698817..1fe570383508 100644 --- a/website/blog/2026-10-03-v0.19-batch-set.md +++ b/website/blog/2026-10-03-v0.19-batch-set.md @@ -139,14 +139,10 @@ render(); Each `set()` is a separate store update, and every store update copies that entity type's table. Writing rows one at a time repeats that copy for every row, so the cost grows with both the batch size and the store size. A batch -pays it once, and notifies subscribers once instead of once per row. +pays it once, and notifies subscribers once instead of once per row: **20x** faster for 50 rows (10.8 ms to 0.54 ms) +and **95x** faster for 500 rows (103 ms to 1.08 ms), into a store holding 500 entities. [#4103](https://github.com/reactive/data-client/pull/4103) -| Rows written into a 500-entity store | One `set()` per row | `set([Ticker], rows)` | Speedup | -| ------------------------------------ | ------------------- | --------------------- | ------- | -| 50 | 10.8 ms | 0.54 ms | **20x** | -| 500 | 103 ms | 1.08 ms | **95x** | -
@@ -165,8 +161,7 @@ xychart-beta
-To use this in a [Manager](/docs/concepts/managers) that receives high-frequency streams or large snapshots, buffer -incoming messages and flush each batch with one `set()`, as described in +In a [Manager](/docs/concepts/managers), buffer incoming messages and flush each batch with one `set()`, as described in [Batching high-frequency updates](/docs/concepts/managers#batching). The coin app's `StreamManager` now flushes Coinbase ticker messages this way. @@ -287,30 +282,8 @@ This upgrade requires updating all package versions simultaneously. Skip this section if you already use TypeScript 4.0 or later. -The TypeScript 3.x declarations are removed. They no longer typechecked on any TypeScript 3.x version, and -`@data-client/rest` already required TypeScript 4.0. Upgrade TypeScript: - - - -```json title="Before" -{ - "devDependencies": { - "typescript": "^3.9.0" - } -} -``` - -```json title="After" -{ - "devDependencies": { - "typescript": "^4.0.0" - } -} -``` - - - -[#4151](https://github.com/reactive/data-client/pull/4151) +The TypeScript 3.x declarations are removed, since they no longer typechecked on any TypeScript 3.x version. Bump +`typescript` to `^4.0.0` or later in your `devDependencies`. [#4151](https://github.com/reactive/data-client/pull/4151) ### Upgrade support