Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "data-monorepo",
"version": "0.10.12",
"version": "0.10.13",
"private": true,
"engines": {
"node": ">=24"
Expand Down
2 changes: 1 addition & 1 deletion packages/data-ai/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "adobe-data-ai",
"version": "0.10.12",
"version": "0.10.13",
"description": "Architecture skills for @adobe/data — data-oriented modelling, archetype iteration, hot-path performance, and related conventions.",
"author": {
"name": "Adobe"
Expand Down
2 changes: 1 addition & 1 deletion packages/data-ai/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-ai",
"version": "0.10.12",
"version": "0.10.13",
"description": "Cross-agent architecture skills for @adobe/data — installable as a Claude Code plugin or copied into any Agent-Skills-compatible agent (Cursor, Codex).",
"type": "module",
"private": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-gpu/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-gpu",
"version": "0.10.12",
"version": "0.10.13",
"description": "Adobe data WebGPU plugins and types for graphics and compute",
"type": "module",
"private": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-lit/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-lit",
"version": "0.10.12",
"version": "0.10.13",
"description": "Adobe data Lit bindings - hooks, elements, decorators",
"type": "module",
"private": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-persistence/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-persistence",
"version": "0.10.12",
"version": "0.10.13",
"description": "Worker-based incremental persistence layer for @adobe/data ECS over OPFS (browser) and node:fs (server).",
"type": "module",
"sideEffects": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-react",
"version": "0.10.12",
"version": "0.10.13",
"description": "Adobe data React bindings — hooks and context for ECS database",
"type": "module",
"private": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-rpc/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-rpc",
"version": "0.10.12",
"version": "0.10.13",
"description": "Schema-driven, bidirectional projection of @adobe/data async data services across a boundary (iframe / MessagePort / Worker). Only Data crosses the wire.",
"type": "module",
"sideEffects": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-solid/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-solid",
"version": "0.10.12",
"version": "0.10.13",
"description": "Adobe data SolidJS bindings — context and provider for ECS database",
"type": "module",
"private": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-sync/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-sync",
"version": "0.10.12",
"version": "0.10.13",
"description": "Multi-user real-time synchronisation for @adobe/data ECS — server, client, and in-process loopback.",
"type": "module",
"sideEffects": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-testing/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-testing",
"version": "0.10.12",
"version": "0.10.13",
"description": "Conformance-testing utilities (Match + Conformance runners) for @adobe/data ECS features",
"type": "module",
"sideEffects": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/data/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data",
"version": "0.10.12",
"version": "0.10.13",
"description": "Adobe data oriented programming library",
"type": "module",
"sideEffects": false,
Expand Down
51 changes: 44 additions & 7 deletions packages/data/src/ecs/archetype/archetype.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,24 @@ import { Entity } from "../entity/entity.js";
import { Table, ReadonlyTable } from "../../table/index.js";
import { Assert } from "../../types/assert.js";
import { Equal } from "../../types/equal.js";
import { Exact, StringKeyof } from "../../types/types.js";
import { Exact, Simplify, StringKeyof } from "../../types/types.js";

export type EntityInsertValues<C> = Omit<C, IdComponent>;
// `DK` (default-factory keys) names the components whose schema declares a
// `defaultFactory` (see Schema.defaultFactory). Those are OPTIONAL at insert:
// omit one and the archetype mints it via the factory; supply it (replication
// inbound / load) and the supplied value wins. Every other component stays
// required. `DK` defaults to `never`, so an archetype with no default factories
// keeps the original "all non-id components required" shape.
export type EntityInsertValues<C, DK extends keyof C = never> =
// No default keys → exactly `Omit<C, IdComponent>` (byte-identical to the
// original shape, so every existing archetype/store/transaction type is
// unchanged). Only when default keys exist do we split them out as optional.
[DK] extends [never]
? Omit<C, IdComponent>
: Simplify<
& Omit<C, IdComponent | DK>
& Partial<Pick<C, Exclude<Extract<DK, keyof C>, IdComponent>>>
>;
export type ArchetypeId = number;

/**
Expand Down Expand Up @@ -47,9 +62,9 @@ export interface ReadonlyArchetype<C = {}> extends BaseArchetype, ReadonlyTable<
toData: (copy?: boolean, omit?: ReadonlySet<string>) => unknown
}

export interface Archetype<C = {}> extends BaseArchetype, Table<C & RequiredComponents> {
export interface Archetype<C = {}, DK extends keyof C = never> extends BaseArchetype, Table<C & RequiredComponents> {
readonly components: ComponentSet<StringKeyof<C>>;
insert: <T extends EntityInsertValues<C>>(rowData: Exact<EntityInsertValues<C>, T>) => Entity;
insert: <T extends EntityInsertValues<C, DK>>(rowData: Exact<EntityInsertValues<C, DK>, T>) => Entity;
/** See {@link ReadonlyArchetype.toData}. */
toData: (copy?: boolean, omit?: ReadonlySet<string>) => unknown
/**
Expand Down Expand Up @@ -80,9 +95,9 @@ export namespace Archetype {
* component) therefore still permits `.insert` with no narrowing — only dense
* column access requires having resolved to a concrete {@link Archetype}.
*/
export interface Router<C = {}> {
export interface Router<C = {}, DK extends keyof C = never> {
readonly components: ComponentSet<StringKeyof<C>>;
insert: <T extends EntityInsertValues<C>>(rowData: Exact<EntityInsertValues<C>, T>) => Entity;
insert: <T extends EntityInsertValues<C, DK>>(rowData: Exact<EntityInsertValues<C, DK>, T>) => Entity;
}
}

Expand All @@ -91,7 +106,7 @@ export namespace Archetype {
// part of the component row.
export type FromArchetype<T> =
T extends ReadonlyArchetype<infer C> ? { readonly [K in keyof Omit<C, IdComponent>]: C[K] } :
T extends Archetype<infer C> ? { readonly [K in keyof Omit<C, IdComponent>]: C[K] } :
T extends Archetype<infer C, any> ? { readonly [K in keyof Omit<C, IdComponent>]: C[K] } :
never;

// compile time type tests.
Expand Down Expand Up @@ -120,4 +135,26 @@ type TestIdColumnStillTyped = Assert<IdComponent extends keyof Archetype<{ a: nu
// @ts-expect-error - Should reject extra properties
arch.insert(invalidData);
};
}

// Compile-time tests for default-factory optionality (DK).
{
// guid names a default factory → optional at insert; position stays required.
type GuidArchetype = Archetype<{ guid: [number, number], position: [number, number, number] }, "guid">;

const testOmitDefaulted = (arch: GuidArchetype) => {
// guid omitted → the archetype mints it. Must compile.
arch.insert({ position: [0, 0, 0] });
// guid supplied (replication-inbound / load path) → wins. Must compile.
arch.insert({ position: [0, 0, 0], guid: [1, 2] });
};

const testStillRequired = (arch: GuidArchetype) => {
// @ts-expect-error - position is not a default-factory key, still required.
arch.insert({ guid: [1, 2] });
};

// A defaulted key is only optional, never removed: EntityInsertValues keeps it.
type Values = EntityInsertValues<{ guid: [number, number], position: [number, number, number] }, "guid">;
type TestGuidOptional = Assert<Equal<Values, { position: [number, number, number]; guid?: [number, number] }>>;
}
53 changes: 52 additions & 1 deletion packages/data/src/ecs/archetype/create-archetype.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// © 2026 Adobe. MIT License. See /LICENSE for details.
import { describe, it, expect } from 'vitest';
import { createArchetype } from '../archetype/index.js';
import { createArchetype, type Archetype } from '../archetype/index.js';
import { createEntityLocationTable } from '../entity-location-table/index.js';
import { Entity } from '../entity/entity.js';
import { U32 } from '../../math/u32/index.js';
Expand Down Expand Up @@ -132,6 +132,57 @@ describe('createArchetype', () => {
expect(newArchetype.columns.id.get(1)).toBe(4);
});

it('mints a default-factory component when the insert row omits it, fresh per insert', () => {
const entityLocationTable = createEntityLocationTable();
const components = { id: Entity.schema, value: U32.schema, seq: U32.schema };
let next = 100;
// Runtime invariant the compiler can't see: `seq` has a default factory, so
// it is optional at insert. createArchetype's return type reports DK=never
// (the core/store boundary is where DK is derived), so we assert it here.
const archetype = createArchetype(components, 5, entityLocationTable, undefined, {
seq: () => next++,
}) as unknown as Archetype<{ value: number; seq: number }, "seq">;

// seq omitted → minted by the factory, a fresh value each insert.
archetype.insert({ value: 1 });
archetype.insert({ value: 2 });
expect(archetype.columns.seq.get(0)).toBe(100);
expect(archetype.columns.seq.get(1)).toBe(101);
expect(next).toBe(102);
});

it('uses the supplied value over the factory (replication-inbound / load path)', () => {
const entityLocationTable = createEntityLocationTable();
const components = { id: Entity.schema, seq: U32.schema };
let calls = 0;
// Runtime invariant the compiler can't see: `seq` is default-factory ⇒ DK.
const archetype = createArchetype(components, 6, entityLocationTable, undefined, {
seq: () => { calls++; return 999; },
}) as unknown as Archetype<{ seq: number }, "seq">;

archetype.insert({ seq: 7 });
expect(archetype.columns.seq.get(0)).toBe(7);
expect(calls).toBe(0); // factory never ran for a supplied value
});

it('applies default factories on the generic (non-identifier name) insert path', () => {
const entityLocationTable = createEntityLocationTable();
// A non-identifier component name forces buildGenericInsert (codegen is skipped).
const components = { id: Entity.schema, ['weird-name']: U32.schema };
let next = 10;
// `any` archetype handle: this test exercises the runtime generic-insert
// path, not the insert types (a non-identifier key isn't a valid TS field).
const archetype: any = createArchetype(components as any, 7, entityLocationTable, undefined, {
['weird-name']: () => next++,
});

archetype.insert({}); // omitted → minted
archetype.insert({ ['weird-name']: 42 }); // supplied → wins
expect((archetype.columns as any)['weird-name'].get(0)).toBe(10);
expect((archetype.columns as any)['weird-name'].get(1)).toBe(42);
expect(next).toBe(11);
});

it('should preserve component set during serialization/deserialization', () => {
const entityLocationTable = createEntityLocationTable();
const components = {
Expand Down
41 changes: 36 additions & 5 deletions packages/data/src/ecs/archetype/create-archetype.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const buildSpecializedInsert = (
archetypeId: number,
columns: Record<string, TypedBuffer<any>>,
entityLocationTable: EntityLocationTable,
defaultFactories: Record<string, () => unknown>,
): InsertImpl | null => {
const componentNames = Object.keys(columns);
if (componentNames.some((n) => !SAFE_IDENT.test(n))) {
Expand All @@ -90,14 +91,26 @@ const buildSpecializedInsert = (
// with reserved words or shadowing globals (e.g. a component called
// `delete`).
const componentParamNames: string[] = [];
const componentParamValues: TypedBuffer<any>[] = [];
const componentParamValues: (TypedBuffer<any> | (() => unknown))[] = [];
const sets: string[] = [];
for (const name of componentNames) {
if (name === ID) continue;
const local = `_${name}`;
componentParamNames.push(local);
componentParamValues.push(columns[name]);
sets.push(` ${local}.set(row, rowData.${name});`);
const factory = defaultFactories[name];
if (factory === undefined) {
sets.push(` ${local}.set(row, rowData.${name});`);
} else {
// Default-factory component: the insert row MAY omit it. When it does,
// mint a fresh value via the baked factory const; a supplied value
// (replication inbound / load) always wins, so the factory never runs
// for it. One extra `!== undefined` branch, only on defaulted columns.
const factoryLocal = `_factory_${name}`;
componentParamNames.push(factoryLocal);
componentParamValues.push(factory);
sets.push(` ${local}.set(row, rowData.${name} !== undefined ? rowData.${name} : ${factoryLocal}());`);
}
}

const factoryBody = `
Expand Down Expand Up @@ -137,7 +150,9 @@ ${sets.join("\n")}
ensureCapacityFn: typeof ensureCapacity,
entityLocationTable: EntityLocationTable,
idColumn: TypedBuffer<number>,
...componentColumns: TypedBuffer<any>[]
// Trailing args are the baked per-component column refs, each optionally
// followed by its `_factory_<name>` const — hence the widened element type.
...componentColumnsAndFactories: (TypedBuffer<any> | (() => unknown))[]
) => InsertImpl;

return factory(
Expand All @@ -156,9 +171,17 @@ ${sets.join("\n")}
const buildGenericInsert = (
archetypeId: number,
entityLocationTable: EntityLocationTable,
defaultFactories: Record<string, () => unknown>,
): InsertImpl => {
const factoryEntries = Object.entries(defaultFactories);
return (archetype: any, rowData: any) => {
const row = TABLE.addRow(archetype, rowData);
// Mint any default-factory component the row omitted. addRow only sets the
// columns present in rowData, so an omitted defaulted column is filled here
// (a supplied value went through addRow already and is left untouched).
for (const [name, factory] of factoryEntries) {
if (rowData[name] === undefined) archetype.columns[name].set(row, factory());
}
const entity = entityLocationTable.create({ archetype: archetypeId, row });
archetype.columns[ID].set(row, entity);
return entity;
Expand All @@ -170,6 +193,14 @@ export const createArchetype = <C extends Record<IdComponent, typeof Entity.sche
id: number,
entityLocationTable: EntityLocationTable,
allocator?: MemoryAllocator,
// Resolved `component name → () => value` map (see Schema.defaultFactory). The
// caller (core) resolves each component's `defaultFactory` NAME against the
// store registry and passes only the entries whose component this archetype
// actually carries. Omitted → no defaulting (the original insert shape). The
// returned Archetype's insert type still reports every component as required
// (DK = never here); the OPTIONAL-at-insert typing is applied by the core /
// store boundary that knows which names are defaulted (see ensureArchetype).
defaultFactories: Record<string, () => unknown> = {},
): Archetype<Omit<{ [K in keyof C]: Schema.ToType<C[K]> }, IdComponent>> => {
// The archetype's public COMPONENT set excludes `id`: id is the entity's
// identity, a column but never a component value. (`table.columns` and the
Expand All @@ -184,8 +215,8 @@ export const createArchetype = <C extends Record<IdComponent, typeof Entity.sche
let insertImpl: InsertImpl;
const refreshInsertImpl = () => {
insertImpl =
buildSpecializedInsert(id, archetype.columns as Record<string, TypedBuffer<any>>, entityLocationTable) ??
buildGenericInsert(id, entityLocationTable);
buildSpecializedInsert(id, archetype.columns as Record<string, TypedBuffer<any>>, entityLocationTable, defaultFactories) ??
buildGenericInsert(id, entityLocationTable, defaultFactories);
};

const createEntity = (rowData: EntityInsertValues<PublicComponents>): Entity => {
Expand Down
8 changes: 6 additions & 2 deletions packages/data/src/ecs/database/create-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { TransactionDeclarations, ToTransactionFunctions } from "../store/t
import type { ToActionFunctions } from "../store/action-functions.js";
import type { FromSchemas } from "../../schema/index.js";
import type { PartitionKeysOf } from "../store/partition.js";
import type { DefaultFactoryKeys } from "../default-factory-keys.js";
import type { StringKeyof, NoInfer, RemoveIndex } from "../../types/types.js";
import { combinePlugins } from "./combine-plugins.js";
import { Store } from "../store/store.js";
Expand Down Expand Up @@ -190,7 +191,7 @@ export function createPlugin<
const RS extends ResourceSchemas,
const A extends ArchetypeComponents<StringKeyof<RemoveIndex<CS> & XP['components'] & IP['components']>>,
const IX extends IndexDeclarations<FromSchemas<RemoveIndex<CS> & XP['components'] & IP['components']>, RemoveIndex<A> & XP['archetypes'] & IP['archetypes']>,
const TD extends TransactionDeclarations<FromSchemas<RemoveIndex<CS> & XP['components'] & IP['components']>, FromSchemas<RemoveIndex<RS> & XP['resources'] & IP['resources']>, RemoveIndex<A> & XP['archetypes'] & IP['archetypes'], RemoveIndex<IX> & XP['indexes'] & IP['indexes'], PartitionKeysOf<RemoveIndex<CS> & XP['components'] & IP['components']>>,
const TD extends TransactionDeclarations<FromSchemas<RemoveIndex<CS> & XP['components'] & IP['components']>, FromSchemas<RemoveIndex<RS> & XP['resources'] & IP['resources']>, RemoveIndex<A> & XP['archetypes'] & IP['archetypes'], RemoveIndex<IX> & XP['indexes'] & IP['indexes'], PartitionKeysOf<RemoveIndex<CS> & XP['components'] & IP['components']>, DefaultFactoryKeys<RemoveIndex<CS> & XP['components'] & IP['components']>>,
const AD,
const S extends string = never,
const SVF extends ServiceFactories<Database.FromPlugin<AmbientPlugin<XP, IP>>> = {},
Expand Down Expand Up @@ -244,7 +245,10 @@ export function createPlugin<
readonly store: Store<
FromSchemas<RemoveIndex<CS> & XP['components'] & IP['components']>,
FromSchemas<RemoveIndex<RS> & XP['resources'] & IP['resources']>,
RemoveIndex<A> & XP['archetypes'] & IP['archetypes']
RemoveIndex<A> & XP['archetypes'] & IP['archetypes'],
{},
never,
DefaultFactoryKeys<RemoveIndex<CS> & XP['components'] & IP['components']>
>
services: { -readonly [K in keyof FromServiceFactories<RemoveIndex<SVF> & XP['services'] & IP['services']>]: FromServiceFactories<RemoveIndex<SVF> & XP['services'] & IP['services']>[K] }
}) => SystemFunction | void;
Expand Down
3 changes: 2 additions & 1 deletion packages/data/src/ecs/database/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { toSystemDatabase as _toSystemDatabase } from "./to-system-database.js";
import { ResourceSchemas } from "../resource-schemas.js";
import { ComponentSchemas } from "../component-schemas.js";
import { PartitionKeysOf } from "../store/partition.js";
import type { DefaultFactoryKeys } from "../default-factory-keys.js";
import { FromSchemas } from "../../schema/index.js";
import type {
TransactionDeclarations,
Expand Down Expand Up @@ -507,7 +508,7 @@ export namespace Database {
* type transaction functions operate on; a store *is* the transaction
* context, so there is no separate transaction-context type.
*/
export type ToStore<P extends Database.Plugin> = Store<FromSchemas<RemoveIndex<P['components']>>, FromSchemas<RemoveIndex<P['resources']>>, RemoveIndex<P['archetypes']>, RemoveIndex<P['indexes']>, PartitionKeysOf<RemoveIndex<P['components']>>>;
export type ToStore<P extends Database.Plugin> = Store<FromSchemas<RemoveIndex<P['components']>>, FromSchemas<RemoveIndex<P['resources']>>, RemoveIndex<P['archetypes']>, RemoveIndex<P['indexes']>, PartitionKeysOf<RemoveIndex<P['components']>>, DefaultFactoryKeys<RemoveIndex<P['components']>>>;
export type ToSystemDatabase<P extends Database.Plugin> = Database.FromPlugin<P> & {
// Systems are allowed to access the database store directly.
// This direct access will NOT trigger observable transactions.
Expand Down
Loading