Skip to content

feat(ecs)!: index API — named-object args, archetype scoping, code-point ordering#117

Merged
krisnye merged 6 commits into
mainfrom
krisnye/index-named-object-args
Jun 5, 2026
Merged

feat(ecs)!: index API — named-object args, archetype scoping, code-point ordering#117
krisnye merged 6 commits into
mainfrom
krisnye/index-named-object-args

Conversation

@krisnye

@krisnye krisnye commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

Description

A set of index-API improvements (all on the new, unconsumed index surface — 0 non-test callers, so the breaking parts are a clean break on 0.x).

1. Lookup args are always a named object (breaking)

find / get / observe / findRange always take { field: value, … } — never a bare scalar. key: "parent"find({ parent: 7 }); uniform across every index, with hover showing { parent: number }. Bare-function keys are removed — computed keys are slot maps ({ emailLower: (c) => … }).

2. Computed extractors take a named Partial<C> object

A computed slot's extractor receives one named object of the component values, read by name ((c) => c.email!.toLowerCase()) rather than positional args. Typed Partial<C> so it doesn't unsafely imply presence — at runtime only the declared components are populated; use !/?. on the ones you declared. (Tight Pick<C, components> would require a per-index builder, deliberately not taken.)

3. Optional archetype scope (new)

indexes: { tasksByParent: { key: "parent", archetype: "Task" } }

Scopes an index to one archetype (superset of its components) instead of every entity sharing the key column. The name is validated against the schema's declared archetypes (typo = compile error). Runtime folds the archetype's component set into the seeding/inclusion predicate. Single archetype only (a list would be ambiguous AND/OR).

4. Code-point string ordering — never locale (correctness)

Added FractionalIndex.compare (explicit (a < b ? -1 : a > b ? 1 : 0)), used in the README in place of localeCompare. Red/green guard tests assert both FractionalIndex.compare and the index default comparator sort by ASCII code point (uppercase before lowercase) — they fail if either is switched to localeCompare, so locale collation can't sneak back into index order.

Verification

tsc -b clean, lint clean, 2509 tests pass (incl. the red/green locale guards and archetype-scope tests). CI gate green.

Related

🤖 Generated with Claude Code

krisnye and others added 3 commits June 5, 2026 12:34
… take a named component object

BREAKING: find/get/observe/findRange no longer accept a bare scalar. Every
index key resolves to a `{ field: value }` lookup object, uniform across all
indexes and with self-documenting intellisense (`find({ parent: 7 })`).

- `key: "col"` is sugar for the one-column tuple `["col"]`; its arg is
  `{ col: value }` (was a bare scalar).
- The bare `(...) => value` key form is removed. A computed key is a slot map,
  e.g. `{ emailLower: (c) => c.email.toLowerCase() }`, so the derived value is
  addressed by name.
- Computed-slot extractors receive a single strongly-typed named object `c` of
  the index's declared `components` (`c.email`), not positional args — there is
  no argument order to get wrong and field access is type-checked.

Runtime: a string key normalizes to a one-column tuple, so buckets and lookup
keys are both `{ col: value }` and array fan-out still composes via the
existing cartesian expansion. No production callers use the handles yet (only
tests), so this is a clean break.

Updates the type surface (index-types.ts), runtime (create-index.ts), the
where/order auto-router (create-database.ts), the README pattern catalogue,
and all tests/type-tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The extractor's named component object is typed Partial<C> instead of C, so
the types don't unsafely imply every component is present: at runtime only the
declared `components` are populated, and now every field is optional. Read
declared components with `!` / `?.` (e.g. (c) => c.email!.toLowerCase()).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dering

Archetype scoping: an index may set `archetype: "Name"` to cover only that
archetype (superset of its components) instead of every entity sharing the key
column. The name is validated against the schema's declared archetypes (typo =
compile error, via threading the archetype map A into IndexDeclarations).
Runtime: the archetype's component set becomes an inclusion predicate folded
into readColumns, so seeding walks only matching archetypes and entities in
other archetypes are excluded. Single archetype only (a list would be
ambiguous AND/OR).

Code-point string ordering (never locale): add FractionalIndex.compare — an
explicit `(a < b ? -1 : a > b ? 1 : 0)` comparator — and use it in the README
in place of localeCompare. Red/green guard tests assert both
FractionalIndex.compare and the index default comparator sort by ASCII code
point (uppercase before lowercase), which fail if either is switched to
localeCompare.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@krisnye krisnye changed the title feat(ecs)!: index lookup args are always named objects feat(ecs)!: index API — named-object args, archetype scoping, code-point ordering Jun 5, 2026
krisnye and others added 3 commits June 5, 2026 14:22
The code-point comparator isn't FractionalIndex-specific — it's the
comparison the whole index + ordered-query system should always use. Moved it
out of fractional-index to `@adobe/data/functions` as `compare`, and wired
both consumers to it:

- index default comparator (create-index) now calls `compare`
- ordered `select` / `observe.select` sort (select-entities) now calls
  `compare` instead of `a - b` — which was NaN for string order keys, a real
  bug; numeric order is unchanged.

Generic over number | bigint | string | boolean; `<`/`>` (code point for
strings), never localeCompare. README + guard tests updated; added a query-sort
code-point guard alongside the index one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…indexes)

Index maintenance no longer scans every index on every mutation. The registry
caches archetypeId -> applicable RuntimeIndex[] (an index applies iff the
archetype's components ⊇ its readColumns, which already folds in the
archetype-scope columns), so insert/update/delete dispatch to only the indexes
that apply to the mutated entity's archetype — O(1) cached lookup + O(applicable)
walk. The store passes the archetype through the three mutation sites; update
captures from/to archetypes and updates the union so an entity that changes
archetype (gains/loses a component) is removed from indexes it left and added to
those it entered.

This also lets the per-entity O(scopeColumns) membership loop be dropped from
bucketEntriesFor entirely (dispatch guarantees only in-scope archetypes reach
it), and the unique pre-checks gate by archetype / readColumns-presence so a
scoped unique index is never tested against an out-of-archetype entity.

Verified flat insert scaling vs unrelated index count (~0.91x at 150 indexes);
correctness covered by new archetype-scoped + archetype-changing-update tests
plus the full index suite (2517 pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@krisnye krisnye merged commit 20f2917 into main Jun 5, 2026
3 checks passed
@krisnye krisnye deleted the krisnye/index-named-object-args branch June 5, 2026 22:39
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