Skip to content

Version Packages#3926

Merged
ntucker merged 1 commit intomasterfrom
changeset-release/master
May 1, 2026
Merged

Version Packages#3926
ntucker merged 1 commit intomasterfrom
changeset-release/master

Conversation

@github-actions
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot commented Apr 28, 2026

This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to master, this PR will be updated.

Releases

@data-client/core@0.18.0

Minor Changes

  • #3931 959465a - Allow one Collection schema to be used both top-level and nested.

    Before:

    const getTodos = new Collection([Todo], { argsKey });
    const userTodos = new Collection([Todo], { nestKey });

    After:

    const userTodos = new Collection([Todo], { argsKey, nestKey });
  • #3887 84078d7 - BREAKING: Schema.denormalize() is now (input, delegate) instead
    of the previous (input, args, unvisit) 3-parameter signature.

    // before
    denormalize(input, args, unvisit) {
      return unvisit(this.schema, input);
    }
    
    // after
    denormalize(input, delegate) {
      return delegate.unvisit(this.schema, input);
    }

    The new IDenormalizeDelegate
    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:

    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 84078d7 - Add 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:

    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 6e8e499 - 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, 84078d7, 6e8e499, 396d163, 84078d7]:

    • @data-client/normalizr@0.18.0

@data-client/endpoint@0.18.0

Minor Changes

  • #3931 959465a - Allow one Collection schema to be used both top-level and nested.

    Before:

    const getTodos = new Collection([Todo], { argsKey });
    const userTodos = new Collection([Todo], { nestKey });

    After:

    const userTodos = new Collection([Todo], { argsKey, nestKey });
  • #3887 84078d7 - BREAKING: Schema.denormalize() is now (input, delegate) instead
    of the previous (input, args, unvisit) 3-parameter signature.

    // before
    denormalize(input, args, unvisit) {
      return unvisit(this.schema, input);
    }
    
    // after
    denormalize(input, delegate) {
      return delegate.unvisit(this.schema, input);
    }

    The new IDenormalizeDelegate
    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:

    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 396d163 - 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:

    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:

    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 84078d7 - Add 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:

    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 6e8e499 - 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.

@data-client/graphql@0.18.0

Minor Changes

  • #3931 959465a - Allow one Collection schema to be used both top-level and nested.

    Before:

    const getTodos = new Collection([Todo], { argsKey });
    const userTodos = new Collection([Todo], { nestKey });

    After:

    const userTodos = new Collection([Todo], { argsKey, nestKey });
  • #3887 84078d7 - BREAKING: Schema.denormalize() is now (input, delegate) instead
    of the previous (input, args, unvisit) 3-parameter signature.

    // before
    denormalize(input, args, unvisit) {
      return unvisit(this.schema, input);
    }
    
    // after
    denormalize(input, delegate) {
      return delegate.unvisit(this.schema, input);
    }

    The new IDenormalizeDelegate
    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:

    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 84078d7 - Add 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:

    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 6e8e499 - 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, 84078d7, 6e8e499, 396d163, 84078d7]:

    • @data-client/endpoint@0.18.0

@data-client/normalizr@0.18.0

Minor Changes

  • #3931 959465a - Allow one Collection schema to be used both top-level and nested.

    Before:

    const getTodos = new Collection([Todo], { argsKey });
    const userTodos = new Collection([Todo], { nestKey });

    After:

    const userTodos = new Collection([Todo], { argsKey, nestKey });
  • #3887 84078d7 - BREAKING: Schema.denormalize() is now (input, delegate) instead
    of the previous (input, args, unvisit) 3-parameter signature.

    // before
    denormalize(input, args, unvisit) {
      return unvisit(this.schema, input);
    }
    
    // after
    denormalize(input, delegate) {
      return delegate.unvisit(this.schema, input);
    }

    The new IDenormalizeDelegate
    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:

    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 396d163 - 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:

    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:

    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 84078d7 - Add 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:

    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 6e8e499 - 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.

@data-client/react@0.18.0

Minor Changes

  • #3931 959465a - Allow one Collection schema to be used both top-level and nested.

    Before:

    const getTodos = new Collection([Todo], { argsKey });
    const userTodos = new Collection([Todo], { nestKey });

    After:

    const userTodos = new Collection([Todo], { argsKey, nestKey });
  • #3887 84078d7 - BREAKING: Schema.denormalize() is now (input, delegate) instead
    of the previous (input, args, unvisit) 3-parameter signature.

    // before
    denormalize(input, args, unvisit) {
      return unvisit(this.schema, input);
    }
    
    // after
    denormalize(input, delegate) {
      return delegate.unvisit(this.schema, input);
    }

    The new IDenormalizeDelegate
    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:

    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 84078d7 - Add 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:

    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 6e8e499 - 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, 84078d7, 6e8e499, 84078d7]:

    • @data-client/core@0.18.0

@data-client/rest@0.18.0

Minor Changes

  • #3931 959465a - Allow one Collection schema to be used both top-level and nested.

    Before:

    const getTodos = new Collection([Todo], { argsKey });
    const userTodos = new Collection([Todo], { nestKey });

    After:

    const userTodos = new Collection([Todo], { argsKey, nestKey });
  • #3887 84078d7 - BREAKING: Schema.denormalize() is now (input, delegate) instead
    of the previous (input, args, unvisit) 3-parameter signature.

    // before
    denormalize(input, args, unvisit) {
      return unvisit(this.schema, input);
    }
    
    // after
    denormalize(input, delegate) {
      return delegate.unvisit(this.schema, input);
    }

    The new IDenormalizeDelegate
    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:

    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 84078d7 - Add 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:

    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 6e8e499 - 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, 84078d7, 6e8e499, 396d163, 84078d7]:

    • @data-client/endpoint@0.18.0

@data-client/vue@0.18.0

Minor Changes

  • #3931 959465a - Allow one Collection schema to be used both top-level and nested.

    Before:

    const getTodos = new Collection([Todo], { argsKey });
    const userTodos = new Collection([Todo], { nestKey });

    After:

    const userTodos = new Collection([Todo], { argsKey, nestKey });
  • #3887 84078d7 - BREAKING: Schema.denormalize() is now (input, delegate) instead
    of the previous (input, args, unvisit) 3-parameter signature.

    // before
    denormalize(input, args, unvisit) {
      return unvisit(this.schema, input);
    }
    
    // after
    denormalize(input, delegate) {
      return delegate.unvisit(this.schema, input);
    }

    The new IDenormalizeDelegate
    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:

    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 84078d7 - Add 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:

    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

@data-client/img@0.18.0

Patch Changes

@data-client/test@0.18.0

Patch Changes

  • 89e06d3 - Bump @data-client/react peer dependency range to include ^0.18.0.

example-benchmark@0.4.85

Patch Changes

example-benchmark-react@0.1.10

Patch Changes

coinbase-lite@0.0.25

Patch Changes

normalizr-github-example@0.1.59

Patch Changes

normalizr-redux-example@0.1.57

Patch Changes

normalizr-relationships@0.1.59

Patch Changes

test-bundlesize@0.1.17

Patch Changes

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs-site Ignored Ignored Preview May 1, 2026 2:09am

Request Review

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 28, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.21%. Comparing base (d0aa990) to head (2dcd47b).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #3926   +/-   ##
=======================================
  Coverage   98.21%   98.21%           
=======================================
  Files         154      154           
  Lines        3024     3024           
  Branches      601      601           
=======================================
  Hits         2970     2970           
  Misses         11       11           
  Partials       43       43           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions github-actions Bot force-pushed the changeset-release/master branch 6 times, most recently from 67df41e to 149a09c Compare April 29, 2026 13:56
@github-actions github-actions Bot force-pushed the changeset-release/master branch 4 times, most recently from c0ec94d to 2dcd47b Compare April 30, 2026 14:17
@github-actions github-actions Bot force-pushed the changeset-release/master branch from 2dcd47b to f405bce Compare May 1, 2026 02:09
@ntucker ntucker merged commit be16593 into master May 1, 2026
2 of 3 checks passed
@ntucker ntucker deleted the changeset-release/master branch May 1, 2026 02:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant