diff --git a/.changeset/collection-context-detection.md b/.changeset/collection-context-detection.md deleted file mode 100644 index 009a631cec4b..000000000000 --- a/.changeset/collection-context-detection.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@data-client/endpoint': minor -'@data-client/rest': minor -'@data-client/graphql': minor -'@data-client/normalizr': minor -'@data-client/core': minor -'@data-client/react': minor -'@data-client/vue': minor ---- - -Allow one `Collection` schema to be used both top-level and nested. - -Before: - -```ts -const getTodos = new Collection([Todo], { argsKey }); -const userTodos = new Collection([Todo], { nestKey }); -``` - -After: - -```ts -const userTodos = new Collection([Todo], { argsKey, nestKey }); -``` diff --git a/.changeset/denormalize-delegate.md b/.changeset/denormalize-delegate.md deleted file mode 100644 index be78e1a74e55..000000000000 --- a/.changeset/denormalize-delegate.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -'@data-client/endpoint': minor -'@data-client/rest': minor -'@data-client/graphql': minor -'@data-client/normalizr': minor -'@data-client/core': minor -'@data-client/react': minor -'@data-client/vue': minor ---- - -**BREAKING**: `Schema.denormalize()` is now `(input, delegate)` instead -of the previous `(input, args, unvisit)` 3-parameter signature. - -```ts -// before -denormalize(input, args, unvisit) { - return unvisit(this.schema, input); -} - -// after -denormalize(input, delegate) { - return delegate.unvisit(this.schema, input); -} -``` - -The new [`IDenormalizeDelegate`](https://dataclient.io/rest/api/CustomSchema) -exposes `unvisit`, `args`, and a new `argsKey(fn)` helper that registers -a memoization dimension when output varies with endpoint args. Reading -`delegate.args` directly does *not* contribute to cache invalidation — -schemas that branch on args must call `argsKey`. The `fn` reference -doubles as the cache path key, so it must be **referentially stable** -— define it on the instance or at module scope, not inline per call: - -```ts -class LensSchema { - constructor({ lens }) { - this.lensSelector = lens; // stable reference across calls - } - denormalize(input, delegate) { - const portfolio = delegate.argsKey(this.lensSelector); - return this.lookup(input, portfolio); - } -} -``` - -All built-in schemas (`Array`, `Object`, `Values`, `Union`, `Query`, -`Invalidate`, `Lazy`, `Collection`) have been updated. Custom schemas -implementing `SchemaSimple` must update their `denormalize` signature. - -`Schema.normalize()` and the `visit()` callback also gain an optional -trailing `parentEntity` argument tracking the nearest enclosing -entity-like schema. This is additive — existing schemas don't need -changes unless they want to use it. diff --git a/.changeset/fix-journey-mutation.md b/.changeset/fix-journey-mutation.md deleted file mode 100644 index 7cfb9d8b69d3..000000000000 --- a/.changeset/fix-journey-mutation.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@data-client/normalizr': patch -'@data-client/core': patch -'@data-client/endpoint': patch -'@data-client/rest': patch -'@data-client/graphql': patch -'@data-client/react': patch ---- - -Fix cached journey being mutated on repeated result-cache hits. - -`GlobalCache.getResults` called `paths.shift()` on a cache hit, mutating -the `journey` array stored by reference on the `WeakDependencyMap` `Link` -node. After the first hit stripped the placeholder input slot, every -subsequent hit on the same cached entry would shift off a real -`EntityPath`, progressively losing subscription entries. This could cause -missed `countRef` tracking (premature GC of still-referenced entities) -and incorrect `entityExpiresAt` calculations. The hit path now returns a -non-mutating copy. diff --git a/.changeset/normalize-delegate.md b/.changeset/normalize-delegate.md deleted file mode 100644 index 440586fa52b6..000000000000 --- a/.changeset/normalize-delegate.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -"@data-client/endpoint": minor -"@data-client/normalizr": minor ---- - -Move normalize `args` and recursive `visit` into the existing normalize delegate passed to schemas. -Custom `Schema.normalize()` implementations should migrate from -`normalize(input, parent, key, args, visit, delegate, parentEntity?)` to -`normalize(input, parent, key, delegate, parentEntity?)`, then read -`delegate.args` and call `delegate.visit()` for recursive normalization. - -Before: - -```ts -class WrapperSchema { - normalize(input, parent, key, args, visit, delegate) { - const normalized = visit(this.schema, input.value, input, 'value', args); - delegate.mergeEntity(this, this.pk(input, parent, key, args), normalized); - return normalized; - } -} -``` - -After: - -```ts -class WrapperSchema { - normalize(input, parent, key, delegate) { - const { args, visit } = delegate; - const normalized = visit(this.schema, input.value, input, 'value'); - delegate.mergeEntity(this, this.pk(input, parent, key, args), normalized); - return normalized; - } -} -``` diff --git a/.changeset/peerdeps-018.md b/.changeset/peerdeps-018.md deleted file mode 100644 index 8c56c499f41b..000000000000 --- a/.changeset/peerdeps-018.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@data-client/img': patch -'@data-client/test': patch ---- - -Bump `@data-client/react` peer dependency range to include `^0.18.0`. diff --git a/.changeset/scalar-entity-binding.md b/.changeset/scalar-entity-binding.md deleted file mode 100644 index 6ae363a785ba..000000000000 --- a/.changeset/scalar-entity-binding.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -'@data-client/endpoint': minor -'@data-client/rest': minor -'@data-client/graphql': minor -'@data-client/normalizr': minor -'@data-client/core': minor -'@data-client/react': minor -'@data-client/vue': minor ---- - -Add [Scalar](https://dataclient.io/rest/api/Scalar) schema for lens-dependent entity fields. - -`Scalar` models entity fields whose values vary by a runtime "lens" (such as the -selected portfolio, currency, or locale). Multiple components can render the -same entity through different lenses simultaneously — each sees the correct -values without the entity itself ever being mutated. Lens-dependent values are -stored in a separate cell table and joined at denormalize time from endpoint -args. - -New exports: `Scalar`, `schema.Scalar`. - -A single `Scalar` instance can serve both as an `Entity.schema` field (parent -entity inferred from the visit) and standalone — inside `Values(Scalar)`, -`[Scalar]`, or `Collection([Scalar])` — for cheap column-only refreshes -(entity bound explicitly via `entity`). Cell pks are derived from the map key -or via `Scalar.entityPk()`, which defaults to `Entity.pk()` so custom and -composite primary keys work with no override: - -```ts -import { Collection, Entity, RestEndpoint, Scalar } from '@data-client/rest'; - -class Company extends Entity { - id = ''; - price = 0; - pct_equity = 0; - shares = 0; -} -const PortfolioScalar = new Scalar({ - lens: args => args[0]?.portfolio, - key: 'portfolio', - entity: Company, -}); -Company.schema = { - pct_equity: PortfolioScalar, - shares: PortfolioScalar, -}; - -// Full load — Company rows + scalar cells for the current portfolio -export const getCompanies = new RestEndpoint({ - path: '/companies', - searchParams: {} as { portfolio: string }, - schema: new Collection([Company], { argsKey: () => ({}) }), -}); -// Lens-only refresh — writes to the same Scalar(portfolio) cell table -export const getPortfolioColumns = new RestEndpoint({ - path: '/companies/columns', - searchParams: {} as { portfolio: string }, - schema: new Collection([PortfolioScalar], { - argsKey: ({ portfolio }) => ({ portfolio }), - }), -}); -``` - -`useSuspense(getCompanies, { portfolio: 'A' })` and -`useSuspense(getCompanies, { portfolio: 'B' })` resolve to different -`pct_equity` / `shares` while sharing the same `Company` row. - -`Scalar.queryKey` enumerates cells in its table for the current lens, so -endpoints that use `Scalar` directly as their top-level schema reconstruct -from cache without a network round-trip once the cells are present. diff --git a/examples/benchmark-react/CHANGELOG.md b/examples/benchmark-react/CHANGELOG.md index c8b8ab9feefe..0c8753c8b0fb 100644 --- a/examples/benchmark-react/CHANGELOG.md +++ b/examples/benchmark-react/CHANGELOG.md @@ -1,5 +1,15 @@ # example-benchmark-react +## 0.1.10 + +### Patch Changes + +- Updated dependencies [[`959465a`](https://github.com/reactive/data-client/commit/959465a064db687176e483932987b083f19718eb), [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32), [`6e8e499`](https://github.com/reactive/data-client/commit/6e8e499441741b58ad35127b517e8d83fc7a58fd), [`396d163`](https://github.com/reactive/data-client/commit/396d163e6f4991818519ec33d903a85437483dfd), [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32)]: + - @data-client/endpoint@0.18.0 + - @data-client/rest@0.18.0 + - @data-client/core@0.18.0 + - @data-client/react@0.18.0 + ## 0.1.9 ### Patch Changes diff --git a/examples/benchmark-react/package.json b/examples/benchmark-react/package.json index 040a5c974bfd..9afe77ba93fa 100644 --- a/examples/benchmark-react/package.json +++ b/examples/benchmark-react/package.json @@ -1,6 +1,6 @@ { "name": "example-benchmark-react", - "version": "0.1.9", + "version": "0.1.10", "private": true, "description": "React rendering benchmark comparing @data-client/react against other data libraries", "scripts": { diff --git a/examples/benchmark/CHANGELOG.md b/examples/benchmark/CHANGELOG.md index 0118307f787f..dc941ae4b237 100644 --- a/examples/benchmark/CHANGELOG.md +++ b/examples/benchmark/CHANGELOG.md @@ -1,5 +1,14 @@ # example-benchmark +## 0.4.85 + +### Patch Changes + +- Updated dependencies [[`959465a`](https://github.com/reactive/data-client/commit/959465a064db687176e483932987b083f19718eb), [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32), [`6e8e499`](https://github.com/reactive/data-client/commit/6e8e499441741b58ad35127b517e8d83fc7a58fd), [`396d163`](https://github.com/reactive/data-client/commit/396d163e6f4991818519ec33d903a85437483dfd), [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32)]: + - @data-client/endpoint@0.18.0 + - @data-client/normalizr@0.18.0 + - @data-client/core@0.18.0 + ## 0.4.84 ### Patch Changes diff --git a/examples/benchmark/package.json b/examples/benchmark/package.json index b97ed9ecc831..4df533c68d7a 100644 --- a/examples/benchmark/package.json +++ b/examples/benchmark/package.json @@ -1,6 +1,6 @@ { "name": "example-benchmark", - "version": "0.4.84", + "version": "0.4.85", "description": "Benchmark for normalizr", "main": "index.js", "author": "Nathaniel Tucker", diff --git a/examples/coin-app/CHANGELOG.md b/examples/coin-app/CHANGELOG.md index c267a5d02e68..2bba037eff1d 100644 --- a/examples/coin-app/CHANGELOG.md +++ b/examples/coin-app/CHANGELOG.md @@ -1,5 +1,14 @@ # coinbase-lite +## 0.0.25 + +### Patch Changes + +- Updated dependencies [[`959465a`](https://github.com/reactive/data-client/commit/959465a064db687176e483932987b083f19718eb), [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32), [`6e8e499`](https://github.com/reactive/data-client/commit/6e8e499441741b58ad35127b517e8d83fc7a58fd), [`89e06d3`](https://github.com/reactive/data-client/commit/89e06d38ab839f12f468283a6ff416983f95d65a), [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32)]: + - @data-client/rest@0.18.0 + - @data-client/react@0.18.0 + - @data-client/img@0.18.0 + ## 0.0.24 ### Patch Changes diff --git a/examples/coin-app/package.json b/examples/coin-app/package.json index dfb88a690344..8aaad58b121e 100644 --- a/examples/coin-app/package.json +++ b/examples/coin-app/package.json @@ -1,6 +1,6 @@ { "name": "coinbase-lite", - "version": "0.0.24", + "version": "0.0.25", "packageManager": "yarn@4.14.1", "description": "Coin App", "scripts": { @@ -44,9 +44,9 @@ "@anansi/core": "0.22.10", "@anansi/router": "0.10.23", "@babel/runtime-corejs3": "7.29.2", - "@data-client/img": "0.16.0", - "@data-client/react": "0.16.7", - "@data-client/rest": "0.17.0", + "@data-client/img": "0.18.0", + "@data-client/react": "0.18.0", + "@data-client/rest": "0.18.0", "core-js": "3.49.0", "d3": "7.9.0", "history": "*", diff --git a/examples/normalizr-github/CHANGELOG.md b/examples/normalizr-github/CHANGELOG.md index 54482a6ffc7a..5d0125805aa0 100644 --- a/examples/normalizr-github/CHANGELOG.md +++ b/examples/normalizr-github/CHANGELOG.md @@ -1,5 +1,13 @@ # normalizr-github-example +## 0.1.59 + +### Patch Changes + +- Updated dependencies [[`959465a`](https://github.com/reactive/data-client/commit/959465a064db687176e483932987b083f19718eb), [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32), [`6e8e499`](https://github.com/reactive/data-client/commit/6e8e499441741b58ad35127b517e8d83fc7a58fd), [`396d163`](https://github.com/reactive/data-client/commit/396d163e6f4991818519ec33d903a85437483dfd), [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32)]: + - @data-client/endpoint@0.18.0 + - @data-client/normalizr@0.18.0 + ## 0.1.58 ### Patch Changes diff --git a/examples/normalizr-github/package.json b/examples/normalizr-github/package.json index e551986c0a14..7dae3e4f1e4c 100644 --- a/examples/normalizr-github/package.json +++ b/examples/normalizr-github/package.json @@ -1,6 +1,6 @@ { "name": "normalizr-github-example", - "version": "0.1.58", + "version": "0.1.59", "description": "And example of using Normalizr with github", "main": "index.js", "author": "Paul Armstrong", diff --git a/examples/normalizr-redux/CHANGELOG.md b/examples/normalizr-redux/CHANGELOG.md index 3c12c1b26277..7434388a11e0 100644 --- a/examples/normalizr-redux/CHANGELOG.md +++ b/examples/normalizr-redux/CHANGELOG.md @@ -1,5 +1,13 @@ # normalizr-redux-example +## 0.1.57 + +### Patch Changes + +- Updated dependencies [[`959465a`](https://github.com/reactive/data-client/commit/959465a064db687176e483932987b083f19718eb), [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32), [`6e8e499`](https://github.com/reactive/data-client/commit/6e8e499441741b58ad35127b517e8d83fc7a58fd), [`396d163`](https://github.com/reactive/data-client/commit/396d163e6f4991818519ec33d903a85437483dfd), [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32)]: + - @data-client/endpoint@0.18.0 + - @data-client/normalizr@0.18.0 + ## 0.1.56 ### Patch Changes diff --git a/examples/normalizr-redux/package.json b/examples/normalizr-redux/package.json index ecda13f220aa..c7fd6ba76919 100644 --- a/examples/normalizr-redux/package.json +++ b/examples/normalizr-redux/package.json @@ -1,6 +1,6 @@ { "name": "normalizr-redux-example", - "version": "0.1.56", + "version": "0.1.57", "description": "And example of using Normalizr with Redux", "main": "index.js", "author": "Paul Armstrong", diff --git a/examples/normalizr-relationships/CHANGELOG.md b/examples/normalizr-relationships/CHANGELOG.md index 23e0ca7e0c90..b81adfa18de4 100644 --- a/examples/normalizr-relationships/CHANGELOG.md +++ b/examples/normalizr-relationships/CHANGELOG.md @@ -1,5 +1,13 @@ # normalizr-relationships +## 0.1.59 + +### Patch Changes + +- Updated dependencies [[`959465a`](https://github.com/reactive/data-client/commit/959465a064db687176e483932987b083f19718eb), [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32), [`6e8e499`](https://github.com/reactive/data-client/commit/6e8e499441741b58ad35127b517e8d83fc7a58fd), [`396d163`](https://github.com/reactive/data-client/commit/396d163e6f4991818519ec33d903a85437483dfd), [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32)]: + - @data-client/endpoint@0.18.0 + - @data-client/normalizr@0.18.0 + ## 0.1.58 ### Patch Changes diff --git a/examples/normalizr-relationships/package.json b/examples/normalizr-relationships/package.json index 95ef917e5b78..aab4eeb47a75 100644 --- a/examples/normalizr-relationships/package.json +++ b/examples/normalizr-relationships/package.json @@ -1,6 +1,6 @@ { "name": "normalizr-relationships", - "version": "0.1.58", + "version": "0.1.59", "description": "And example of using Normalizr with relationships", "main": "index.js", "author": "Paul Armstrong", diff --git a/examples/test-bundlesize/CHANGELOG.md b/examples/test-bundlesize/CHANGELOG.md index 4a7c4b7e0766..a8224d7b3d1b 100644 --- a/examples/test-bundlesize/CHANGELOG.md +++ b/examples/test-bundlesize/CHANGELOG.md @@ -1,5 +1,14 @@ # test-bundlesize +## 0.1.17 + +### Patch Changes + +- Updated dependencies [[`959465a`](https://github.com/reactive/data-client/commit/959465a064db687176e483932987b083f19718eb), [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32), [`6e8e499`](https://github.com/reactive/data-client/commit/6e8e499441741b58ad35127b517e8d83fc7a58fd), [`89e06d3`](https://github.com/reactive/data-client/commit/89e06d38ab839f12f468283a6ff416983f95d65a), [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32)]: + - @data-client/rest@0.18.0 + - @data-client/react@0.18.0 + - @data-client/img@0.18.0 + ## 0.1.16 ### Patch Changes diff --git a/examples/test-bundlesize/package.json b/examples/test-bundlesize/package.json index 97c2cb038299..87f87f37848d 100644 --- a/examples/test-bundlesize/package.json +++ b/examples/test-bundlesize/package.json @@ -1,6 +1,6 @@ { "name": "test-bundlesize", - "version": "0.1.16", + "version": "0.1.17", "packageManager": "yarn@4.14.1", "description": "Testing Bundled Size", "scripts": { diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 75da9f19d647..9b15d95bec00 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,145 @@ # @data-client/core +## 0.18.0 + +### Minor Changes + +- [#3931](https://github.com/reactive/data-client/pull/3931) [`959465a`](https://github.com/reactive/data-client/commit/959465a064db687176e483932987b083f19718eb) - Allow one `Collection` schema to be used both top-level and nested. + + Before: + + ```ts + const getTodos = new Collection([Todo], { argsKey }); + const userTodos = new Collection([Todo], { nestKey }); + ``` + + After: + + ```ts + const userTodos = new Collection([Todo], { argsKey, nestKey }); + ``` + +- [#3887](https://github.com/reactive/data-client/pull/3887) [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32) - **BREAKING**: `Schema.denormalize()` is now `(input, delegate)` instead + of the previous `(input, args, unvisit)` 3-parameter signature. + + ```ts + // before + denormalize(input, args, unvisit) { + return unvisit(this.schema, input); + } + + // after + denormalize(input, delegate) { + return delegate.unvisit(this.schema, input); + } + ``` + + The new [`IDenormalizeDelegate`](https://dataclient.io/rest/api/CustomSchema) + exposes `unvisit`, `args`, and a new `argsKey(fn)` helper that registers + a memoization dimension when output varies with endpoint args. Reading + `delegate.args` directly does _not_ contribute to cache invalidation — + schemas that branch on args must call `argsKey`. The `fn` reference + doubles as the cache path key, so it must be **referentially stable** + — define it on the instance or at module scope, not inline per call: + + ```ts + class LensSchema { + constructor({ lens }) { + this.lensSelector = lens; // stable reference across calls + } + denormalize(input, delegate) { + const portfolio = delegate.argsKey(this.lensSelector); + return this.lookup(input, portfolio); + } + } + ``` + + All built-in schemas (`Array`, `Object`, `Values`, `Union`, `Query`, + `Invalidate`, `Lazy`, `Collection`) have been updated. Custom schemas + implementing `SchemaSimple` must update their `denormalize` signature. + + `Schema.normalize()` and the `visit()` callback also gain an optional + trailing `parentEntity` argument tracking the nearest enclosing + entity-like schema. This is additive — existing schemas don't need + changes unless they want to use it. + +- [#3887](https://github.com/reactive/data-client/pull/3887) [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32) - Add [Scalar](https://dataclient.io/rest/api/Scalar) schema for lens-dependent entity fields. + + `Scalar` models entity fields whose values vary by a runtime "lens" (such as the + selected portfolio, currency, or locale). Multiple components can render the + same entity through different lenses simultaneously — each sees the correct + values without the entity itself ever being mutated. Lens-dependent values are + stored in a separate cell table and joined at denormalize time from endpoint + args. + + New exports: `Scalar`, `schema.Scalar`. + + A single `Scalar` instance can serve both as an `Entity.schema` field (parent + entity inferred from the visit) and standalone — inside `Values(Scalar)`, + `[Scalar]`, or `Collection([Scalar])` — for cheap column-only refreshes + (entity bound explicitly via `entity`). Cell pks are derived from the map key + or via `Scalar.entityPk()`, which defaults to `Entity.pk()` so custom and + composite primary keys work with no override: + + ```ts + import { Collection, Entity, RestEndpoint, Scalar } from '@data-client/rest'; + + class Company extends Entity { + id = ''; + price = 0; + pct_equity = 0; + shares = 0; + } + const PortfolioScalar = new Scalar({ + lens: args => args[0]?.portfolio, + key: 'portfolio', + entity: Company, + }); + Company.schema = { + pct_equity: PortfolioScalar, + shares: PortfolioScalar, + }; + + // Full load — Company rows + scalar cells for the current portfolio + export const getCompanies = new RestEndpoint({ + path: '/companies', + searchParams: {} as { portfolio: string }, + schema: new Collection([Company], { argsKey: () => ({}) }), + }); + // Lens-only refresh — writes to the same Scalar(portfolio) cell table + export const getPortfolioColumns = new RestEndpoint({ + path: '/companies/columns', + searchParams: {} as { portfolio: string }, + schema: new Collection([PortfolioScalar], { + argsKey: ({ portfolio }) => ({ portfolio }), + }), + }); + ``` + + `useSuspense(getCompanies, { portfolio: 'A' })` and + `useSuspense(getCompanies, { portfolio: 'B' })` resolve to different + `pct_equity` / `shares` while sharing the same `Company` row. + + `Scalar.queryKey` enumerates cells in its table for the current lens, so + endpoints that use `Scalar` directly as their top-level schema reconstruct + from cache without a network round-trip once the cells are present. + +### Patch Changes + +- [#3925](https://github.com/reactive/data-client/pull/3925) [`6e8e499`](https://github.com/reactive/data-client/commit/6e8e499441741b58ad35127b517e8d83fc7a58fd) - Fix cached journey being mutated on repeated result-cache hits. + + `GlobalCache.getResults` called `paths.shift()` on a cache hit, mutating + the `journey` array stored by reference on the `WeakDependencyMap` `Link` + node. After the first hit stripped the placeholder input slot, every + subsequent hit on the same cached entry would shift off a real + `EntityPath`, progressively losing subscription entries. This could cause + missed `countRef` tracking (premature GC of still-referenced entities) + and incorrect `entityExpiresAt` calculations. The hit path now returns a + non-mutating copy. + +- Updated dependencies [[`959465a`](https://github.com/reactive/data-client/commit/959465a064db687176e483932987b083f19718eb), [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32), [`6e8e499`](https://github.com/reactive/data-client/commit/6e8e499441741b58ad35127b517e8d83fc7a58fd), [`396d163`](https://github.com/reactive/data-client/commit/396d163e6f4991818519ec33d903a85437483dfd), [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32)]: + - @data-client/normalizr@0.18.0 + ## 0.16.7 ### Patch Changes diff --git a/packages/core/package.json b/packages/core/package.json index 23be1c588614..316e64b6ae50 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@data-client/core", - "version": "0.16.7", + "version": "0.18.0", "description": "Async State Management without the Management. REST, GraphQL, SSE, Websockets, Fetch", "sideEffects": false, "main": "dist/index.js", diff --git a/packages/endpoint/CHANGELOG.md b/packages/endpoint/CHANGELOG.md index 0204cfdbf775..3cf166114204 100644 --- a/packages/endpoint/CHANGELOG.md +++ b/packages/endpoint/CHANGELOG.md @@ -1,5 +1,173 @@ # @data-client/endpoint +## 0.18.0 + +### Minor Changes + +- [#3931](https://github.com/reactive/data-client/pull/3931) [`959465a`](https://github.com/reactive/data-client/commit/959465a064db687176e483932987b083f19718eb) - Allow one `Collection` schema to be used both top-level and nested. + + Before: + + ```ts + const getTodos = new Collection([Todo], { argsKey }); + const userTodos = new Collection([Todo], { nestKey }); + ``` + + After: + + ```ts + const userTodos = new Collection([Todo], { argsKey, nestKey }); + ``` + +- [#3887](https://github.com/reactive/data-client/pull/3887) [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32) - **BREAKING**: `Schema.denormalize()` is now `(input, delegate)` instead + of the previous `(input, args, unvisit)` 3-parameter signature. + + ```ts + // before + denormalize(input, args, unvisit) { + return unvisit(this.schema, input); + } + + // after + denormalize(input, delegate) { + return delegate.unvisit(this.schema, input); + } + ``` + + The new [`IDenormalizeDelegate`](https://dataclient.io/rest/api/CustomSchema) + exposes `unvisit`, `args`, and a new `argsKey(fn)` helper that registers + a memoization dimension when output varies with endpoint args. Reading + `delegate.args` directly does _not_ contribute to cache invalidation — + schemas that branch on args must call `argsKey`. The `fn` reference + doubles as the cache path key, so it must be **referentially stable** + — define it on the instance or at module scope, not inline per call: + + ```ts + class LensSchema { + constructor({ lens }) { + this.lensSelector = lens; // stable reference across calls + } + denormalize(input, delegate) { + const portfolio = delegate.argsKey(this.lensSelector); + return this.lookup(input, portfolio); + } + } + ``` + + All built-in schemas (`Array`, `Object`, `Values`, `Union`, `Query`, + `Invalidate`, `Lazy`, `Collection`) have been updated. Custom schemas + implementing `SchemaSimple` must update their `denormalize` signature. + + `Schema.normalize()` and the `visit()` callback also gain an optional + trailing `parentEntity` argument tracking the nearest enclosing + entity-like schema. This is additive — existing schemas don't need + changes unless they want to use it. + +- [#3934](https://github.com/reactive/data-client/pull/3934) [`396d163`](https://github.com/reactive/data-client/commit/396d163e6f4991818519ec33d903a85437483dfd) - Move normalize `args` and recursive `visit` into the existing normalize delegate passed to schemas. + Custom `Schema.normalize()` implementations should migrate from + `normalize(input, parent, key, args, visit, delegate, parentEntity?)` to + `normalize(input, parent, key, delegate, parentEntity?)`, then read + `delegate.args` and call `delegate.visit()` for recursive normalization. + + Before: + + ```ts + class WrapperSchema { + normalize(input, parent, key, args, visit, delegate) { + const normalized = visit(this.schema, input.value, input, 'value', args); + delegate.mergeEntity(this, this.pk(input, parent, key, args), normalized); + return normalized; + } + } + ``` + + After: + + ```ts + class WrapperSchema { + normalize(input, parent, key, delegate) { + const { args, visit } = delegate; + const normalized = visit(this.schema, input.value, input, 'value'); + delegate.mergeEntity(this, this.pk(input, parent, key, args), normalized); + return normalized; + } + } + ``` + +- [#3887](https://github.com/reactive/data-client/pull/3887) [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32) - Add [Scalar](https://dataclient.io/rest/api/Scalar) schema for lens-dependent entity fields. + + `Scalar` models entity fields whose values vary by a runtime "lens" (such as the + selected portfolio, currency, or locale). Multiple components can render the + same entity through different lenses simultaneously — each sees the correct + values without the entity itself ever being mutated. Lens-dependent values are + stored in a separate cell table and joined at denormalize time from endpoint + args. + + New exports: `Scalar`, `schema.Scalar`. + + A single `Scalar` instance can serve both as an `Entity.schema` field (parent + entity inferred from the visit) and standalone — inside `Values(Scalar)`, + `[Scalar]`, or `Collection([Scalar])` — for cheap column-only refreshes + (entity bound explicitly via `entity`). Cell pks are derived from the map key + or via `Scalar.entityPk()`, which defaults to `Entity.pk()` so custom and + composite primary keys work with no override: + + ```ts + import { Collection, Entity, RestEndpoint, Scalar } from '@data-client/rest'; + + class Company extends Entity { + id = ''; + price = 0; + pct_equity = 0; + shares = 0; + } + const PortfolioScalar = new Scalar({ + lens: args => args[0]?.portfolio, + key: 'portfolio', + entity: Company, + }); + Company.schema = { + pct_equity: PortfolioScalar, + shares: PortfolioScalar, + }; + + // Full load — Company rows + scalar cells for the current portfolio + export const getCompanies = new RestEndpoint({ + path: '/companies', + searchParams: {} as { portfolio: string }, + schema: new Collection([Company], { argsKey: () => ({}) }), + }); + // Lens-only refresh — writes to the same Scalar(portfolio) cell table + export const getPortfolioColumns = new RestEndpoint({ + path: '/companies/columns', + searchParams: {} as { portfolio: string }, + schema: new Collection([PortfolioScalar], { + argsKey: ({ portfolio }) => ({ portfolio }), + }), + }); + ``` + + `useSuspense(getCompanies, { portfolio: 'A' })` and + `useSuspense(getCompanies, { portfolio: 'B' })` resolve to different + `pct_equity` / `shares` while sharing the same `Company` row. + + `Scalar.queryKey` enumerates cells in its table for the current lens, so + endpoints that use `Scalar` directly as their top-level schema reconstruct + from cache without a network round-trip once the cells are present. + +### Patch Changes + +- [#3925](https://github.com/reactive/data-client/pull/3925) [`6e8e499`](https://github.com/reactive/data-client/commit/6e8e499441741b58ad35127b517e8d83fc7a58fd) - Fix cached journey being mutated on repeated result-cache hits. + + `GlobalCache.getResults` called `paths.shift()` on a cache hit, mutating + the `journey` array stored by reference on the `WeakDependencyMap` `Link` + node. After the first hit stripped the placeholder input slot, every + subsequent hit on the same cached entry would shift off a real + `EntityPath`, progressively losing subscription entries. This could cause + missed `countRef` tracking (premature GC of still-referenced entities) + and incorrect `entityExpiresAt` calculations. The hit path now returns a + non-mutating copy. + ## 0.16.6 ### Patch Changes diff --git a/packages/endpoint/package.json b/packages/endpoint/package.json index 3f9aba0dc0b8..3bef435d8eb4 100644 --- a/packages/endpoint/package.json +++ b/packages/endpoint/package.json @@ -1,6 +1,6 @@ { "name": "@data-client/endpoint", - "version": "0.16.6", + "version": "0.18.0", "description": "Declarative Network Interface Definitions", "homepage": "https://dataclient.io/docs/guides/custom-protocol", "keywords": [ diff --git a/packages/graphql/CHANGELOG.md b/packages/graphql/CHANGELOG.md index 3ed4df2ef4c1..dde8c06b50cd 100644 --- a/packages/graphql/CHANGELOG.md +++ b/packages/graphql/CHANGELOG.md @@ -1,5 +1,145 @@ # @data-client/graphql +## 0.18.0 + +### Minor Changes + +- [#3931](https://github.com/reactive/data-client/pull/3931) [`959465a`](https://github.com/reactive/data-client/commit/959465a064db687176e483932987b083f19718eb) - Allow one `Collection` schema to be used both top-level and nested. + + Before: + + ```ts + const getTodos = new Collection([Todo], { argsKey }); + const userTodos = new Collection([Todo], { nestKey }); + ``` + + After: + + ```ts + const userTodos = new Collection([Todo], { argsKey, nestKey }); + ``` + +- [#3887](https://github.com/reactive/data-client/pull/3887) [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32) - **BREAKING**: `Schema.denormalize()` is now `(input, delegate)` instead + of the previous `(input, args, unvisit)` 3-parameter signature. + + ```ts + // before + denormalize(input, args, unvisit) { + return unvisit(this.schema, input); + } + + // after + denormalize(input, delegate) { + return delegate.unvisit(this.schema, input); + } + ``` + + The new [`IDenormalizeDelegate`](https://dataclient.io/rest/api/CustomSchema) + exposes `unvisit`, `args`, and a new `argsKey(fn)` helper that registers + a memoization dimension when output varies with endpoint args. Reading + `delegate.args` directly does _not_ contribute to cache invalidation — + schemas that branch on args must call `argsKey`. The `fn` reference + doubles as the cache path key, so it must be **referentially stable** + — define it on the instance or at module scope, not inline per call: + + ```ts + class LensSchema { + constructor({ lens }) { + this.lensSelector = lens; // stable reference across calls + } + denormalize(input, delegate) { + const portfolio = delegate.argsKey(this.lensSelector); + return this.lookup(input, portfolio); + } + } + ``` + + All built-in schemas (`Array`, `Object`, `Values`, `Union`, `Query`, + `Invalidate`, `Lazy`, `Collection`) have been updated. Custom schemas + implementing `SchemaSimple` must update their `denormalize` signature. + + `Schema.normalize()` and the `visit()` callback also gain an optional + trailing `parentEntity` argument tracking the nearest enclosing + entity-like schema. This is additive — existing schemas don't need + changes unless they want to use it. + +- [#3887](https://github.com/reactive/data-client/pull/3887) [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32) - Add [Scalar](https://dataclient.io/rest/api/Scalar) schema for lens-dependent entity fields. + + `Scalar` models entity fields whose values vary by a runtime "lens" (such as the + selected portfolio, currency, or locale). Multiple components can render the + same entity through different lenses simultaneously — each sees the correct + values without the entity itself ever being mutated. Lens-dependent values are + stored in a separate cell table and joined at denormalize time from endpoint + args. + + New exports: `Scalar`, `schema.Scalar`. + + A single `Scalar` instance can serve both as an `Entity.schema` field (parent + entity inferred from the visit) and standalone — inside `Values(Scalar)`, + `[Scalar]`, or `Collection([Scalar])` — for cheap column-only refreshes + (entity bound explicitly via `entity`). Cell pks are derived from the map key + or via `Scalar.entityPk()`, which defaults to `Entity.pk()` so custom and + composite primary keys work with no override: + + ```ts + import { Collection, Entity, RestEndpoint, Scalar } from '@data-client/rest'; + + class Company extends Entity { + id = ''; + price = 0; + pct_equity = 0; + shares = 0; + } + const PortfolioScalar = new Scalar({ + lens: args => args[0]?.portfolio, + key: 'portfolio', + entity: Company, + }); + Company.schema = { + pct_equity: PortfolioScalar, + shares: PortfolioScalar, + }; + + // Full load — Company rows + scalar cells for the current portfolio + export const getCompanies = new RestEndpoint({ + path: '/companies', + searchParams: {} as { portfolio: string }, + schema: new Collection([Company], { argsKey: () => ({}) }), + }); + // Lens-only refresh — writes to the same Scalar(portfolio) cell table + export const getPortfolioColumns = new RestEndpoint({ + path: '/companies/columns', + searchParams: {} as { portfolio: string }, + schema: new Collection([PortfolioScalar], { + argsKey: ({ portfolio }) => ({ portfolio }), + }), + }); + ``` + + `useSuspense(getCompanies, { portfolio: 'A' })` and + `useSuspense(getCompanies, { portfolio: 'B' })` resolve to different + `pct_equity` / `shares` while sharing the same `Company` row. + + `Scalar.queryKey` enumerates cells in its table for the current lens, so + endpoints that use `Scalar` directly as their top-level schema reconstruct + from cache without a network round-trip once the cells are present. + +### Patch Changes + +- [#3925](https://github.com/reactive/data-client/pull/3925) [`6e8e499`](https://github.com/reactive/data-client/commit/6e8e499441741b58ad35127b517e8d83fc7a58fd) - Fix cached journey being mutated on repeated result-cache hits. + + `GlobalCache.getResults` called `paths.shift()` on a cache hit, mutating + the `journey` array stored by reference on the `WeakDependencyMap` `Link` + node. After the first hit stripped the placeholder input slot, every + subsequent hit on the same cached entry would shift off a real + `EntityPath`, progressively losing subscription entries. This could cause + missed `countRef` tracking (premature GC of still-referenced entities) + and incorrect `entityExpiresAt` calculations. The hit path now returns a + non-mutating copy. + +- Updated dependencies [[`959465a`](https://github.com/reactive/data-client/commit/959465a064db687176e483932987b083f19718eb), [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32), [`6e8e499`](https://github.com/reactive/data-client/commit/6e8e499441741b58ad35127b517e8d83fc7a58fd), [`396d163`](https://github.com/reactive/data-client/commit/396d163e6f4991818519ec33d903a85437483dfd), [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32)]: + - @data-client/endpoint@0.18.0 + ## 0.16.3 ### Patch Changes diff --git a/packages/graphql/package.json b/packages/graphql/package.json index b3ff9b81422c..42ea30f4a8f8 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -1,6 +1,6 @@ { "name": "@data-client/graphql", - "version": "0.16.3", + "version": "0.18.0", "description": "Quickly define typed GraphQL resources and endpoints", "homepage": "https://dataclient.io/docs/graphql", "repository": { diff --git a/packages/img/CHANGELOG.md b/packages/img/CHANGELOG.md index 1efac5b45fc6..6d2724a68cb3 100644 --- a/packages/img/CHANGELOG.md +++ b/packages/img/CHANGELOG.md @@ -1,5 +1,14 @@ # @data-client/img +## 0.18.0 + +### Patch Changes + +- [`89e06d3`](https://github.com/reactive/data-client/commit/89e06d38ab839f12f468283a6ff416983f95d65a) - Bump `@data-client/react` peer dependency range to include `^0.18.0`. + +- Updated dependencies [[`959465a`](https://github.com/reactive/data-client/commit/959465a064db687176e483932987b083f19718eb), [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32), [`6e8e499`](https://github.com/reactive/data-client/commit/6e8e499441741b58ad35127b517e8d83fc7a58fd), [`396d163`](https://github.com/reactive/data-client/commit/396d163e6f4991818519ec33d903a85437483dfd), [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32)]: + - @data-client/endpoint@0.18.0 + ## 0.16.0 ### Patch Changes diff --git a/packages/img/package.json b/packages/img/package.json index 964354f747ab..2ebfb295eb2e 100644 --- a/packages/img/package.json +++ b/packages/img/package.json @@ -1,6 +1,6 @@ { "name": "@data-client/img", - "version": "0.16.0", + "version": "0.18.0", "description": "Suspenseful images", "homepage": "https://dataclient.io/docs/guides/img-media#just-images", "repository": { diff --git a/packages/normalizr/CHANGELOG.md b/packages/normalizr/CHANGELOG.md index 3eaccbd9d964..95d7efbf644f 100644 --- a/packages/normalizr/CHANGELOG.md +++ b/packages/normalizr/CHANGELOG.md @@ -1,5 +1,173 @@ # Change Log +## 0.18.0 + +### Minor Changes + +- [#3931](https://github.com/reactive/data-client/pull/3931) [`959465a`](https://github.com/reactive/data-client/commit/959465a064db687176e483932987b083f19718eb) - Allow one `Collection` schema to be used both top-level and nested. + + Before: + + ```ts + const getTodos = new Collection([Todo], { argsKey }); + const userTodos = new Collection([Todo], { nestKey }); + ``` + + After: + + ```ts + const userTodos = new Collection([Todo], { argsKey, nestKey }); + ``` + +- [#3887](https://github.com/reactive/data-client/pull/3887) [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32) - **BREAKING**: `Schema.denormalize()` is now `(input, delegate)` instead + of the previous `(input, args, unvisit)` 3-parameter signature. + + ```ts + // before + denormalize(input, args, unvisit) { + return unvisit(this.schema, input); + } + + // after + denormalize(input, delegate) { + return delegate.unvisit(this.schema, input); + } + ``` + + The new [`IDenormalizeDelegate`](https://dataclient.io/rest/api/CustomSchema) + exposes `unvisit`, `args`, and a new `argsKey(fn)` helper that registers + a memoization dimension when output varies with endpoint args. Reading + `delegate.args` directly does _not_ contribute to cache invalidation — + schemas that branch on args must call `argsKey`. The `fn` reference + doubles as the cache path key, so it must be **referentially stable** + — define it on the instance or at module scope, not inline per call: + + ```ts + class LensSchema { + constructor({ lens }) { + this.lensSelector = lens; // stable reference across calls + } + denormalize(input, delegate) { + const portfolio = delegate.argsKey(this.lensSelector); + return this.lookup(input, portfolio); + } + } + ``` + + All built-in schemas (`Array`, `Object`, `Values`, `Union`, `Query`, + `Invalidate`, `Lazy`, `Collection`) have been updated. Custom schemas + implementing `SchemaSimple` must update their `denormalize` signature. + + `Schema.normalize()` and the `visit()` callback also gain an optional + trailing `parentEntity` argument tracking the nearest enclosing + entity-like schema. This is additive — existing schemas don't need + changes unless they want to use it. + +- [#3934](https://github.com/reactive/data-client/pull/3934) [`396d163`](https://github.com/reactive/data-client/commit/396d163e6f4991818519ec33d903a85437483dfd) - Move normalize `args` and recursive `visit` into the existing normalize delegate passed to schemas. + Custom `Schema.normalize()` implementations should migrate from + `normalize(input, parent, key, args, visit, delegate, parentEntity?)` to + `normalize(input, parent, key, delegate, parentEntity?)`, then read + `delegate.args` and call `delegate.visit()` for recursive normalization. + + Before: + + ```ts + class WrapperSchema { + normalize(input, parent, key, args, visit, delegate) { + const normalized = visit(this.schema, input.value, input, 'value', args); + delegate.mergeEntity(this, this.pk(input, parent, key, args), normalized); + return normalized; + } + } + ``` + + After: + + ```ts + class WrapperSchema { + normalize(input, parent, key, delegate) { + const { args, visit } = delegate; + const normalized = visit(this.schema, input.value, input, 'value'); + delegate.mergeEntity(this, this.pk(input, parent, key, args), normalized); + return normalized; + } + } + ``` + +- [#3887](https://github.com/reactive/data-client/pull/3887) [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32) - Add [Scalar](https://dataclient.io/rest/api/Scalar) schema for lens-dependent entity fields. + + `Scalar` models entity fields whose values vary by a runtime "lens" (such as the + selected portfolio, currency, or locale). Multiple components can render the + same entity through different lenses simultaneously — each sees the correct + values without the entity itself ever being mutated. Lens-dependent values are + stored in a separate cell table and joined at denormalize time from endpoint + args. + + New exports: `Scalar`, `schema.Scalar`. + + A single `Scalar` instance can serve both as an `Entity.schema` field (parent + entity inferred from the visit) and standalone — inside `Values(Scalar)`, + `[Scalar]`, or `Collection([Scalar])` — for cheap column-only refreshes + (entity bound explicitly via `entity`). Cell pks are derived from the map key + or via `Scalar.entityPk()`, which defaults to `Entity.pk()` so custom and + composite primary keys work with no override: + + ```ts + import { Collection, Entity, RestEndpoint, Scalar } from '@data-client/rest'; + + class Company extends Entity { + id = ''; + price = 0; + pct_equity = 0; + shares = 0; + } + const PortfolioScalar = new Scalar({ + lens: args => args[0]?.portfolio, + key: 'portfolio', + entity: Company, + }); + Company.schema = { + pct_equity: PortfolioScalar, + shares: PortfolioScalar, + }; + + // Full load — Company rows + scalar cells for the current portfolio + export const getCompanies = new RestEndpoint({ + path: '/companies', + searchParams: {} as { portfolio: string }, + schema: new Collection([Company], { argsKey: () => ({}) }), + }); + // Lens-only refresh — writes to the same Scalar(portfolio) cell table + export const getPortfolioColumns = new RestEndpoint({ + path: '/companies/columns', + searchParams: {} as { portfolio: string }, + schema: new Collection([PortfolioScalar], { + argsKey: ({ portfolio }) => ({ portfolio }), + }), + }); + ``` + + `useSuspense(getCompanies, { portfolio: 'A' })` and + `useSuspense(getCompanies, { portfolio: 'B' })` resolve to different + `pct_equity` / `shares` while sharing the same `Company` row. + + `Scalar.queryKey` enumerates cells in its table for the current lens, so + endpoints that use `Scalar` directly as their top-level schema reconstruct + from cache without a network round-trip once the cells are present. + +### Patch Changes + +- [#3925](https://github.com/reactive/data-client/pull/3925) [`6e8e499`](https://github.com/reactive/data-client/commit/6e8e499441741b58ad35127b517e8d83fc7a58fd) - Fix cached journey being mutated on repeated result-cache hits. + + `GlobalCache.getResults` called `paths.shift()` on a cache hit, mutating + the `journey` array stored by reference on the `WeakDependencyMap` `Link` + node. After the first hit stripped the placeholder input slot, every + subsequent hit on the same cached entry would shift off a real + `EntityPath`, progressively losing subscription entries. This could cause + missed `countRef` tracking (premature GC of still-referenced entities) + and incorrect `entityExpiresAt` calculations. The hit path now returns a + non-mutating copy. + ## 0.16.6 ### Patch Changes diff --git a/packages/normalizr/package.json b/packages/normalizr/package.json index 1b49c335ca4e..eee54b462685 100644 --- a/packages/normalizr/package.json +++ b/packages/normalizr/package.json @@ -1,6 +1,6 @@ { "name": "@data-client/normalizr", - "version": "0.16.6", + "version": "0.18.0", "description": "Normalizes and denormalizes JSON according to schema for Redux and Flux applications", "homepage": "https://dataclient.io/docs/concepts/normalization", "keywords": [ diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index a1b44d3561f6..de14fc6fe186 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -1,5 +1,145 @@ # @data-client/react +## 0.18.0 + +### Minor Changes + +- [#3931](https://github.com/reactive/data-client/pull/3931) [`959465a`](https://github.com/reactive/data-client/commit/959465a064db687176e483932987b083f19718eb) - Allow one `Collection` schema to be used both top-level and nested. + + Before: + + ```ts + const getTodos = new Collection([Todo], { argsKey }); + const userTodos = new Collection([Todo], { nestKey }); + ``` + + After: + + ```ts + const userTodos = new Collection([Todo], { argsKey, nestKey }); + ``` + +- [#3887](https://github.com/reactive/data-client/pull/3887) [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32) - **BREAKING**: `Schema.denormalize()` is now `(input, delegate)` instead + of the previous `(input, args, unvisit)` 3-parameter signature. + + ```ts + // before + denormalize(input, args, unvisit) { + return unvisit(this.schema, input); + } + + // after + denormalize(input, delegate) { + return delegate.unvisit(this.schema, input); + } + ``` + + The new [`IDenormalizeDelegate`](https://dataclient.io/rest/api/CustomSchema) + exposes `unvisit`, `args`, and a new `argsKey(fn)` helper that registers + a memoization dimension when output varies with endpoint args. Reading + `delegate.args` directly does _not_ contribute to cache invalidation — + schemas that branch on args must call `argsKey`. The `fn` reference + doubles as the cache path key, so it must be **referentially stable** + — define it on the instance or at module scope, not inline per call: + + ```ts + class LensSchema { + constructor({ lens }) { + this.lensSelector = lens; // stable reference across calls + } + denormalize(input, delegate) { + const portfolio = delegate.argsKey(this.lensSelector); + return this.lookup(input, portfolio); + } + } + ``` + + All built-in schemas (`Array`, `Object`, `Values`, `Union`, `Query`, + `Invalidate`, `Lazy`, `Collection`) have been updated. Custom schemas + implementing `SchemaSimple` must update their `denormalize` signature. + + `Schema.normalize()` and the `visit()` callback also gain an optional + trailing `parentEntity` argument tracking the nearest enclosing + entity-like schema. This is additive — existing schemas don't need + changes unless they want to use it. + +- [#3887](https://github.com/reactive/data-client/pull/3887) [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32) - Add [Scalar](https://dataclient.io/rest/api/Scalar) schema for lens-dependent entity fields. + + `Scalar` models entity fields whose values vary by a runtime "lens" (such as the + selected portfolio, currency, or locale). Multiple components can render the + same entity through different lenses simultaneously — each sees the correct + values without the entity itself ever being mutated. Lens-dependent values are + stored in a separate cell table and joined at denormalize time from endpoint + args. + + New exports: `Scalar`, `schema.Scalar`. + + A single `Scalar` instance can serve both as an `Entity.schema` field (parent + entity inferred from the visit) and standalone — inside `Values(Scalar)`, + `[Scalar]`, or `Collection([Scalar])` — for cheap column-only refreshes + (entity bound explicitly via `entity`). Cell pks are derived from the map key + or via `Scalar.entityPk()`, which defaults to `Entity.pk()` so custom and + composite primary keys work with no override: + + ```ts + import { Collection, Entity, RestEndpoint, Scalar } from '@data-client/rest'; + + class Company extends Entity { + id = ''; + price = 0; + pct_equity = 0; + shares = 0; + } + const PortfolioScalar = new Scalar({ + lens: args => args[0]?.portfolio, + key: 'portfolio', + entity: Company, + }); + Company.schema = { + pct_equity: PortfolioScalar, + shares: PortfolioScalar, + }; + + // Full load — Company rows + scalar cells for the current portfolio + export const getCompanies = new RestEndpoint({ + path: '/companies', + searchParams: {} as { portfolio: string }, + schema: new Collection([Company], { argsKey: () => ({}) }), + }); + // Lens-only refresh — writes to the same Scalar(portfolio) cell table + export const getPortfolioColumns = new RestEndpoint({ + path: '/companies/columns', + searchParams: {} as { portfolio: string }, + schema: new Collection([PortfolioScalar], { + argsKey: ({ portfolio }) => ({ portfolio }), + }), + }); + ``` + + `useSuspense(getCompanies, { portfolio: 'A' })` and + `useSuspense(getCompanies, { portfolio: 'B' })` resolve to different + `pct_equity` / `shares` while sharing the same `Company` row. + + `Scalar.queryKey` enumerates cells in its table for the current lens, so + endpoints that use `Scalar` directly as their top-level schema reconstruct + from cache without a network round-trip once the cells are present. + +### Patch Changes + +- [#3925](https://github.com/reactive/data-client/pull/3925) [`6e8e499`](https://github.com/reactive/data-client/commit/6e8e499441741b58ad35127b517e8d83fc7a58fd) - Fix cached journey being mutated on repeated result-cache hits. + + `GlobalCache.getResults` called `paths.shift()` on a cache hit, mutating + the `journey` array stored by reference on the `WeakDependencyMap` `Link` + node. After the first hit stripped the placeholder input slot, every + subsequent hit on the same cached entry would shift off a real + `EntityPath`, progressively losing subscription entries. This could cause + missed `countRef` tracking (premature GC of still-referenced entities) + and incorrect `entityExpiresAt` calculations. The hit path now returns a + non-mutating copy. + +- Updated dependencies [[`959465a`](https://github.com/reactive/data-client/commit/959465a064db687176e483932987b083f19718eb), [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32), [`6e8e499`](https://github.com/reactive/data-client/commit/6e8e499441741b58ad35127b517e8d83fc7a58fd), [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32)]: + - @data-client/core@0.18.0 + ## 0.16.7 ### Patch Changes diff --git a/packages/react/package.json b/packages/react/package.json index 190bfe2346ff..8447bc152708 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@data-client/react", - "version": "0.16.7", + "version": "0.18.0", "description": "Async State Management without the Management. REST, GraphQL, SSE, Websockets, Fetch", "homepage": "https://dataclient.io", "repository": { diff --git a/packages/rest/CHANGELOG.md b/packages/rest/CHANGELOG.md index b52d0dc3b3a7..eec0f7fdaad9 100644 --- a/packages/rest/CHANGELOG.md +++ b/packages/rest/CHANGELOG.md @@ -1,5 +1,145 @@ # @data-client/rest +## 0.18.0 + +### Minor Changes + +- [#3931](https://github.com/reactive/data-client/pull/3931) [`959465a`](https://github.com/reactive/data-client/commit/959465a064db687176e483932987b083f19718eb) - Allow one `Collection` schema to be used both top-level and nested. + + Before: + + ```ts + const getTodos = new Collection([Todo], { argsKey }); + const userTodos = new Collection([Todo], { nestKey }); + ``` + + After: + + ```ts + const userTodos = new Collection([Todo], { argsKey, nestKey }); + ``` + +- [#3887](https://github.com/reactive/data-client/pull/3887) [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32) - **BREAKING**: `Schema.denormalize()` is now `(input, delegate)` instead + of the previous `(input, args, unvisit)` 3-parameter signature. + + ```ts + // before + denormalize(input, args, unvisit) { + return unvisit(this.schema, input); + } + + // after + denormalize(input, delegate) { + return delegate.unvisit(this.schema, input); + } + ``` + + The new [`IDenormalizeDelegate`](https://dataclient.io/rest/api/CustomSchema) + exposes `unvisit`, `args`, and a new `argsKey(fn)` helper that registers + a memoization dimension when output varies with endpoint args. Reading + `delegate.args` directly does _not_ contribute to cache invalidation — + schemas that branch on args must call `argsKey`. The `fn` reference + doubles as the cache path key, so it must be **referentially stable** + — define it on the instance or at module scope, not inline per call: + + ```ts + class LensSchema { + constructor({ lens }) { + this.lensSelector = lens; // stable reference across calls + } + denormalize(input, delegate) { + const portfolio = delegate.argsKey(this.lensSelector); + return this.lookup(input, portfolio); + } + } + ``` + + All built-in schemas (`Array`, `Object`, `Values`, `Union`, `Query`, + `Invalidate`, `Lazy`, `Collection`) have been updated. Custom schemas + implementing `SchemaSimple` must update their `denormalize` signature. + + `Schema.normalize()` and the `visit()` callback also gain an optional + trailing `parentEntity` argument tracking the nearest enclosing + entity-like schema. This is additive — existing schemas don't need + changes unless they want to use it. + +- [#3887](https://github.com/reactive/data-client/pull/3887) [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32) - Add [Scalar](https://dataclient.io/rest/api/Scalar) schema for lens-dependent entity fields. + + `Scalar` models entity fields whose values vary by a runtime "lens" (such as the + selected portfolio, currency, or locale). Multiple components can render the + same entity through different lenses simultaneously — each sees the correct + values without the entity itself ever being mutated. Lens-dependent values are + stored in a separate cell table and joined at denormalize time from endpoint + args. + + New exports: `Scalar`, `schema.Scalar`. + + A single `Scalar` instance can serve both as an `Entity.schema` field (parent + entity inferred from the visit) and standalone — inside `Values(Scalar)`, + `[Scalar]`, or `Collection([Scalar])` — for cheap column-only refreshes + (entity bound explicitly via `entity`). Cell pks are derived from the map key + or via `Scalar.entityPk()`, which defaults to `Entity.pk()` so custom and + composite primary keys work with no override: + + ```ts + import { Collection, Entity, RestEndpoint, Scalar } from '@data-client/rest'; + + class Company extends Entity { + id = ''; + price = 0; + pct_equity = 0; + shares = 0; + } + const PortfolioScalar = new Scalar({ + lens: args => args[0]?.portfolio, + key: 'portfolio', + entity: Company, + }); + Company.schema = { + pct_equity: PortfolioScalar, + shares: PortfolioScalar, + }; + + // Full load — Company rows + scalar cells for the current portfolio + export const getCompanies = new RestEndpoint({ + path: '/companies', + searchParams: {} as { portfolio: string }, + schema: new Collection([Company], { argsKey: () => ({}) }), + }); + // Lens-only refresh — writes to the same Scalar(portfolio) cell table + export const getPortfolioColumns = new RestEndpoint({ + path: '/companies/columns', + searchParams: {} as { portfolio: string }, + schema: new Collection([PortfolioScalar], { + argsKey: ({ portfolio }) => ({ portfolio }), + }), + }); + ``` + + `useSuspense(getCompanies, { portfolio: 'A' })` and + `useSuspense(getCompanies, { portfolio: 'B' })` resolve to different + `pct_equity` / `shares` while sharing the same `Company` row. + + `Scalar.queryKey` enumerates cells in its table for the current lens, so + endpoints that use `Scalar` directly as their top-level schema reconstruct + from cache without a network round-trip once the cells are present. + +### Patch Changes + +- [#3925](https://github.com/reactive/data-client/pull/3925) [`6e8e499`](https://github.com/reactive/data-client/commit/6e8e499441741b58ad35127b517e8d83fc7a58fd) - Fix cached journey being mutated on repeated result-cache hits. + + `GlobalCache.getResults` called `paths.shift()` on a cache hit, mutating + the `journey` array stored by reference on the `WeakDependencyMap` `Link` + node. After the first hit stripped the placeholder input slot, every + subsequent hit on the same cached entry would shift off a real + `EntityPath`, progressively losing subscription entries. This could cause + missed `countRef` tracking (premature GC of still-referenced entities) + and incorrect `entityExpiresAt` calculations. The hit path now returns a + non-mutating copy. + +- Updated dependencies [[`959465a`](https://github.com/reactive/data-client/commit/959465a064db687176e483932987b083f19718eb), [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32), [`6e8e499`](https://github.com/reactive/data-client/commit/6e8e499441741b58ad35127b517e8d83fc7a58fd), [`396d163`](https://github.com/reactive/data-client/commit/396d163e6f4991818519ec33d903a85437483dfd), [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32)]: + - @data-client/endpoint@0.18.0 + ## 0.17.0 ### Minor Changes diff --git a/packages/rest/package.json b/packages/rest/package.json index 3cb7af5015d1..7baa740ce48e 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -1,6 +1,6 @@ { "name": "@data-client/rest", - "version": "0.17.0", + "version": "0.18.0", "description": "Quickly define typed REST resources and endpoints", "homepage": "https://dataclient.io/rest", "repository": { diff --git a/packages/test/CHANGELOG.md b/packages/test/CHANGELOG.md index 39c6b2af31db..0a4a4d01c311 100644 --- a/packages/test/CHANGELOG.md +++ b/packages/test/CHANGELOG.md @@ -1,5 +1,11 @@ # @data-client/test +## 0.18.0 + +### Patch Changes + +- [`89e06d3`](https://github.com/reactive/data-client/commit/89e06d38ab839f12f468283a6ff416983f95d65a) - Bump `@data-client/react` peer dependency range to include `^0.18.0`. + ## 0.16.0 ### Patch Changes diff --git a/packages/test/package.json b/packages/test/package.json index e739f6c8454d..407f4177fdfa 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -1,6 +1,6 @@ { "name": "@data-client/test", - "version": "0.16.0", + "version": "0.18.0", "description": "Testing utilities for Data Client", "homepage": "https://dataclient.io/docs/guides/storybook", "repository": { diff --git a/packages/vue/CHANGELOG.md b/packages/vue/CHANGELOG.md index c4f4b6ddc68c..2fd8f456c822 100644 --- a/packages/vue/CHANGELOG.md +++ b/packages/vue/CHANGELOG.md @@ -1,5 +1,134 @@ # @data-client/vue +## 0.18.0 + +### Minor Changes + +- [#3931](https://github.com/reactive/data-client/pull/3931) [`959465a`](https://github.com/reactive/data-client/commit/959465a064db687176e483932987b083f19718eb) - Allow one `Collection` schema to be used both top-level and nested. + + Before: + + ```ts + const getTodos = new Collection([Todo], { argsKey }); + const userTodos = new Collection([Todo], { nestKey }); + ``` + + After: + + ```ts + const userTodos = new Collection([Todo], { argsKey, nestKey }); + ``` + +- [#3887](https://github.com/reactive/data-client/pull/3887) [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32) - **BREAKING**: `Schema.denormalize()` is now `(input, delegate)` instead + of the previous `(input, args, unvisit)` 3-parameter signature. + + ```ts + // before + denormalize(input, args, unvisit) { + return unvisit(this.schema, input); + } + + // after + denormalize(input, delegate) { + return delegate.unvisit(this.schema, input); + } + ``` + + The new [`IDenormalizeDelegate`](https://dataclient.io/rest/api/CustomSchema) + exposes `unvisit`, `args`, and a new `argsKey(fn)` helper that registers + a memoization dimension when output varies with endpoint args. Reading + `delegate.args` directly does _not_ contribute to cache invalidation — + schemas that branch on args must call `argsKey`. The `fn` reference + doubles as the cache path key, so it must be **referentially stable** + — define it on the instance or at module scope, not inline per call: + + ```ts + class LensSchema { + constructor({ lens }) { + this.lensSelector = lens; // stable reference across calls + } + denormalize(input, delegate) { + const portfolio = delegate.argsKey(this.lensSelector); + return this.lookup(input, portfolio); + } + } + ``` + + All built-in schemas (`Array`, `Object`, `Values`, `Union`, `Query`, + `Invalidate`, `Lazy`, `Collection`) have been updated. Custom schemas + implementing `SchemaSimple` must update their `denormalize` signature. + + `Schema.normalize()` and the `visit()` callback also gain an optional + trailing `parentEntity` argument tracking the nearest enclosing + entity-like schema. This is additive — existing schemas don't need + changes unless they want to use it. + +- [#3887](https://github.com/reactive/data-client/pull/3887) [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32) - Add [Scalar](https://dataclient.io/rest/api/Scalar) schema for lens-dependent entity fields. + + `Scalar` models entity fields whose values vary by a runtime "lens" (such as the + selected portfolio, currency, or locale). Multiple components can render the + same entity through different lenses simultaneously — each sees the correct + values without the entity itself ever being mutated. Lens-dependent values are + stored in a separate cell table and joined at denormalize time from endpoint + args. + + New exports: `Scalar`, `schema.Scalar`. + + A single `Scalar` instance can serve both as an `Entity.schema` field (parent + entity inferred from the visit) and standalone — inside `Values(Scalar)`, + `[Scalar]`, or `Collection([Scalar])` — for cheap column-only refreshes + (entity bound explicitly via `entity`). Cell pks are derived from the map key + or via `Scalar.entityPk()`, which defaults to `Entity.pk()` so custom and + composite primary keys work with no override: + + ```ts + import { Collection, Entity, RestEndpoint, Scalar } from '@data-client/rest'; + + class Company extends Entity { + id = ''; + price = 0; + pct_equity = 0; + shares = 0; + } + const PortfolioScalar = new Scalar({ + lens: args => args[0]?.portfolio, + key: 'portfolio', + entity: Company, + }); + Company.schema = { + pct_equity: PortfolioScalar, + shares: PortfolioScalar, + }; + + // Full load — Company rows + scalar cells for the current portfolio + export const getCompanies = new RestEndpoint({ + path: '/companies', + searchParams: {} as { portfolio: string }, + schema: new Collection([Company], { argsKey: () => ({}) }), + }); + // Lens-only refresh — writes to the same Scalar(portfolio) cell table + export const getPortfolioColumns = new RestEndpoint({ + path: '/companies/columns', + searchParams: {} as { portfolio: string }, + schema: new Collection([PortfolioScalar], { + argsKey: ({ portfolio }) => ({ portfolio }), + }), + }); + ``` + + `useSuspense(getCompanies, { portfolio: 'A' })` and + `useSuspense(getCompanies, { portfolio: 'B' })` resolve to different + `pct_equity` / `shares` while sharing the same `Company` row. + + `Scalar.queryKey` enumerates cells in its table for the current lens, so + endpoints that use `Scalar` directly as their top-level schema reconstruct + from cache without a network round-trip once the cells are present. + +### Patch Changes + +- Updated dependencies [[`959465a`](https://github.com/reactive/data-client/commit/959465a064db687176e483932987b083f19718eb), [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32), [`6e8e499`](https://github.com/reactive/data-client/commit/6e8e499441741b58ad35127b517e8d83fc7a58fd), [`84078d7`](https://github.com/reactive/data-client/commit/84078d7d36bf5cf0fd16a479ce16c48c5d804f32)]: + - @data-client/core@0.18.0 + ## 0.16.1 ### Patch Changes diff --git a/packages/vue/package.json b/packages/vue/package.json index ec37e40cc425..d816e01e8abc 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "@data-client/vue", - "version": "0.16.1", + "version": "0.18.0", "description": "Async State Management without the Management. REST, GraphQL, SSE, Websockets, Fetch", "homepage": "https://dataclient.io", "repository": { diff --git a/yarn.lock b/yarn.lock index b841a320e10b..d1fa17693fe8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3244,7 +3244,7 @@ __metadata: languageName: unknown linkType: soft -"@data-client/img@npm:0.16.0, @data-client/img@workspace:*, @data-client/img@workspace:packages/img": +"@data-client/img@npm:0.18.0, @data-client/img@workspace:*, @data-client/img@workspace:packages/img": version: 0.0.0-use.local resolution: "@data-client/img@workspace:packages/img" dependencies: @@ -3286,7 +3286,7 @@ __metadata: languageName: unknown linkType: soft -"@data-client/react@npm:0.16.7, @data-client/react@workspace:*, @data-client/react@workspace:packages/react": +"@data-client/react@npm:0.18.0, @data-client/react@workspace:*, @data-client/react@workspace:packages/react": version: 0.0.0-use.local resolution: "@data-client/react@workspace:packages/react" dependencies: @@ -3330,7 +3330,7 @@ __metadata: languageName: unknown linkType: soft -"@data-client/rest@npm:0.17.0, @data-client/rest@workspace:*, @data-client/rest@workspace:packages/rest": +"@data-client/rest@npm:0.18.0, @data-client/rest@workspace:*, @data-client/rest@workspace:packages/rest": version: 0.0.0-use.local resolution: "@data-client/rest@workspace:packages/rest" dependencies: @@ -11046,9 +11046,9 @@ __metadata: "@anansi/webpack-config": "npm:21.1.17" "@babel/core": "npm:7.29.0" "@babel/runtime-corejs3": "npm:7.29.2" - "@data-client/img": "npm:0.16.0" - "@data-client/react": "npm:0.16.7" - "@data-client/rest": "npm:0.17.0" + "@data-client/img": "npm:0.18.0" + "@data-client/react": "npm:0.18.0" + "@data-client/rest": "npm:0.18.0" "@linaria/core": "npm:*" "@linaria/react": "npm:*" "@linaria/shaker": "npm:*"