Skip to content

Commit 6958d00

Browse files
krisnyeclaude
andauthored
Add typed, plugin-declared indexes to the ECS database (#107)
* Add typed, plugin-declared indexes to the ECS database Plugins now declare `indexes` alongside `components` / `archetypes`. The runtime maintains them eagerly per mutation at the Store layer, so `db.indexes.<name>` and `t.indexes.<name>` lookups are always fresh — including inside the same transaction that just mutated a row. Unique conflicts pre-check before any column mutation and surface from the offending insert/update call, so store and index stay consistent under rollback. Public API additions: - `Database.Index<C, Keys, Unique>` and `Database.Index.Handle<C, I>` (compile-time-checked component keys; `get` exposed only on unique). - 9th generic `IX` on `Database<...>`; 4th generic `IX` on `Store<...>` / `ReadonlyStore<...>` / `TransactionContext<...>` / `TransactionDeclaration<...>`. All defaults are `{}` so existing call sites keep type-checking unchanged. - `Store.Schema.indexes` (optional) and plugin descriptor `indexes`, slotted between `archetypes` and `computed` in the property order. Auto-routing: - `db.select(include, { where })` and `db.observe.select(...)` route through a matching index when `where` is pure equality on the index's full key tuple and no order is given. Other shapes fall back to the archetype scan unchanged. Registration strictness (matches the `===` rule `combinePlugins` already enforces for components / transactions, plus one new rule): - Same name + different decl object → throws. - Different name + same shape → throws (almost always an unintentional duplicate; error names both indexes so the author can pick one). Architecture: - `Core` stays untouched; the Store layer owns the `IndexRegistry` and wraps `archetype.insert` / `store.update` / `store.delete` to maintain indexes. TransactionalStore / Observed / Reconciling only forward the new `IX` generic; their runtime is unchanged because mutations flow through Store methods. - `db.indexes` is `store.indexes` by reference. Rollback in `applyWriteOperations` goes back through the same Store methods, so index undo is free. Tests: 38 new index tests (66 total in the index suite); full data package suite 2276 passing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Seed each index from its own archetypes, not every entity The previous rebuild path walked every live entity in the database via a generator over `queryArchetypes([])`, then called `store.read` per entity. That ignored what archetypes are for: an index over `["email"]` can only ever be populated by archetypes whose component set contains `email`. Every other archetype's rows are skipped at `idx.add` time anyway — but only after we already walked them and did a redundant `locate` from `read`. Now: - `createIndexRegistry` no longer takes an `EntityIterator`. `register` returns the new `RuntimeIndex` (or null on a benign identity-equal re-register) so the caller can seed it. `rebuild()` is replaced by `clear()` — the registry zeros its buckets, seeding is the store's job. - `createStore` adds `seedIndexFromArchetypes(idx)`. It calls `core.queryArchetypes(idx.components)` to narrow to the archetypes that could contribute, then reads values directly from each archetype's dense column buffers per row. No `read`, no `locate`. - `reset()` and `fromData()` call `indexRegistry.clear()` and then seed each index via the same targeted walk. Each index sees only the archetypes containing its components. Also: dropped one gratuitous cast in `create-database.ts` (`partialDatabase` is already `any`; the `as { indexes: unknown }` narrowed for an assignment that didn't need narrowing). All 2276 tests still pass; tsc -b clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add computed, multi-value, and sorted indexes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Redesign index declaration API around a unified `key` union Replaces the V1 `components`/`compute` split with a single `key` field that accepts four shapes — bare column, column tuple, function, or slot map — and an `order: { by, compare? }` for within-bucket sorting. `get` now returns `Entity | null` (known absent), never `undefined`. - `IndexKey<C>` constrains the `key` declaration to columns of C; the inlined `IndexDeclarations` shape makes typo-keys a compile error. - `findRange` accepts comparison-operator filters (`<,<=,>,>=,==,!=`) on the bucket key, scalar or per-field. - Auto-router dispatches a raw-equality `where` through the matching index's `handle.find` (spies + instrumentation see the call). - README catalogues all 12 patterns with their handle signatures. - 92 runtime tests + 2 type-only proof files exercise every shape. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Defer within-bucket sorting to first read after writes Sorted indexes now append on insert and mark the bucket dirty; the first find/findRange that touches a dirty bucket pays one O(n log n) sort, subsequent reads are free until the next write. Optimizes the batched- write-then-read workload from O(k·n) to O(k + n log n) per batch of `k` inserts. Public interface unchanged. Bucket-key Map stays eager so the uniqueness pre-check fires before any column mutation and t.indexes.x.get(...) inside a transaction still sees just-written rows. - Strengthen unique-conflict tests to snapshot store + index pre-throw and assert no drift after rollback. - New database.index.performance.test.ts: nine inserts-are-linear-in-N tests across every catalogue shape, plus a "second read is free" check. - README: per-insert and read-after-write big-O tables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Make single-entity index writes O(1) in bucket size Non-unique bucket payload is now Set<Entity> instead of Entity[]. Removes the arr.indexOf scan from remove/update-with-bucket-change, so a single- row write touches O(b) buckets at O(1) each — never proportional to bucket size or total entities in the index. For sorted indexes, the materialized + sorted array is cached per bucket and dropped on any write touching that bucket. First read after writes pays the O(n log n) sort once; subsequent reads slice the cached array. Red/green: three new perf tests in database.index.performance.test.ts under "Single-entity write is O(1) in bucket size" — non-unique delete, update-with-bucket-change, sorted-index delete. Each compares 4k vs 40k bucket and asserts ratio < 3×. All three failed with array-based storage, pass after the Set refactor. They stay in the suite to catch regressions (any reintroduction of arr.indexOf in the bucket hot path makes them red again). Insertion tests get warmThenMeasure to absorb JIT cold-start variance when running alongside the full repo suite; QUADRATIC_FLOOR lifted to 5× to still catch real O(n²) but tolerate noise on size-doubling tests. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 851f7cf commit 6958d00

18 files changed

Lines changed: 3753 additions & 57 deletions

packages/data/src/ecs/README.md

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,203 @@ const worldPlugin = Database.Plugin.create({
135135
});
136136
```
137137

138+
## Indexes
139+
140+
Indexes give O(1) lookup by some derived or column-valued key. Declare them on the plugin alongside components and archetypes; the runtime maintains them automatically on every insert/update/delete and exposes typed lookup handles at `db.indexes.<name>` and `t.indexes.<name>` (inside transactions).
141+
142+
### Declaration shape
143+
144+
| Field | Required | Shape |
145+
|---|---|---|
146+
| `key` | yes | `string``string[]``(args) => Value``{ slot: string ∣ (args) => Value }` |
147+
| `order` | no | `{ by: string[]; compare?: (a, b) => number }` |
148+
| `unique` | no | `boolean` — when `true`, exposes `get(key) → Entity \| null` |
149+
| `components` | only when an extractor function reads a column not already implied by an identity string in `key`/`order` | `string[]` |
150+
151+
`find(key) → readonly Entity[]` returns every entity in the matching bucket (sorted if `order` is declared). `get(key) → Entity | null` is exposed only on unique indexes; `null` means "we know this key has no entity," never `undefined`. Array values (a `T[]` column, or a `compute` return that is `T[]`) auto-fan-out into one bucket entry per element.
152+
153+
### Pattern catalogue
154+
155+
#### Raw indexes (identity reads from columns)
156+
157+
**Single-column unique lookup**
158+
```ts
159+
indexes: {
160+
byEmail: { key: "email", unique: true },
161+
}
162+
// db.indexes.byEmail.get("alice@x.com") → Entity | null
163+
```
164+
165+
**Multi-column compound unique**`MappedChildOf` style when both parts are top-level columns:
166+
```ts
167+
indexes: {
168+
playerSlot: { key: ["team", "position"], unique: true },
169+
}
170+
// db.indexes.playerSlot.get({ team: T, position: "qb" }) → Entity | null
171+
```
172+
173+
**Non-unique by single column**`ChildOf` style:
174+
```ts
175+
indexes: {
176+
childrenOf: { key: "parent" },
177+
}
178+
// db.indexes.childrenOf.find(P) → readonly Entity[]
179+
```
180+
181+
**Sorted children**`OrderedChildOf` style with top-level columns:
182+
```ts
183+
indexes: {
184+
orderedChildrenOf: { key: "parent", order: { by: ["fractIndex"] } },
185+
}
186+
// db.indexes.orderedChildrenOf.find(P) → readonly Entity[] // sorted by fractIndex
187+
```
188+
189+
**Multi-value (array column)** — per-element fan-out is automatic:
190+
```ts
191+
indexes: {
192+
tasksByAssignee: { key: "assigned" }, // assigned: string[]
193+
}
194+
// db.indexes.tasksByAssignee.find("joe") → all tasks where assigned includes "joe"
195+
```
196+
197+
#### Computed indexes (function-derived)
198+
199+
**Scalar derived key** (case-insensitive lookup):
200+
```ts
201+
indexes: {
202+
byEmailCi: { components: ["email"], key: (email) => email.toLowerCase() },
203+
}
204+
// db.indexes.byEmailCi.find("ALICE@x.com") → readonly Entity[]
205+
```
206+
207+
**Multi-value computed**`compute` returns an array, each element becomes a bucket entry:
208+
```ts
209+
indexes: {
210+
docsByKeyword: { components: ["body"], key: (body) => extractTags(body) },
211+
}
212+
// db.indexes.docsByKeyword.find("typescript")
213+
```
214+
215+
**Compound key from nested data**`MappedChildOf` when the relationship data lives in one nested component:
216+
```ts
217+
// player: { parent: Team, key: Position }
218+
indexes: {
219+
playerByRoster: {
220+
components: ["player"],
221+
key: { team: (p) => p.parent, position: (p) => p.key },
222+
unique: true,
223+
},
224+
}
225+
// db.indexes.playerByRoster.get({ team: T, position: "qb" }) → Entity | null
226+
```
227+
228+
**Sorted from nested data**`OrderedChildOf` with nested struct:
229+
```ts
230+
// foo: { parent: Entity, order: FractionalIndex }
231+
indexes: {
232+
orderedChildrenOfFoo: {
233+
components: ["foo"],
234+
key: (f) => f.parent,
235+
order: {
236+
by: ["foo"],
237+
compare: (a, b) => a.foo.order < b.foo.order ? -1 : 1,
238+
},
239+
},
240+
}
241+
// db.indexes.orderedChildrenOfFoo.find(T) → readonly Entity[] // sorted by foo.order
242+
```
243+
244+
#### Combined / advanced
245+
246+
**Mixed identity + derived parts in one compound key**:
247+
```ts
248+
indexes: {
249+
playerByTeamRole: {
250+
components: ["player"],
251+
key: {
252+
team: "team", // identity from top-level `team` column
253+
role: (p) => p.position, // derived from nested player.position
254+
},
255+
unique: true,
256+
},
257+
}
258+
// db.indexes.playerByTeamRole.get({ team: T, role: "qb" }) → Entity | null
259+
```
260+
261+
**Computed mapped *and* sorted** — full `SortedMappedChildOf`:
262+
```ts
263+
indexes: {
264+
orderedRoster: {
265+
components: ["item"],
266+
key: { team: (i) => i.parent, role: (i) => i.key },
267+
order: {
268+
by: ["item"],
269+
compare: (a, b) => a.item.fractIndex.localeCompare(b.item.fractIndex),
270+
},
271+
unique: true,
272+
},
273+
}
274+
// db.indexes.orderedRoster.get({ team: T, role: "qb" }) → Entity | null
275+
```
276+
277+
**Custom comparator** — descending, mixed direction, locale-aware, semver, etc.:
278+
```ts
279+
indexes: {
280+
tasksByPriority: {
281+
key: "owner",
282+
order: {
283+
by: ["priority", "due"],
284+
compare: (a, b) => b.priority - a.priority || a.due - b.due, // priority desc, due asc
285+
},
286+
},
287+
}
288+
// db.indexes.tasksByPriority.find(ownerEntity) → readonly Entity[] // ordered per comparator
289+
```
290+
291+
### Order semantics
292+
293+
- `order` is always a single object: `{ by, compare? }`. `by` declares which columns are read into the per-entity sort cache; `compare` (if present) is the comparator over `Pick<C, by>`. When `compare` is omitted, the default is ascending across `by` left-to-right with positional tie-break.
294+
- All direction control happens through the comparator — there are no `asc: true | false` booleans. Descending, mixed direction, case-insensitive, semver, and natural-sort comparators are all written the same way.
295+
296+
### Auto-routing of `select`
297+
298+
When `db.select(include, { where })` or `db.observe.select(...)` is called with a `where` clause that exactly matches the `key` of a declared raw index by equality, the query is served from the index instead of scanning archetypes. No code-site change required — declare the index and the planner picks it up. Other query shapes fall through to the archetype scan unchanged.
299+
300+
### Maintenance and atomicity
301+
302+
- Indexes are maintained *eagerly* per mutation, so `t.indexes.<name>` inside a transaction sees rows the transaction has just written.
303+
- Unique constraints are pre-checked before any column mutation. A conflict throws *from the offending insert/update call*; the existing transaction rollback path restores both the store and the index together. No partial mutation lands in either the store or the index.
304+
305+
### Performance characteristics
306+
307+
Index maintenance is **O(b)** per change, where `b` is the number of buckets the affected entity occupies (usually 1, more for multi-value fan-out). It is *never* proportional to total entities in the index or to a bucket's size. Sorting (when an `order` is declared) is deferred to the first read that touches a dirty bucket.
308+
309+
| Declaration | Per-insert cost | Per-delete cost | Notes |
310+
|---|---|---|---|
311+
| `key: "col"` (non-unique) | `O(1)` | `O(1)` | `Set<Entity>` per bucket. |
312+
| `key: "col"`, `unique: true` | `O(1)` | `O(1)` | Map set; throws on duplicate. |
313+
| `key: ["a", "b", …]` | `O(k)` where `k` = key arity | `O(k)` | One serialized key per row. |
314+
| `key: (...) => v` (scalar) | `O(1 + compute)` | `O(1)` | Compute runs once per row. |
315+
| `key: "arr"` where `arr: T[]` | `O(m)` where `m` = array length | `O(m)` | One bucket per array element. |
316+
| `key: (...) => T[]` (multi) | `O(m + compute)` | `O(m)` | Same fan-out, derived. |
317+
| `key: { slot: ..., ... }` | `O(s + Π array sizes)` | `O(s + Π array sizes)` | `s` = slot count; cartesian fan-out across array-valued slots. |
318+
| `+ order: { by, compare? }` | adds `O(b)` for the snapshot | adds `O(b)` for cache invalidation. No comparator calls. |
319+
320+
The crucial property: removing one entity from a non-unique bucket holding `N` entities is `O(1)`, not `O(N)`. Non-unique buckets are stored as `Set<Entity>` so `Set.delete` is the hot path — no `arr.indexOf` scan. Verified by `database.index.performance.test.ts`: deleting from a 40 000-entity bucket has the same per-delete cost as deleting from a 4 000-entity bucket.
321+
322+
Reads pay one catch-up sort the first time they touch a dirty bucket:
323+
324+
| Read | First call after writes | Subsequent calls |
325+
|---|---|---|
326+
| `find(key)` (unsorted) | `O(1)` Map lookup + `O(n)` slice | same |
327+
| `find(key)` (sorted, dirty) | `O(n log n)` sort, then slice | `O(1)` Map lookup + `O(n)` slice |
328+
| `findRange(filter)` | `O(B)` walk + per-matched-bucket sort if dirty | per-matched-bucket sort only on first read after a write |
329+
| `get(key)` (unique) | `O(1)` Map lookup | same |
330+
331+
`B` = total bucket count for the index. A `find` against a bucket that has not received any writes since the previous `find` is free (clean buckets aren't re-sorted).
332+
333+
**Unique-conflict timing:** the conflict check runs *before* the column store mutates. The throw originates from the insert/update call; the rollback path restores any state the transaction touched. Verified by `database.index.test.ts` ("unique conflict on insert is caught up-front — no partial store or index mutation") and `database.index.performance.test.ts`.
334+
138335
## Transactions
139336

140337
Transactions are synchronous, deterministic functions that mutate the store. They automatically produce undo/redo operations and notify observers.

packages/data/src/ecs/database/combine-plugins.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,16 @@ export type CombinePlugins<Plugins extends readonly Database.Plugin[]> = Databas
2626
>,
2727
{} & IntersectAll<{ [K in keyof Plugins]: Plugins[K]['actions'] }>,
2828
{} & IntersectAll<{ [K in keyof Plugins]: Plugins[K]['services'] }>,
29-
{} & IntersectAll<{ [K in keyof Plugins]: Plugins[K]['computed'] }>
29+
{} & IntersectAll<{ [K in keyof Plugins]: Plugins[K]['computed'] }>,
30+
{} & IntersectAll<{ [K in keyof Plugins]: Plugins[K]['indexes'] }>
3031
>;
3132

3233

3334
/**
3435
* Combines multiple plugins into a single plugin.
35-
* All plugin properties (components, resources, archetypes, computed, transactions, systems, actions, services)
36+
* All plugin properties (components, resources, archetypes, indexes, computed, transactions, systems, actions, services)
3637
* require identity (===) when the same key exists across plugins.
37-
*
38+
*
3839
* IMPORTANT: Services are merged in order, preserving the initialization order
3940
* so that extended plugin services are initialized before current plugin services.
4041
*/
@@ -48,9 +49,10 @@ export function combinePlugins<
4849
Extract<UnionAll<{ [K in keyof Plugins]: StringKeyof<Plugins[K]['systems']> }>, string>,
4950
{} & IntersectAll<{ [K in keyof Plugins]: Plugins[K]['actions'] }>,
5051
{} & IntersectAll<{ [K in keyof Plugins]: Plugins[K]['services'] }>,
51-
{} & IntersectAll<{ [K in keyof Plugins]: Plugins[K]['computed'] }>
52+
{} & IntersectAll<{ [K in keyof Plugins]: Plugins[K]['computed'] }>,
53+
{} & IntersectAll<{ [K in keyof Plugins]: Plugins[K]['indexes'] }>
5254
> {
53-
const keys = ['services', 'components', 'resources', 'archetypes', 'computed', 'transactions', 'actions', 'systems'] as const;
55+
const keys = ['services', 'components', 'resources', 'archetypes', 'indexes', 'computed', 'transactions', 'actions', 'systems'] as const;
5456

5557
const merge = (base: any, next: any) =>
5658
Object.fromEntries(keys.map(key => {
@@ -70,7 +72,7 @@ export function combinePlugins<
7072
return [key, merged];
7173
}));
7274

73-
const emptyPlugin = { components: {}, resources: {}, archetypes: {}, computed: {}, transactions: {}, actions: {}, systems: {}, services: {} };
75+
const emptyPlugin = { components: {}, resources: {}, archetypes: {}, indexes: {}, computed: {}, transactions: {}, actions: {}, systems: {}, services: {} };
7476

7577
// Merge all plugins together
7678
const result = plugins.reduce(merge, emptyPlugin);

packages/data/src/ecs/database/create-plugin.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// © 2026 Adobe. MIT License. See /LICENSE for details.
22

3-
import { Database, SystemFunction, ServiceFactories, FromServiceFactories, FromComputedFactories, type PluginComputedFactories } from "./database.js";
3+
import { Database, SystemFunction, ServiceFactories, FromServiceFactories, FromComputedFactories, type PluginComputedFactories, type IndexDeclarations } from "./database.js";
44
import type { ComponentSchemas } from "../component-schemas.js";
55
import type { ResourceSchemas } from "../resource-schemas.js";
66
import type { ArchetypeComponents } from "../store/archetype-components.js";
@@ -11,6 +11,7 @@ import type { StringKeyof, NoInfer, RemoveIndex } from "../../types/types.js";
1111
import { combinePlugins } from "./combine-plugins.js";
1212
import { Store } from "../store/store.js";
1313

14+
1415
/**
1516
* Direct-intersection return type for createPlugin.
1617
*
@@ -20,7 +21,7 @@ import { Store } from "../store/store.js";
2021
*/
2122
type CreatePluginResult<
2223
XP extends Database.Plugin,
23-
CS, RS, A, TD, S extends string, AD, SVF, CVF
24+
CS, RS, A, TD, S extends string, AD, SVF, CVF, IX
2425
> = Database.Plugin<
2526
XP['components'] & CS,
2627
XP['resources'] & RS,
@@ -29,7 +30,8 @@ type CreatePluginResult<
2930
S | StringKeyof<XP['systems']>,
3031
XP['actions'] & AD,
3132
XP['services'] & SVF,
32-
XP['computed'] & CVF
33+
XP['computed'] & CVF,
34+
XP['indexes'] & IX
3335
>;
3436

3537
/**
@@ -49,7 +51,7 @@ type DatabaseWithServices<
4951
>;
5052

5153
function validatePropertyOrder(plugins: Record<string, unknown>): void {
52-
const expectedOrder = ['extends', 'services', 'components', 'resources', 'archetypes', 'computed', 'transactions', 'actions', 'systems'];
54+
const expectedOrder = ['extends', 'services', 'components', 'resources', 'archetypes', 'indexes', 'computed', 'transactions', 'actions', 'systems'];
5355
const actualKeys = Object.keys(plugins);
5456
const definedKeys = actualKeys.filter(key => key in plugins);
5557

@@ -84,11 +86,12 @@ function validatePropertyOrder(plugins: Record<string, unknown>): void {
8486
* 3. components (optional) - Component schema definitions
8587
* 4. resources (optional) - Resource schema definitions
8688
* 5. archetypes (optional) - Archetype definitions
87-
* 6. computed (optional) - Computed observe factories (each returns Observe<unknown>)
88-
* 7. transactions (optional) - Transaction declarations
89-
* 8. actions (optional) - Action declarations
90-
* 9. systems (optional) - System declarations
91-
*
89+
* 6. indexes (optional) - Index declarations over components
90+
* 7. computed (optional) - Computed observe factories (each returns Observe<unknown>)
91+
* 8. transactions (optional) - Transaction declarations
92+
* 9. actions (optional) - Action declarations
93+
* 10. systems (optional) - System declarations
94+
*
9295
* Example:
9396
* ```ts
9497
* Database.Plugin.create({
@@ -99,10 +102,11 @@ function validatePropertyOrder(plugins: Record<string, unknown>): void {
99102
* components: { ... }, // 3. components
100103
* resources: { ... }, // 4. resources
101104
* archetypes: { ... }, // 5. archetypes
102-
* computed: { ... }, // 6. computed
103-
* transactions: { ... }, // 7. transactions
104-
* actions: { ... }, // 8. actions
105-
* systems: { ... }, // 9. systems
105+
* indexes: { ... }, // 6. indexes
106+
* computed: { ... }, // 7. computed
107+
* transactions: { ... }, // 8. transactions
108+
* actions: { ... }, // 9. actions
109+
* systems: { ... }, // 10. systems
106110
* })
107111
* ```
108112
*
@@ -131,11 +135,12 @@ type FullDBForPlugin<
131135
>;
132136

133137
export function createPlugin<
134-
const XP extends Database.Plugin<{}, {}, {}, {}, never, {}, {}, {}>,
138+
const XP extends Database.Plugin<{}, {}, {}, {}, never, {}, {}, {}, {}>,
135139
const CS extends ComponentSchemas,
136140
const RS extends ResourceSchemas,
137141
const A extends ArchetypeComponents<StringKeyof<RemoveIndex<CS> & XP['components']>>,
138-
const TD extends TransactionDeclarations<FromSchemas<RemoveIndex<CS> & XP['components']>, FromSchemas<RemoveIndex<RS> & XP['resources']>, RemoveIndex<A> & XP['archetypes']>,
142+
const IX extends IndexDeclarations<FromSchemas<RemoveIndex<CS> & XP['components']>>,
143+
const TD extends TransactionDeclarations<FromSchemas<RemoveIndex<CS> & XP['components']>, FromSchemas<RemoveIndex<RS> & XP['resources']>, RemoveIndex<A> & XP['archetypes'], RemoveIndex<IX> & XP['indexes']>,
139144
const AD,
140145
const S extends string = never,
141146
const SVF extends ServiceFactories<Database.FromPlugin<XP>> = {},
@@ -149,6 +154,7 @@ export function createPlugin<
149154
components?: CS,
150155
resources?: RS,
151156
archetypes?: A,
157+
indexes?: IX,
152158
computed?: CVF & PluginComputedFactories<FullDBForPlugin<RemoveIndex<CS>, RemoveIndex<RS>, RemoveIndex<A>, {}, string, RemoveIndex<AD> & XP['actions'], XP, RemoveIndex<SVF>>>,
153159
transactions?: TD,
154160
actions?: AD & {
@@ -160,7 +166,8 @@ export function createPlugin<
160166
S | StringKeyof<XP['systems']>,
161167
ToActionFunctions<XP['actions']>,
162168
FromServiceFactories<RemoveIndex<SVF> & XP['services']>,
163-
FromComputedFactories<RemoveIndex<CVF> & XP['computed']>
169+
FromComputedFactories<RemoveIndex<CVF> & XP['computed']>,
170+
RemoveIndex<IX> & XP['indexes']
164171
>, input?: any) => any
165172
}
166173
systems?: { readonly [K in S]: {
@@ -172,7 +179,8 @@ export function createPlugin<
172179
S | StringKeyof<XP['systems']>,
173180
ToActionFunctions<RemoveIndex<AD> & XP['actions']>,
174181
FromServiceFactories<RemoveIndex<SVF> & XP['services']>,
175-
FromComputedFactories<RemoveIndex<CVF> & XP['computed']>
182+
FromComputedFactories<RemoveIndex<CVF> & XP['computed']>,
183+
RemoveIndex<IX> & XP['indexes']
176184
> & {
177185
readonly store: Store<
178186
FromSchemas<RemoveIndex<CS> & XP['components']>,
@@ -189,7 +197,7 @@ export function createPlugin<
189197
}
190198
},
191199
},
192-
): CreatePluginResult<XP, RemoveIndex<CS>, RemoveIndex<RS>, RemoveIndex<A>, RemoveIndex<TD>, S, AD, RemoveIndex<SVF>, RemoveIndex<CVF>> {
200+
): CreatePluginResult<XP, RemoveIndex<CS>, RemoveIndex<RS>, RemoveIndex<A>, RemoveIndex<TD>, S, AD, RemoveIndex<SVF>, RemoveIndex<CVF>, RemoveIndex<IX>> {
193201
validatePropertyOrder(plugins);
194202

195203
// Normalize plugins descriptor to a plugin object in correct order
@@ -198,6 +206,7 @@ export function createPlugin<
198206
components: plugins.components ?? {},
199207
resources: plugins.resources ?? {},
200208
archetypes: plugins.archetypes ?? {},
209+
indexes: plugins.indexes ?? {},
201210
computed: plugins.computed ?? {},
202211
transactions: plugins.transactions ?? {},
203212
actions: plugins.actions ?? {},

0 commit comments

Comments
 (0)