Skip to content

ecs: name archetype rows without a cast + flatten combine's IntersectAll#122

Merged
krisnye merged 3 commits into
mainfrom
krisnye/db-type-fix
Jun 9, 2026
Merged

ecs: name archetype rows without a cast + flatten combine's IntersectAll#122
krisnye merged 3 commits into
mainfrom
krisnye/db-type-fix

Conversation

@krisnye

@krisnye krisnye commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Description

Two related type-system improvements in @adobe/data (packages/data/src/ecs).

Part 1 — name archetype rows without a cast (Database.Archetype.RowOf)

db.archetypes.<Name> is a ReadonlyArchetype<Row> whose Row is derived from the declared columns. Consumers exposing an archetype under a hand-authored service interface had to launder it through as unknown as at every public boundary (and per-entity in db.observe.entity(id, archetype)).

We considered carrying an explicit "brand" type through inference, but confirmed branding adds no type checking — it only lets you hide internal columns behind a narrower public row. The full structural row is fine here, so we drop branding entirely. The cast only ever arose because a service type re-declared archetype rows by hand; a service type derived from the plugin (Database.Plugin.ToDatabase<typeof plugin>) already has the same row type, so db.archetypes assigns with no cast.

  • Add Database.Archetype.RowOf<S, K> (reusing the existing FromArchetype) so a row can be named without re-spelling its columns.
  • No runtime change; no change to ArchetypeComponents, the store mapped type, or create-plugin.
  • New archetype-row.type-test.ts proves the no-cast assignment, RowOf equality, ReadonlyArchetype<RowOf<…>>, and the observe.entity row flow. README note steers consumers to derive, not re-declare or cast.
type MainService = Database.Plugin.ToDatabase<typeof plugin>;
type Track = Database.Archetype.RowOf<MainService, "Track">;
const t: ReadonlyArchetype<Track> = db.archetypes.Track; // no cast

Part 2 — flatten combine's IntersectAll

combine-plugins.ts intersected each of the 9 plugin buckets with a linearly-recursive, Simplify-wrapped IntersectAll<T> = Simplify<H & IntersectAll<R>>. The per-level Simplify forced eager re-materialization of the intersection (×9 buckets — the bulk of the instantiation count).

Reformulated using the already-shipped non-recursive UnionToIntersection<T[number]> (empty-tuple guarded), and UnionAllX[number] for the systems union.

  • ~29% fewer instantiations (296,595 → 210,305) on a 24-plugin single combine (measured locally; everything else constant).
  • Behavior-preserving: merged buckets are object maps that intersect soundly; the === identity conflict guard and merge order are value-level and unchanged. systems stays a real union.
  • New combine-plugins.type-test.ts guards correct merged-type resolution over a wide single combine.

Verification

  • tsc -b clean; eslint clean on changed files.
  • All 2585 package tests pass (one timing-ratio perf test flaked under parallel load; passes in isolation — Part 2 is type-only and cannot affect runtime).
  • extends typeperf baseline unchanged (that path goes through CreatePluginResult, not combine).

Related PRs

Follows #120 (thread index declarations into the computed-factory db type) and #117 (index API).

krisnye and others added 3 commits June 8, 2026 19:09
…t a cast

db.archetypes.<Name> is a ReadonlyArchetype<Row> whose Row is derived from the
declared columns. Consumers exposing an archetype under a hand-authored service
interface had to launder it through `as unknown as` at every boundary. The cast
only arises when the service type re-declares archetype rows by hand; a service
type *derived* from the plugin (Database.Plugin.ToDatabase) already has the same
row type, so db.archetypes assigns with no cast.

Add Database.Archetype.RowOf<S, K> (reusing the existing FromArchetype) so a row
can be *named* without re-spelling its columns. No runtime change, no branding —
the full structural row is preserved through inference.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
combine-plugins.ts intersected each of the 9 plugin buckets with a linearly
recursive, Simplify-wrapped IntersectAll<T> = Simplify<H & IntersectAll<R>>.
Two cost multipliers: the per-level Simplify forced eager re-materialization of
the intersection (×9 buckets, the bulk of the instantiation count), and the
linear recursion depth scaled with plugin count (the TS2589 ceiling).

Reformulate using the already-shipped non-recursive UnionToIntersection over
the element union, with an empty-tuple guard preserving the old `unknown`. The
systems slot stays a real union, now via X[number] (UnionAll deleted).

Behavior-preserving: the merged buckets are object maps that intersect soundly;
the === identity conflict guard and merge order are value-level and unchanged.
Measured ~29% fewer instantiations (296,595 → 210,305) on a 24-plugin single
combine; all 2585 tests pass; type-check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@krisnye krisnye merged commit 00c674e into main Jun 9, 2026
3 checks passed
@krisnye krisnye deleted the krisnye/db-type-fix branch June 9, 2026 02:28
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