feat(ecs)!: index API — named-object args, archetype scoping, code-point ordering#117
Merged
Conversation
… 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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/findRangealways 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>objectA computed slot's extractor receives one named object of the component values, read by name (
(c) => c.email!.toLowerCase()) rather than positional args. TypedPartial<C>so it doesn't unsafely imply presence — at runtime only the declaredcomponentsare populated; use!/?.on the ones you declared. (TightPick<C, components>would require a per-index builder, deliberately not taken.)3. Optional
archetypescope (new)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 oflocaleCompare. Red/green guard tests assert bothFractionalIndex.compareand the index default comparator sort by ASCII code point (uppercase before lowercase) — they fail if either is switched tolocaleCompare, so locale collation can't sneak back into index order.Verification
tsc -bclean, lint clean, 2509 tests pass (incl. the red/green locale guards and archetype-scope tests). CI gate green.Related
observe), fix(ci): add PR build/test gate and repair all tsc -b + lint failures #115 (CI gate).🤖 Generated with Claude Code