Skip to content

Releases: reactive/data-client

@data-client/vue@0.18.0

01 May 02:15
be16593

Choose a tag to compare

📝 Read the full release announcement


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/test@0.18.0

01 May 02:15
be16593

Choose a tag to compare

📝 Read the full release announcement


Patch Changes

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

@data-client/rest@0.18.0

01 May 02:15
be16593

Choose a tag to compare

📝 Read the full release announcement


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/react@0.18.0

01 May 02:15
be16593

Choose a tag to compare

📝 Read the full release announcement


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/normalizr@0.18.0

01 May 02:15
be16593

Choose a tag to compare

📝 Read the full release announcement


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/img@0.18.0

01 May 02:15
be16593

Choose a tag to compare

📝 Read the full release announcement


Patch Changes

@data-client/graphql@0.18.0

01 May 02:15
be16593

Choose a tag to compare

📝 Read the full release announcement


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/endpoint@0.18.0

01 May 02:15
be16593

Choose a tag to compare

📝 Read the full release announcement


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/core@0.18.0

01 May 02:15
be16593

Choose a tag to compare

📝 Read the full release announcement


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/vue@0.16.1

16 Apr 02:18
4f5b6b6

Choose a tag to compare

📝 Read the full release announcement


Patch Changes

  • fd64b41 - Include @data-client/normalizr@0.16.6 performance improvements:

    • #3875 467a5f6 - Fix deepClone to only copy own properties

      deepClone in the immutable store path now uses Object.keys() instead of for...in, preventing inherited properties from being copied into cloned state.

    • #3877 e9e96f1 - Replace megamorphic computed dispatch in getDependency with switch

      getDependency used delegate[array[index]](...spread) which creates a temporary array, a computed property lookup, and a spread call on every invocation — a megamorphic pattern that prevents V8 from inlining or type-specializing the call site. Replaced with a switch on path.length for monomorphic dispatch.

    • #3876 7d28629 - Improve denormalization performance by pre-allocating the dependency tracking slot

      Replace Array.prototype.unshift() in GlobalCache.getResults() with a pre-allocated slot at index 0, avoiding O(n) element shifting on every cache-miss denormalization.

    • #3884 7df6a49 - Move entity table POJO clone from getNewEntities to setEntity

      Lazy-clone entity and meta tables on first write per entity type instead of eagerly in getNewEntities. This keeps getNewEntities as a pure Map operation, eliminating its V8 Maglev bailout ("Insufficient type feedback for generic named access" on this.entities).

    • #3878 98a7831 - Avoid hidden class mutation in normalize() return object

      The normalize result object was constructed with result: '' as any then mutated via ret.result = visit(...), causing a V8 hidden class transition when the property type changed from string to the actual result type. Restructured to compute the result first and construct the final object in a single step.

  • Updated dependencies [fd64b41]:

    • @data-client/core@0.16.7