@adobe/data 0.9.62 → 0.9.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/ecs/database/create-plugin.d.ts +1 -1
  2. package/dist/ecs/database/database.index.performance.test.js +5 -5
  3. package/dist/ecs/database/database.index.performance.test.js.map +1 -1
  4. package/dist/ecs/database/database.index.test.js +193 -68
  5. package/dist/ecs/database/database.index.test.js.map +1 -1
  6. package/dist/ecs/database/database.index.type-test.js +34 -10
  7. package/dist/ecs/database/database.index.type-test.js.map +1 -1
  8. package/dist/ecs/database/index-registry/create-index-registry.d.ts +38 -9
  9. package/dist/ecs/database/index-registry/create-index-registry.js +67 -20
  10. package/dist/ecs/database/index-registry/create-index-registry.js.map +1 -1
  11. package/dist/ecs/database/index-registry/create-index.d.ts +10 -2
  12. package/dist/ecs/database/index-registry/create-index.js +44 -47
  13. package/dist/ecs/database/index-registry/create-index.js.map +1 -1
  14. package/dist/ecs/database/public/create-database.js +5 -7
  15. package/dist/ecs/database/public/create-database.js.map +1 -1
  16. package/dist/ecs/store/core/select-entities.js +6 -3
  17. package/dist/ecs/store/core/select-entities.js.map +1 -1
  18. package/dist/ecs/store/index-types.d.ts +43 -17
  19. package/dist/ecs/store/public/create-store.js +15 -5
  20. package/dist/ecs/store/public/create-store.js.map +1 -1
  21. package/dist/ecs/store/store.d.ts +2 -2
  22. package/dist/functions/compare.d.ts +13 -0
  23. package/dist/functions/compare.js +15 -0
  24. package/dist/functions/compare.js.map +1 -0
  25. package/dist/functions/compare.test.d.ts +1 -0
  26. package/dist/functions/compare.test.js +24 -0
  27. package/dist/functions/compare.test.js.map +1 -0
  28. package/dist/functions/index.d.ts +1 -0
  29. package/dist/functions/index.js +1 -0
  30. package/dist/functions/index.js.map +1 -1
  31. package/dist/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +1 -1
  33. package/references/data-lit/package.json +1 -1
  34. package/references/data-lit-tictactoe/package.json +1 -1
  35. package/references/data-react/package.json +1 -1
  36. package/references/data-react-hello/package.json +1 -1
  37. package/references/data-react-pixie/package.json +1 -1
@@ -21,17 +21,17 @@ describe("Pattern 1 — single-column unique lookup (byEmail)", () => {
21
21
  it("get returns the entity for a known scalar key", () => {
22
22
  const db = Database.create(plugin());
23
23
  const e = db.transactions.add("alice@a.com");
24
- expect(db.indexes.byEmail.get("alice@a.com")).toBe(e);
24
+ expect(db.indexes.byEmail.get({ email: "alice@a.com" })).toBe(e);
25
25
  });
26
26
  it("get returns null for an unknown key (known absent)", () => {
27
27
  const db = Database.create(plugin());
28
- expect(db.indexes.byEmail.get("missing@x.com")).toBeNull();
28
+ expect(db.indexes.byEmail.get({ email: "missing@x.com" })).toBeNull();
29
29
  });
30
30
  it("duplicate insert throws atomically — original row stays intact", () => {
31
31
  const db = Database.create(plugin());
32
32
  const first = db.transactions.add("shared@x.com");
33
33
  expect(() => db.transactions.add("shared@x.com")).toThrow(/Unique index conflict/);
34
- expect(db.indexes.byEmail.get("shared@x.com")).toBe(first);
34
+ expect(db.indexes.byEmail.get({ email: "shared@x.com" })).toBe(first);
35
35
  });
36
36
  });
37
37
  describe("Pattern 2 — multi-column compound unique (playerSlot)", () => {
@@ -84,21 +84,21 @@ describe("Pattern 3 — non-unique by single column (childrenOf)", () => {
84
84
  const a = db.transactions.add({ parent: 7, label: "a" });
85
85
  const b = db.transactions.add({ parent: 7, label: "b" });
86
86
  db.transactions.add({ parent: 9, label: "c" });
87
- expect([...db.indexes.childrenOf.find(7)].sort()).toEqual([a, b].sort());
87
+ expect([...db.indexes.childrenOf.find({ parent: 7 })].sort()).toEqual([a, b].sort());
88
88
  });
89
89
  it("delete removes the entity from the bucket", () => {
90
90
  const db = Database.create(plugin());
91
91
  const e = db.transactions.add({ parent: 7, label: "a" });
92
- expect(db.indexes.childrenOf.find(7)).toEqual([e]);
92
+ expect(db.indexes.childrenOf.find({ parent: 7 })).toEqual([e]);
93
93
  db.transactions.delete(e);
94
- expect(db.indexes.childrenOf.find(7)).toEqual([]);
94
+ expect(db.indexes.childrenOf.find({ parent: 7 })).toEqual([]);
95
95
  });
96
96
  it("update moves the entity between buckets", () => {
97
97
  const db = Database.create(plugin());
98
98
  const e = db.transactions.add({ parent: 7, label: "a" });
99
99
  db.transactions.move({ entity: e, newParent: 9 });
100
- expect(db.indexes.childrenOf.find(7)).toEqual([]);
101
- expect(db.indexes.childrenOf.find(9)).toEqual([e]);
100
+ expect(db.indexes.childrenOf.find({ parent: 7 })).toEqual([]);
101
+ expect(db.indexes.childrenOf.find({ parent: 9 })).toEqual([e]);
102
102
  });
103
103
  it("`get` is absent on non-unique handles at runtime and types", () => {
104
104
  const db = Database.create(plugin());
@@ -129,7 +129,7 @@ describe("Pattern 4 — sorted children (orderedChildrenOf)", () => {
129
129
  const c = db.transactions.add({ parent: 7, fractIndex: "c" });
130
130
  const a = db.transactions.add({ parent: 7, fractIndex: "a" });
131
131
  const b = db.transactions.add({ parent: 7, fractIndex: "b" });
132
- expect(db.indexes.orderedChildrenOf.find(7)).toEqual([a, b, c]);
132
+ expect(db.indexes.orderedChildrenOf.find({ parent: 7 })).toEqual([a, b, c]);
133
133
  });
134
134
  it("update on the sort key repositions the entity in place", () => {
135
135
  const db = Database.create(plugin());
@@ -137,9 +137,9 @@ describe("Pattern 4 — sorted children (orderedChildrenOf)", () => {
137
137
  const b = db.transactions.add({ parent: 7, fractIndex: "b" });
138
138
  const c = db.transactions.add({ parent: 7, fractIndex: "c" });
139
139
  db.transactions.move({ entity: b, fractIndex: "z" });
140
- expect(db.indexes.orderedChildrenOf.find(7)).toEqual([a, c, b]);
140
+ expect(db.indexes.orderedChildrenOf.find({ parent: 7 })).toEqual([a, c, b]);
141
141
  db.transactions.move({ entity: a, fractIndex: "d" });
142
- expect(db.indexes.orderedChildrenOf.find(7)).toEqual([c, a, b]);
142
+ expect(db.indexes.orderedChildrenOf.find({ parent: 7 })).toEqual([c, a, b]);
143
143
  });
144
144
  it("delete preserves sort order of the remaining entities", () => {
145
145
  const db = Database.create(plugin());
@@ -147,7 +147,7 @@ describe("Pattern 4 — sorted children (orderedChildrenOf)", () => {
147
147
  const b = db.transactions.add({ parent: 7, fractIndex: "b" });
148
148
  const c = db.transactions.add({ parent: 7, fractIndex: "c" });
149
149
  db.transactions.delete(b);
150
- expect(db.indexes.orderedChildrenOf.find(7)).toEqual([a, c]);
150
+ expect(db.indexes.orderedChildrenOf.find({ parent: 7 })).toEqual([a, c]);
151
151
  });
152
152
  it("populates and sorts when the index is registered after data exists", () => {
153
153
  const base = Database.Plugin.create({
@@ -171,7 +171,7 @@ describe("Pattern 4 — sorted children (orderedChildrenOf)", () => {
171
171
  },
172
172
  });
173
173
  const ext = db.extend(indexed);
174
- expect(ext.indexes.orderedChildrenOf.find(5)).toEqual([a, b, c]);
174
+ expect(ext.indexes.orderedChildrenOf.find({ parent: 5 })).toEqual([a, b, c]);
175
175
  });
176
176
  });
177
177
  describe("Pattern 4 — observe(arg): reactive sorted bucket view", () => {
@@ -200,7 +200,7 @@ describe("Pattern 4 — observe(arg): reactive sorted bucket view", () => {
200
200
  const a = db.transactions.add({ parent: 7, fractIndex: "a" });
201
201
  const b = db.transactions.add({ parent: 7, fractIndex: "b" });
202
202
  const emissions = [];
203
- const unsub = db.indexes.orderedChildrenOf.observe(7)((entities) => { emissions.push([...entities]); });
203
+ const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((entities) => { emissions.push([...entities]); });
204
204
  expect(emissions).toEqual([[a, b, c]]);
205
205
  unsub();
206
206
  });
@@ -209,7 +209,7 @@ describe("Pattern 4 — observe(arg): reactive sorted bucket view", () => {
209
209
  const a = db.transactions.add({ parent: 7, fractIndex: "a" });
210
210
  const c = db.transactions.add({ parent: 7, fractIndex: "c" });
211
211
  const emissions = [];
212
- const unsub = db.indexes.orderedChildrenOf.observe(7)((entities) => { emissions.push([...entities]); });
212
+ const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((entities) => { emissions.push([...entities]); });
213
213
  const b = db.transactions.add({ parent: 7, fractIndex: "b" });
214
214
  await Promise.resolve();
215
215
  expect(emissions[0]).toEqual([a, c]);
@@ -222,7 +222,7 @@ describe("Pattern 4 — observe(arg): reactive sorted bucket view", () => {
222
222
  const b = db.transactions.add({ parent: 7, fractIndex: "b" });
223
223
  const c = db.transactions.add({ parent: 7, fractIndex: "c" });
224
224
  const emissions = [];
225
- const unsub = db.indexes.orderedChildrenOf.observe(7)((entities) => { emissions.push([...entities]); });
225
+ const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((entities) => { emissions.push([...entities]); });
226
226
  expect(emissions[0]).toEqual([a, b, c]);
227
227
  // Reorder-only transaction: parent (the bucket key) is untouched, so a
228
228
  // `where`-only observe.select would never see this. The index does.
@@ -236,7 +236,7 @@ describe("Pattern 4 — observe(arg): reactive sorted bucket view", () => {
236
236
  const a = db.transactions.add({ parent: 7, fractIndex: "a" });
237
237
  const b = db.transactions.add({ parent: 7, fractIndex: "b" });
238
238
  const emissions = [];
239
- const unsub = db.indexes.orderedChildrenOf.observe(7)((entities) => { emissions.push([...entities]); });
239
+ const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((entities) => { emissions.push([...entities]); });
240
240
  db.transactions.delete(b);
241
241
  await Promise.resolve();
242
242
  expect(emissions[emissions.length - 1]).toEqual([a]);
@@ -247,7 +247,7 @@ describe("Pattern 4 — observe(arg): reactive sorted bucket view", () => {
247
247
  db.transactions.add({ parent: 7, fractIndex: "a" });
248
248
  const other = db.transactions.add({ parent: 9, fractIndex: "a" });
249
249
  const emissions = [];
250
- const unsub = db.indexes.orderedChildrenOf.observe(7)((entities) => { emissions.push([...entities]); });
250
+ const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((entities) => { emissions.push([...entities]); });
251
251
  expect(emissions).toHaveLength(1);
252
252
  // A child of a *different* parent moves. Same component (`fractIndex`)
253
253
  // changes, so the observer is woken and recomputes — but its own
@@ -263,8 +263,8 @@ describe("Pattern 4 — observe(arg): reactive sorted bucket view", () => {
263
263
  const m = db.transactions.add({ parent: 7, fractIndex: "b" });
264
264
  const fromSeven = [];
265
265
  const toNine = [];
266
- const unsub7 = db.indexes.orderedChildrenOf.observe(7)((e) => fromSeven.push([...e]));
267
- const unsub9 = db.indexes.orderedChildrenOf.observe(9)((e) => toNine.push([...e]));
266
+ const unsub7 = db.indexes.orderedChildrenOf.observe({ parent: 7 })((e) => fromSeven.push([...e]));
267
+ const unsub9 = db.indexes.orderedChildrenOf.observe({ parent: 9 })((e) => toNine.push([...e]));
268
268
  db.transactions.reparent({ entity: m, parent: 9 });
269
269
  await Promise.resolve();
270
270
  expect(fromSeven[fromSeven.length - 1]).toEqual([a]);
@@ -276,7 +276,7 @@ describe("Pattern 4 — observe(arg): reactive sorted bucket view", () => {
276
276
  const db = Database.create(plugin());
277
277
  db.transactions.add({ parent: 7, fractIndex: "a" });
278
278
  const emissions = [];
279
- const unsub = db.indexes.orderedChildrenOf.observe(7)((entities) => { emissions.push([...entities]); });
279
+ const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((entities) => { emissions.push([...entities]); });
280
280
  expect(emissions).toHaveLength(1);
281
281
  unsub();
282
282
  db.transactions.add({ parent: 7, fractIndex: "b" });
@@ -304,7 +304,7 @@ describe("observe(arg) — additional edge cases", () => {
304
304
  it("emits [] for an initially-empty bucket, then the entity once populated", async () => {
305
305
  const db = Database.create(sortedPlugin());
306
306
  const emissions = [];
307
- const unsub = db.indexes.orderedChildrenOf.observe(42)((e) => emissions.push([...e]));
307
+ const unsub = db.indexes.orderedChildrenOf.observe({ parent: 42 })((e) => emissions.push([...e]));
308
308
  expect(emissions).toEqual([[]]);
309
309
  const e = db.transactions.add({ parent: 42, fractIndex: "a" });
310
310
  await Promise.resolve();
@@ -315,7 +315,7 @@ describe("observe(arg) — additional edge cases", () => {
315
315
  const db = Database.create(sortedPlugin());
316
316
  const only = db.transactions.add({ parent: 7, fractIndex: "a" });
317
317
  const emissions = [];
318
- const unsub = db.indexes.orderedChildrenOf.observe(7)((e) => emissions.push([...e]));
318
+ const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((e) => emissions.push([...e]));
319
319
  db.transactions.delete(only);
320
320
  await Promise.resolve();
321
321
  expect(emissions[emissions.length - 1]).toEqual([]);
@@ -326,8 +326,8 @@ describe("observe(arg) — additional edge cases", () => {
326
326
  const a = db.transactions.add({ parent: 7, fractIndex: "a" });
327
327
  const first = [];
328
328
  const second = [];
329
- const unsub1 = db.indexes.orderedChildrenOf.observe(7)((e) => first.push([...e]));
330
- const unsub2 = db.indexes.orderedChildrenOf.observe(7)((e) => second.push([...e]));
329
+ const unsub1 = db.indexes.orderedChildrenOf.observe({ parent: 7 })((e) => first.push([...e]));
330
+ const unsub2 = db.indexes.orderedChildrenOf.observe({ parent: 7 })((e) => second.push([...e]));
331
331
  const b = db.transactions.add({ parent: 7, fractIndex: "b" });
332
332
  await Promise.resolve();
333
333
  expect(first[first.length - 1]).toEqual([a, b]);
@@ -339,7 +339,7 @@ describe("observe(arg) — additional edge cases", () => {
339
339
  const db = Database.create(sortedPlugin());
340
340
  const a = db.transactions.add({ parent: 7, fractIndex: "a" });
341
341
  const emissions = [];
342
- const unsub = db.indexes.orderedChildrenOf.observe(7)((e) => emissions.push([...e]));
342
+ const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((e) => emissions.push([...e]));
343
343
  // Three synchronous transactions, no awaits in between.
344
344
  const c = db.transactions.add({ parent: 7, fractIndex: "c" });
345
345
  const b = db.transactions.add({ parent: 7, fractIndex: "b" });
@@ -354,7 +354,7 @@ describe("observe(arg) — additional edge cases", () => {
354
354
  const db = Database.create(sortedPlugin());
355
355
  const a = db.transactions.add({ parent: 7, fractIndex: "a" });
356
356
  const emissions = [];
357
- const unsub = db.indexes.orderedChildrenOf.observe(7)((e) => emissions.push([...e]));
357
+ const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((e) => emissions.push([...e]));
358
358
  // Move the only child to a new sort key and back within the same
359
359
  // microtask window: the bucket is woken but its final sequence is
360
360
  // identical, so no second emission.
@@ -368,7 +368,7 @@ describe("observe(arg) — additional edge cases", () => {
368
368
  const db = Database.create(sortedPlugin());
369
369
  const a = db.transactions.add({ parent: 7, fractIndex: "a" });
370
370
  const emissions = [];
371
- const unsub = db.indexes.childrenOf.observe(7)((e) => emissions.push([...e].sort()));
371
+ const unsub = db.indexes.childrenOf.observe({ parent: 7 })((e) => emissions.push([...e].sort()));
372
372
  expect(emissions[0]).toEqual([a]);
373
373
  const b = db.transactions.add({ parent: 7, fractIndex: "b" });
374
374
  await Promise.resolve();
@@ -402,7 +402,7 @@ describe("observe(arg) — additional edge cases", () => {
402
402
  const low = db.transactions.add({ owner: 1, priority: 1, due: 100 });
403
403
  const high = db.transactions.add({ owner: 1, priority: 3, due: 50 });
404
404
  const emissions = [];
405
- const unsub = db.indexes.tasksByPriority.observe(1)((e) => emissions.push([...e]));
405
+ const unsub = db.indexes.tasksByPriority.observe({ owner: 1 })((e) => emissions.push([...e]));
406
406
  // priority desc → [high, low].
407
407
  expect(emissions[0]).toEqual([high, low]);
408
408
  db.transactions.setPriority({ entity: low, priority: 5 });
@@ -416,7 +416,7 @@ describe("observe(arg) — additional edge cases", () => {
416
416
  const early = db.transactions.add({ owner: 1, priority: 2, due: 10 });
417
417
  const late = db.transactions.add({ owner: 1, priority: 2, due: 20 });
418
418
  const emissions = [];
419
- const unsub = db.indexes.tasksByPriority.observe(1)((e) => emissions.push([...e]));
419
+ const unsub = db.indexes.tasksByPriority.observe({ owner: 1 })((e) => emissions.push([...e]));
420
420
  expect(emissions[0]).toEqual([early, late]);
421
421
  // Push `early`'s due past `late`'s — flips the tie-break order.
422
422
  db.transactions.setDue({ entity: early, due: 30 });
@@ -444,36 +444,36 @@ describe("Pattern 5 — multi-value (array column → fan-out): tasksByAssignee"
444
444
  it("each array element becomes its own bucket entry", () => {
445
445
  const db = Database.create(plugin());
446
446
  const e = db.transactions.add({ title: "ship", assigned: ["joe", "bob"] });
447
- expect(db.indexes.tasksByAssignee.find("joe")).toEqual([e]);
448
- expect(db.indexes.tasksByAssignee.find("bob")).toEqual([e]);
449
- expect(db.indexes.tasksByAssignee.find("carol")).toEqual([]);
447
+ expect(db.indexes.tasksByAssignee.find({ assigned: "joe" })).toEqual([e]);
448
+ expect(db.indexes.tasksByAssignee.find({ assigned: "bob" })).toEqual([e]);
449
+ expect(db.indexes.tasksByAssignee.find({ assigned: "carol" })).toEqual([]);
450
450
  });
451
451
  it("multiple entities sharing an element are both returned", () => {
452
452
  const db = Database.create(plugin());
453
453
  const a = db.transactions.add({ title: "A", assigned: ["joe", "bob"] });
454
454
  const b = db.transactions.add({ title: "B", assigned: ["joe", "carol"] });
455
455
  db.transactions.add({ title: "C", assigned: ["diane"] });
456
- expect([...db.indexes.tasksByAssignee.find("joe")].sort()).toEqual([a, b].sort());
456
+ expect([...db.indexes.tasksByAssignee.find({ assigned: "joe" })].sort()).toEqual([a, b].sort());
457
457
  });
458
458
  it("empty array contributes no bucket entries", () => {
459
459
  const db = Database.create(plugin());
460
460
  db.transactions.add({ title: "Unassigned", assigned: [] });
461
- expect(db.indexes.tasksByAssignee.find("joe")).toEqual([]);
461
+ expect(db.indexes.tasksByAssignee.find({ assigned: "joe" })).toEqual([]);
462
462
  });
463
463
  it("update set-diffs the bucket membership", () => {
464
464
  const db = Database.create(plugin());
465
465
  const e = db.transactions.add({ title: "T", assigned: ["joe", "bob"] });
466
466
  db.transactions.reassign({ entity: e, assigned: ["joe", "carol"] });
467
- expect(db.indexes.tasksByAssignee.find("joe")).toEqual([e]);
468
- expect(db.indexes.tasksByAssignee.find("bob")).toEqual([]);
469
- expect(db.indexes.tasksByAssignee.find("carol")).toEqual([e]);
467
+ expect(db.indexes.tasksByAssignee.find({ assigned: "joe" })).toEqual([e]);
468
+ expect(db.indexes.tasksByAssignee.find({ assigned: "bob" })).toEqual([]);
469
+ expect(db.indexes.tasksByAssignee.find({ assigned: "carol" })).toEqual([e]);
470
470
  });
471
471
  it("delete drops the entity from every bucket it was in", () => {
472
472
  const db = Database.create(plugin());
473
473
  const e = db.transactions.add({ title: "T", assigned: ["joe", "bob"] });
474
474
  db.transactions.delete(e);
475
- expect(db.indexes.tasksByAssignee.find("joe")).toEqual([]);
476
- expect(db.indexes.tasksByAssignee.find("bob")).toEqual([]);
475
+ expect(db.indexes.tasksByAssignee.find({ assigned: "joe" })).toEqual([]);
476
+ expect(db.indexes.tasksByAssignee.find({ assigned: "bob" })).toEqual([]);
477
477
  });
478
478
  });
479
479
  describe("Pattern 6 — computed scalar (byEmailCi)", () => {
@@ -481,7 +481,7 @@ describe("Pattern 6 — computed scalar (byEmailCi)", () => {
481
481
  components: { email: { type: "string" } },
482
482
  archetypes: { User: ["email"] },
483
483
  indexes: {
484
- byEmailCi: { key: (email) => email.toLowerCase(), components: ["email"] },
484
+ byEmailCi: { key: { email: (c) => c.email.toLowerCase() }, components: ["email"] },
485
485
  },
486
486
  transactions: {
487
487
  add: (t, email) => t.archetypes.User.insert({ email }),
@@ -492,23 +492,23 @@ describe("Pattern 6 — computed scalar (byEmailCi)", () => {
492
492
  it("looks up by the computed (lowercased) key", () => {
493
493
  const db = Database.create(plugin());
494
494
  const e = db.transactions.add("Alice@Example.com");
495
- expect(db.indexes.byEmailCi.find("alice@example.com")).toEqual([e]);
495
+ expect(db.indexes.byEmailCi.find({ email: "alice@example.com" })).toEqual([e]);
496
496
  // The original-case input is not a key.
497
- expect(db.indexes.byEmailCi.find("Alice@Example.com")).toEqual([]);
497
+ expect(db.indexes.byEmailCi.find({ email: "Alice@Example.com" })).toEqual([]);
498
498
  });
499
499
  it("update re-derives the key", () => {
500
500
  const db = Database.create(plugin());
501
501
  const e = db.transactions.add("alice@a.com");
502
- expect(db.indexes.byEmailCi.find("alice@a.com")).toEqual([e]);
502
+ expect(db.indexes.byEmailCi.find({ email: "alice@a.com" })).toEqual([e]);
503
503
  db.transactions.rename({ entity: e, email: "alice@b.com" });
504
- expect(db.indexes.byEmailCi.find("alice@a.com")).toEqual([]);
505
- expect(db.indexes.byEmailCi.find("alice@b.com")).toEqual([e]);
504
+ expect(db.indexes.byEmailCi.find({ email: "alice@a.com" })).toEqual([]);
505
+ expect(db.indexes.byEmailCi.find({ email: "alice@b.com" })).toEqual([e]);
506
506
  });
507
507
  it("delete removes the computed bucket entry", () => {
508
508
  const db = Database.create(plugin());
509
509
  const e = db.transactions.add("alice@a.com");
510
510
  db.transactions.delete(e);
511
- expect(db.indexes.byEmailCi.find("alice@a.com")).toEqual([]);
511
+ expect(db.indexes.byEmailCi.find({ email: "alice@a.com" })).toEqual([]);
512
512
  });
513
513
  });
514
514
  describe("Pattern 7 — multi-value computed (docsByKeyword)", () => {
@@ -517,7 +517,9 @@ describe("Pattern 7 — multi-value computed (docsByKeyword)", () => {
517
517
  archetypes: { Doc: ["body"] },
518
518
  indexes: {
519
519
  docsByKeyword: {
520
- key: (body) => body.toLowerCase().split(/\s+/).filter((s) => s.length > 0),
520
+ key: {
521
+ keyword: (c) => c.body.toLowerCase().split(/\s+/).filter((s) => s.length > 0),
522
+ },
521
523
  components: ["body"],
522
524
  },
523
525
  },
@@ -528,14 +530,14 @@ describe("Pattern 7 — multi-value computed (docsByKeyword)", () => {
528
530
  it("fans out each computed array element into its own bucket", () => {
529
531
  const db = Database.create(plugin());
530
532
  const d = db.transactions.add("the quick brown fox");
531
- expect(db.indexes.docsByKeyword.find("quick")).toEqual([d]);
532
- expect(db.indexes.docsByKeyword.find("brown")).toEqual([d]);
533
- expect(db.indexes.docsByKeyword.find("missing")).toEqual([]);
533
+ expect(db.indexes.docsByKeyword.find({ keyword: "quick" })).toEqual([d]);
534
+ expect(db.indexes.docsByKeyword.find({ keyword: "brown" })).toEqual([d]);
535
+ expect(db.indexes.docsByKeyword.find({ keyword: "missing" })).toEqual([]);
534
536
  });
535
537
  it("findRange supports operator filters on the scalar element", () => {
536
538
  const db = Database.create(plugin());
537
539
  const d = db.transactions.add("alpha beta gamma");
538
- const inRange = [...db.indexes.docsByKeyword.findRange({ ">=": "b", "<": "z" })].sort();
540
+ const inRange = [...db.indexes.docsByKeyword.findRange({ keyword: { ">=": "b", "<": "z" } })].sort();
539
541
  expect(inRange).toEqual([d]);
540
542
  });
541
543
  });
@@ -556,8 +558,8 @@ describe("Pattern 8 — compound from nested data (playerByRoster)", () => {
556
558
  indexes: {
557
559
  playerByRoster: {
558
560
  key: {
559
- team: (r) => r.team,
560
- position: (r) => r.position,
561
+ team: (c) => c.roster.team,
562
+ position: (c) => c.roster.position,
561
563
  },
562
564
  unique: true,
563
565
  components: ["roster"],
@@ -593,7 +595,7 @@ describe("Pattern 10 — mixed identity + derived slots (playerByTeamRole)", ()
593
595
  playerByTeamRole: {
594
596
  key: {
595
597
  team: "team",
596
- role: (r) => r.role,
598
+ role: (c) => c.roster.role,
597
599
  },
598
600
  unique: true,
599
601
  components: ["roster"],
@@ -649,12 +651,135 @@ describe("Pattern 12 — custom comparator (tasksByPriority)", () => {
649
651
  const e2a = db.transactions.add({ owner: 1, priority: 2, due: 10 });
650
652
  const e2b = db.transactions.add({ owner: 1, priority: 2, due: 20 });
651
653
  // Priority desc, then due asc.
652
- expect(db.indexes.tasksByPriority.find(1)).toEqual([e3, e2a, e2b, e1]);
654
+ expect(db.indexes.tasksByPriority.find({ owner: 1 })).toEqual([e3, e2a, e2b, e1]);
653
655
  });
654
656
  });
655
657
  // ============================================================================
656
658
  // Cross-cutting concerns
657
659
  // ============================================================================
660
+ describe("string ordering is by code point, never locale", () => {
661
+ const plugin = () => Database.Plugin.create({
662
+ components: { group: { type: "number" }, label: { type: "string" } },
663
+ archetypes: { Row: ["group", "label"] },
664
+ indexes: { byGroupSorted: { key: "group", order: { by: ["label"] } } },
665
+ transactions: {
666
+ add: (t, a) => t.archetypes.Row.insert(a),
667
+ },
668
+ });
669
+ it("default comparator sorts strings by ASCII code point (uppercase before lowercase)", () => {
670
+ const db = Database.create(plugin());
671
+ const a = db.transactions.add({ group: 1, label: "a" }); // 'a' = 97
672
+ const Z = db.transactions.add({ group: 1, label: "Z" }); // 'Z' = 90
673
+ const zero = db.transactions.add({ group: 1, label: "0" }); // '0' = 48
674
+ const A = db.transactions.add({ group: 1, label: "A" }); // 'A' = 65
675
+ // Code point: '0' < 'A' < 'Z' < 'a'. A locale-aware comparator would
676
+ // interleave case (lowercase 'a' before 'Z'), so this fails if the
677
+ // index ever sorts via localeCompare — the no-locale guard.
678
+ expect(db.indexes.byGroupSorted.find({ group: 1 })).toEqual([zero, A, Z, a]);
679
+ });
680
+ it("ordered db.select sorts strings by code point (not numeric subtraction, not locale)", () => {
681
+ const db = Database.create(plugin());
682
+ const a = db.transactions.add({ group: 1, label: "a" });
683
+ const Z = db.transactions.add({ group: 1, label: "Z" });
684
+ const zero = db.transactions.add({ group: 1, label: "0" });
685
+ const A = db.transactions.add({ group: 1, label: "A" });
686
+ // No `where`, so this hits the archetype-scan sort in selectEntities —
687
+ // which previously used `a - b` (NaN for strings). Now code-point.
688
+ expect(db.select(["label"], { order: { label: true } })).toEqual([zero, A, Z, a]);
689
+ });
690
+ });
691
+ describe("archetype-scoped index", () => {
692
+ // Task and Note both have `parent`; the index is scoped to Task only.
693
+ const plugin = () => Database.Plugin.create({
694
+ components: {
695
+ parent: { type: "number" },
696
+ priority: { type: "number" },
697
+ body: { type: "string" },
698
+ },
699
+ archetypes: {
700
+ Task: ["parent", "priority"],
701
+ Note: ["parent", "body"],
702
+ },
703
+ indexes: {
704
+ tasksByParent: { key: "parent", archetype: "Task" },
705
+ },
706
+ transactions: {
707
+ addTask: (t, a) => t.archetypes.Task.insert(a),
708
+ addNote: (t, a) => t.archetypes.Note.insert(a),
709
+ delete: (t, e) => t.delete(e),
710
+ },
711
+ });
712
+ it("indexes only the scoped archetype, excluding others that share the key column", () => {
713
+ const db = Database.create(plugin());
714
+ const t1 = db.transactions.addTask({ parent: 7, priority: 1 });
715
+ const t2 = db.transactions.addTask({ parent: 7, priority: 2 });
716
+ db.transactions.addNote({ parent: 7, body: "shares parent 7 but is a Note" });
717
+ // Without scoping the Note (parent === 7) would appear here too.
718
+ expect([...db.indexes.tasksByParent.find({ parent: 7 })].sort()).toEqual([t1, t2].sort());
719
+ });
720
+ it("seeds from only the scoped archetype when registered after data exists", () => {
721
+ const base = Database.Plugin.create({
722
+ components: {
723
+ parent: { type: "number" },
724
+ priority: { type: "number" },
725
+ body: { type: "string" },
726
+ },
727
+ archetypes: { Task: ["parent", "priority"], Note: ["parent", "body"] },
728
+ transactions: {
729
+ addTask: (t, a) => t.archetypes.Task.insert(a),
730
+ addNote: (t, a) => t.archetypes.Note.insert(a),
731
+ },
732
+ });
733
+ const db = Database.create(base);
734
+ const t = db.transactions.addTask({ parent: 5, priority: 1 });
735
+ db.transactions.addNote({ parent: 5, body: "note" });
736
+ const ext = db.extend(Database.Plugin.create({
737
+ extends: base,
738
+ indexes: { tasksByParent: { key: "parent", archetype: "Task" } },
739
+ }));
740
+ expect(ext.indexes.tasksByParent.find({ parent: 5 })).toEqual([t]);
741
+ });
742
+ it("throws when scoped to an unknown archetype", () => {
743
+ const bad = Database.Plugin.create({
744
+ components: { parent: { type: "number" } },
745
+ archetypes: { Task: ["parent"] },
746
+ // `archetype` typed loosely here via `as any` to reach the runtime guard.
747
+ indexes: { x: { key: "parent", archetype: "Nonexistent" } },
748
+ });
749
+ expect(() => Database.create(bad)).toThrow(/unknown archetype/i);
750
+ });
751
+ });
752
+ describe("archetype-changing update keeps indexes correct (dispatch union)", () => {
753
+ // `byTag` only applies to archetypes that have `tag`. Adding/removing the
754
+ // `tag` component moves the entity between archetypes, so the entity must
755
+ // enter / leave the index accordingly — this exercises the registry's
756
+ // union(from, to) dispatch for updates that change archetype.
757
+ const plugin = () => Database.Plugin.create({
758
+ components: { tag: { type: "string" }, name: { type: "string" } },
759
+ archetypes: { Tagged: ["tag", "name"], Named: ["name"] },
760
+ indexes: { byTag: { key: "tag" } },
761
+ transactions: {
762
+ addTagged: (t, a) => t.archetypes.Tagged.insert(a),
763
+ addNamed: (t, a) => t.archetypes.Named.insert(a),
764
+ setTag: (t, a) => t.update(a.entity, { tag: a.tag }),
765
+ removeTag: (t, e) => t.update(e, { tag: undefined }),
766
+ },
767
+ });
768
+ it("removes the entity from the index when an update drops the indexed component", () => {
769
+ const db = Database.create(plugin());
770
+ const e = db.transactions.addTagged({ tag: "x", name: "n" });
771
+ expect(db.indexes.byTag.find({ tag: "x" })).toEqual([e]);
772
+ db.transactions.removeTag(e); // Tagged -> Named: byTag no longer applies
773
+ expect(db.indexes.byTag.find({ tag: "x" })).toEqual([]);
774
+ });
775
+ it("adds the entity to the index when an update introduces the indexed component", () => {
776
+ const db = Database.create(plugin());
777
+ const e = db.transactions.addNamed({ name: "n" });
778
+ expect(db.indexes.byTag.find({ tag: "y" })).toEqual([]);
779
+ db.transactions.setTag({ entity: e, tag: "y" }); // Named -> Tagged: enters byTag
780
+ expect(db.indexes.byTag.find({ tag: "y" })).toEqual([e]);
781
+ });
782
+ });
658
783
  describe("findRange — operator filters on the bucket key", () => {
659
784
  it("filters by range operators on a tuple-keyed index", () => {
660
785
  const plugin = Database.Plugin.create({
@@ -741,7 +866,7 @@ describe("auto-routing of db.select", () => {
741
866
  archetypes: { U: ["name"] },
742
867
  indexes: {
743
868
  byLowerName: {
744
- key: (name) => name.toLowerCase(),
869
+ key: { name: (c) => c.name.toLowerCase() },
745
870
  components: ["name"],
746
871
  },
747
872
  },
@@ -963,21 +1088,21 @@ describe("t.indexes — eager maintenance inside transactions", () => {
963
1088
  transactions: {
964
1089
  add: (t, args) => t.archetypes.User.insert(args),
965
1090
  addIfNew: (t, args) => {
966
- const existing = t.indexes.uniqueByEmail.get(args.email);
1091
+ const existing = t.indexes.uniqueByEmail.get({ email: args.email });
967
1092
  if (existing !== null)
968
1093
  return existing;
969
1094
  return t.archetypes.User.insert(args);
970
1095
  },
971
1096
  renameAndLookup: (t, args) => {
972
1097
  t.update(args.entity, { name: args.newName });
973
- const found = t.indexes.byName.find(args.newName);
1098
+ const found = t.indexes.byName.find({ name: args.newName });
974
1099
  if (!found.includes(args.entity)) {
975
1100
  throw new Error("name index stale after t.update");
976
1101
  }
977
1102
  },
978
1103
  deleteAndLookup: (t, args) => {
979
1104
  t.delete(args.entity);
980
- const stillThere = t.indexes.byName.find(args.name);
1105
+ const stillThere = t.indexes.byName.find({ name: args.name });
981
1106
  if (stillThere.includes(args.entity)) {
982
1107
  throw new Error("index still references deleted entity");
983
1108
  }
@@ -996,14 +1121,14 @@ describe("t.indexes — eager maintenance inside transactions", () => {
996
1121
  const db = Database.create(plugin());
997
1122
  const e = db.transactions.add({ name: "alice", email: "a@a.com" });
998
1123
  expect(() => db.transactions.renameAndLookup({ entity: e, newName: "alex" })).not.toThrow();
999
- expect(db.indexes.byName.find("alex")).toEqual([e]);
1000
- expect(db.indexes.byName.find("alice")).toEqual([]);
1124
+ expect(db.indexes.byName.find({ name: "alex" })).toEqual([e]);
1125
+ expect(db.indexes.byName.find({ name: "alice" })).toEqual([]);
1001
1126
  });
1002
1127
  it("find does not see a row deleted earlier in the same transaction body", () => {
1003
1128
  const db = Database.create(plugin());
1004
1129
  const e = db.transactions.add({ name: "alice", email: "a@a.com" });
1005
1130
  expect(() => db.transactions.deleteAndLookup({ entity: e, name: "alice" })).not.toThrow();
1006
- expect(db.indexes.byName.find("alice")).toEqual([]);
1131
+ expect(db.indexes.byName.find({ name: "alice" })).toEqual([]);
1007
1132
  });
1008
1133
  it("unique conflict on insert is caught up-front — no partial store or index mutation", () => {
1009
1134
  const db = Database.create(plugin());
@@ -1012,11 +1137,11 @@ describe("t.indexes — eager maintenance inside transactions", () => {
1012
1137
  const beforeRows = db.select(["email"]);
1013
1138
  expect(() => db.transactions.add({ name: "alex", email: "shared@x.com" })).toThrow(/Unique index conflict/);
1014
1139
  // The unique-key bucket still points at the original entity.
1015
- expect(db.indexes.uniqueByEmail.get("shared@x.com")).toBe(first);
1140
+ expect(db.indexes.uniqueByEmail.get({ email: "shared@x.com" })).toBe(first);
1016
1141
  // No phantom row landed in either index or store. byName.find("alex")
1017
1142
  // returning [] proves the secondary index never saw the row;
1018
1143
  // db.select asserts the store itself never grew.
1019
- expect(db.indexes.byName.find("alex")).toEqual([]);
1144
+ expect(db.indexes.byName.find({ name: "alex" })).toEqual([]);
1020
1145
  const afterRows = db.select(["email"]);
1021
1146
  expect([...afterRows].sort()).toEqual([...beforeRows].sort());
1022
1147
  });
@@ -1029,8 +1154,8 @@ describe("t.indexes — eager maintenance inside transactions", () => {
1029
1154
  const e2BeforeRead = db.read(e2);
1030
1155
  expect(() => db.transactions.updateEmail({ entity: e1, newEmail: "b@b.com" })).toThrow(/Unique index conflict/);
1031
1156
  // Index untouched on both keys.
1032
- expect(db.indexes.uniqueByEmail.get("a@a.com")).toBe(e1);
1033
- expect(db.indexes.uniqueByEmail.get("b@b.com")).toBe(e2);
1157
+ expect(db.indexes.uniqueByEmail.get({ email: "a@a.com" })).toBe(e1);
1158
+ expect(db.indexes.uniqueByEmail.get({ email: "b@b.com" })).toBe(e2);
1034
1159
  // Underlying store untouched — e1's email is still "a@a.com",
1035
1160
  // proving the pre-check fired before `core.update` ran.
1036
1161
  expect(db.read(e1)).toEqual(e1BeforeRead);
@@ -1136,7 +1261,7 @@ describe("registry maintenance", () => {
1136
1261
  const e1 = db.transactions.add("alice");
1137
1262
  const e2 = db.transactions.add("alice");
1138
1263
  const ext = db.extend(indexed);
1139
- expect([...ext.indexes.byName.find("alice")].sort()).toEqual([e1, e2].sort());
1264
+ expect([...ext.indexes.byName.find({ name: "alice" })].sort()).toEqual([e1, e2].sort());
1140
1265
  });
1141
1266
  });
1142
1267
  //# sourceMappingURL=database.index.test.js.map