@adobe/data 0.9.61 → 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 (97) hide show
  1. package/dist/ecs/archetype/archetype.d.ts +9 -2
  2. package/dist/ecs/archetype/archetype.js.map +1 -1
  3. package/dist/ecs/archetype/create-archetype.js +5 -2
  4. package/dist/ecs/archetype/create-archetype.js.map +1 -1
  5. package/dist/ecs/database/concurrency/concurrency-strategy.d.ts +86 -0
  6. package/dist/ecs/database/concurrency/concurrency-strategy.js +3 -0
  7. package/dist/ecs/database/concurrency/concurrency-strategy.js.map +1 -0
  8. package/dist/ecs/database/concurrency/immediate-concurrency.d.ts +22 -0
  9. package/dist/ecs/database/concurrency/immediate-concurrency.js +74 -0
  10. package/dist/ecs/database/concurrency/immediate-concurrency.js.map +1 -0
  11. package/dist/ecs/database/concurrency/index.d.ts +4 -0
  12. package/dist/ecs/database/concurrency/index.js +5 -0
  13. package/dist/ecs/database/concurrency/index.js.map +1 -0
  14. package/dist/ecs/database/concurrency/rebase-replay-concurrency.d.ts +20 -0
  15. package/dist/ecs/database/concurrency/rebase-replay-concurrency.js +34 -0
  16. package/dist/ecs/database/concurrency/rebase-replay-concurrency.js.map +1 -0
  17. package/dist/ecs/database/concurrency/roll-forward-concurrency.d.ts +33 -0
  18. package/dist/ecs/database/concurrency/roll-forward-concurrency.js +115 -0
  19. package/dist/ecs/database/concurrency/roll-forward-concurrency.js.map +1 -0
  20. package/dist/ecs/database/concurrency/roll-forward-concurrency.test.d.ts +1 -0
  21. package/dist/ecs/database/concurrency/roll-forward-concurrency.test.js +136 -0
  22. package/dist/ecs/database/concurrency/roll-forward-concurrency.test.js.map +1 -0
  23. package/dist/ecs/database/create-plugin.d.ts +1 -1
  24. package/dist/ecs/database/database.d.ts +29 -7
  25. package/dist/ecs/database/database.index.performance.test.js +5 -5
  26. package/dist/ecs/database/database.index.performance.test.js.map +1 -1
  27. package/dist/ecs/database/database.index.test.js +556 -51
  28. package/dist/ecs/database/database.index.test.js.map +1 -1
  29. package/dist/ecs/database/database.index.type-test.js +34 -10
  30. package/dist/ecs/database/database.index.type-test.js.map +1 -1
  31. package/dist/ecs/database/database.js.map +1 -1
  32. package/dist/ecs/database/database.reset.test.js +2 -1
  33. package/dist/ecs/database/database.reset.test.js.map +1 -1
  34. package/dist/ecs/database/index-registry/create-index-registry.d.ts +38 -9
  35. package/dist/ecs/database/index-registry/create-index-registry.js +67 -20
  36. package/dist/ecs/database/index-registry/create-index-registry.js.map +1 -1
  37. package/dist/ecs/database/index-registry/create-index.d.ts +10 -2
  38. package/dist/ecs/database/index-registry/create-index.js +44 -47
  39. package/dist/ecs/database/index-registry/create-index.js.map +1 -1
  40. package/dist/ecs/database/index.d.ts +1 -0
  41. package/dist/ecs/database/index.js +1 -0
  42. package/dist/ecs/database/index.js.map +1 -1
  43. package/dist/ecs/database/observe-index-entities.d.ts +26 -0
  44. package/dist/ecs/database/observe-index-entities.js +66 -0
  45. package/dist/ecs/database/observe-index-entities.js.map +1 -0
  46. package/dist/ecs/database/observed/create-observed-database.js +1 -1
  47. package/dist/ecs/database/observed/create-observed-database.js.map +1 -1
  48. package/dist/ecs/database/observed/observed-database.d.ts +1 -1
  49. package/dist/ecs/database/public/create-database.d.ts +12 -27
  50. package/dist/ecs/database/public/create-database.js +131 -37
  51. package/dist/ecs/database/public/create-database.js.map +1 -1
  52. package/dist/ecs/database/public/create-database.test.js +8 -7
  53. package/dist/ecs/database/public/create-database.test.js.map +1 -1
  54. package/dist/ecs/database/public/create-transaction-dispatcher.d.ts +4 -2
  55. package/dist/ecs/database/public/create-transaction-dispatcher.js +2 -8
  56. package/dist/ecs/database/public/create-transaction-dispatcher.js.map +1 -1
  57. package/dist/ecs/database/reconciling/create-rebase-replay-applier.d.ts +26 -0
  58. package/dist/ecs/database/reconciling/create-rebase-replay-applier.js +121 -0
  59. package/dist/ecs/database/reconciling/create-rebase-replay-applier.js.map +1 -0
  60. package/dist/ecs/database/reconciling/create-reconciling-database.d.ts +11 -0
  61. package/dist/ecs/database/reconciling/create-reconciling-database.js +28 -137
  62. package/dist/ecs/database/reconciling/create-reconciling-database.js.map +1 -1
  63. package/dist/ecs/database/transactional-store/create-transactional-store.js.map +1 -1
  64. package/dist/ecs/database/transactional-store/transactional-store.d.ts +1 -1
  65. package/dist/ecs/entity-location-table/create-entity-location-table.js +2 -2
  66. package/dist/ecs/entity-location-table/create-entity-location-table.js.map +1 -1
  67. package/dist/ecs/entity-location-table/entity-location-table.d.ts +7 -1
  68. package/dist/ecs/store/core/core.d.ts +7 -1
  69. package/dist/ecs/store/core/create-core.js +3 -3
  70. package/dist/ecs/store/core/create-core.js.map +1 -1
  71. package/dist/ecs/store/core/select-entities.js +6 -3
  72. package/dist/ecs/store/core/select-entities.js.map +1 -1
  73. package/dist/ecs/store/index-types.d.ts +53 -17
  74. package/dist/ecs/store/public/create-store.js +32 -6
  75. package/dist/ecs/store/public/create-store.js.map +1 -1
  76. package/dist/ecs/store/public/create-store.test.js +25 -0
  77. package/dist/ecs/store/public/create-store.test.js.map +1 -1
  78. package/dist/ecs/store/store.d.ts +10 -3
  79. package/dist/ecs/store/store.js.map +1 -1
  80. package/dist/functions/compare.d.ts +13 -0
  81. package/dist/functions/compare.js +15 -0
  82. package/dist/functions/compare.js.map +1 -0
  83. package/dist/functions/compare.test.d.ts +1 -0
  84. package/dist/functions/compare.test.js +24 -0
  85. package/dist/functions/compare.test.js.map +1 -0
  86. package/dist/functions/index.d.ts +1 -0
  87. package/dist/functions/index.js +1 -0
  88. package/dist/functions/index.js.map +1 -1
  89. package/dist/test-setup.js +2 -2
  90. package/dist/test-setup.js.map +1 -1
  91. package/dist/tsconfig.tsbuildinfo +1 -1
  92. package/package.json +3 -1
  93. package/references/data-lit/package.json +1 -1
  94. package/references/data-lit-tictactoe/package.json +1 -1
  95. package/references/data-react/package.json +1 -1
  96. package/references/data-react-hello/package.json +1 -1
  97. 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,258 @@ 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
+ });
176
+ });
177
+ describe("Pattern 4 — observe(arg): reactive sorted bucket view", () => {
178
+ const plugin = () => Database.Plugin.create({
179
+ components: {
180
+ parent: { type: "number" },
181
+ fractIndex: { type: "string" },
182
+ },
183
+ archetypes: { Child: ["parent", "fractIndex"] },
184
+ indexes: {
185
+ orderedChildrenOf: {
186
+ key: "parent",
187
+ order: { by: ["fractIndex"] },
188
+ },
189
+ },
190
+ transactions: {
191
+ add: (t, args) => t.archetypes.Child.insert(args),
192
+ move: (t, args) => t.update(args.entity, { fractIndex: args.fractIndex }),
193
+ reparent: (t, args) => t.update(args.entity, { parent: args.parent }),
194
+ delete: (t, e) => t.delete(e),
195
+ },
196
+ });
197
+ it("emits the initial sorted list synchronously on subscribe", () => {
198
+ const db = Database.create(plugin());
199
+ const c = db.transactions.add({ parent: 7, fractIndex: "c" });
200
+ const a = db.transactions.add({ parent: 7, fractIndex: "a" });
201
+ const b = db.transactions.add({ parent: 7, fractIndex: "b" });
202
+ const emissions = [];
203
+ const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((entities) => { emissions.push([...entities]); });
204
+ expect(emissions).toEqual([[a, b, c]]);
205
+ unsub();
206
+ });
207
+ it("emits the re-sorted list when an entity is inserted into the bucket", async () => {
208
+ const db = Database.create(plugin());
209
+ const a = db.transactions.add({ parent: 7, fractIndex: "a" });
210
+ const c = db.transactions.add({ parent: 7, fractIndex: "c" });
211
+ const emissions = [];
212
+ const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((entities) => { emissions.push([...entities]); });
213
+ const b = db.transactions.add({ parent: 7, fractIndex: "b" });
214
+ await Promise.resolve();
215
+ expect(emissions[0]).toEqual([a, c]);
216
+ expect(emissions[emissions.length - 1]).toEqual([a, b, c]);
217
+ unsub();
218
+ });
219
+ it("emits a reorder when only the sort key changes (regression case)", async () => {
220
+ const db = Database.create(plugin());
221
+ const a = db.transactions.add({ parent: 7, fractIndex: "a" });
222
+ const b = db.transactions.add({ parent: 7, fractIndex: "b" });
223
+ const c = db.transactions.add({ parent: 7, fractIndex: "c" });
224
+ const emissions = [];
225
+ const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((entities) => { emissions.push([...entities]); });
226
+ expect(emissions[0]).toEqual([a, b, c]);
227
+ // Reorder-only transaction: parent (the bucket key) is untouched, so a
228
+ // `where`-only observe.select would never see this. The index does.
229
+ db.transactions.move({ entity: b, fractIndex: "z" });
230
+ await Promise.resolve();
231
+ expect(emissions[emissions.length - 1]).toEqual([a, c, b]);
232
+ unsub();
233
+ });
234
+ it("emits the shrunken list when an entity is deleted from the bucket", async () => {
235
+ const db = Database.create(plugin());
236
+ const a = db.transactions.add({ parent: 7, fractIndex: "a" });
237
+ const b = db.transactions.add({ parent: 7, fractIndex: "b" });
238
+ const emissions = [];
239
+ const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((entities) => { emissions.push([...entities]); });
240
+ db.transactions.delete(b);
241
+ await Promise.resolve();
242
+ expect(emissions[emissions.length - 1]).toEqual([a]);
243
+ unsub();
244
+ });
245
+ it("does NOT emit when an unrelated bucket's entity changes", async () => {
246
+ const db = Database.create(plugin());
247
+ db.transactions.add({ parent: 7, fractIndex: "a" });
248
+ const other = db.transactions.add({ parent: 9, fractIndex: "a" });
249
+ const emissions = [];
250
+ const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((entities) => { emissions.push([...entities]); });
251
+ expect(emissions).toHaveLength(1);
252
+ // A child of a *different* parent moves. Same component (`fractIndex`)
253
+ // changes, so the observer is woken and recomputes — but its own
254
+ // bucket is unchanged, so no second emission.
255
+ db.transactions.move({ entity: other, fractIndex: "z" });
256
+ await Promise.resolve();
257
+ expect(emissions).toHaveLength(1);
258
+ unsub();
259
+ });
260
+ it("emits on both buckets when an entity reparents between them", async () => {
261
+ const db = Database.create(plugin());
262
+ const a = db.transactions.add({ parent: 7, fractIndex: "a" });
263
+ const m = db.transactions.add({ parent: 7, fractIndex: "b" });
264
+ const fromSeven = [];
265
+ const toNine = [];
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
+ db.transactions.reparent({ entity: m, parent: 9 });
269
+ await Promise.resolve();
270
+ expect(fromSeven[fromSeven.length - 1]).toEqual([a]);
271
+ expect(toNine[toNine.length - 1]).toEqual([m]);
272
+ unsub7();
273
+ unsub9();
274
+ });
275
+ it("unsubscribe stops further emissions", async () => {
276
+ const db = Database.create(plugin());
277
+ db.transactions.add({ parent: 7, fractIndex: "a" });
278
+ const emissions = [];
279
+ const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((entities) => { emissions.push([...entities]); });
280
+ expect(emissions).toHaveLength(1);
281
+ unsub();
282
+ db.transactions.add({ parent: 7, fractIndex: "b" });
283
+ await Promise.resolve();
284
+ expect(emissions).toHaveLength(1);
285
+ });
286
+ });
287
+ describe("observe(arg) — additional edge cases", () => {
288
+ const sortedPlugin = () => Database.Plugin.create({
289
+ components: {
290
+ parent: { type: "number" },
291
+ fractIndex: { type: "string" },
292
+ },
293
+ archetypes: { Child: ["parent", "fractIndex"] },
294
+ indexes: {
295
+ orderedChildrenOf: { key: "parent", order: { by: ["fractIndex"] } },
296
+ childrenOf: { key: "parent" },
297
+ },
298
+ transactions: {
299
+ add: (t, args) => t.archetypes.Child.insert(args),
300
+ move: (t, args) => t.update(args.entity, { fractIndex: args.fractIndex }),
301
+ delete: (t, e) => t.delete(e),
302
+ },
303
+ });
304
+ it("emits [] for an initially-empty bucket, then the entity once populated", async () => {
305
+ const db = Database.create(sortedPlugin());
306
+ const emissions = [];
307
+ const unsub = db.indexes.orderedChildrenOf.observe({ parent: 42 })((e) => emissions.push([...e]));
308
+ expect(emissions).toEqual([[]]);
309
+ const e = db.transactions.add({ parent: 42, fractIndex: "a" });
310
+ await Promise.resolve();
311
+ expect(emissions[emissions.length - 1]).toEqual([e]);
312
+ unsub();
313
+ });
314
+ it("emits [] when the last entity leaves the bucket", async () => {
315
+ const db = Database.create(sortedPlugin());
316
+ const only = db.transactions.add({ parent: 7, fractIndex: "a" });
317
+ const emissions = [];
318
+ const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((e) => emissions.push([...e]));
319
+ db.transactions.delete(only);
320
+ await Promise.resolve();
321
+ expect(emissions[emissions.length - 1]).toEqual([]);
322
+ unsub();
323
+ });
324
+ it("notifies every subscriber of the same bucket", async () => {
325
+ const db = Database.create(sortedPlugin());
326
+ const a = db.transactions.add({ parent: 7, fractIndex: "a" });
327
+ const first = [];
328
+ const second = [];
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
+ const b = db.transactions.add({ parent: 7, fractIndex: "b" });
332
+ await Promise.resolve();
333
+ expect(first[first.length - 1]).toEqual([a, b]);
334
+ expect(second[second.length - 1]).toEqual([a, b]);
335
+ unsub1();
336
+ unsub2();
337
+ });
338
+ it("coalesces several transactions before the microtask into one emission", async () => {
339
+ const db = Database.create(sortedPlugin());
340
+ const a = db.transactions.add({ parent: 7, fractIndex: "a" });
341
+ const emissions = [];
342
+ const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((e) => emissions.push([...e]));
343
+ // Three synchronous transactions, no awaits in between.
344
+ const c = db.transactions.add({ parent: 7, fractIndex: "c" });
345
+ const b = db.transactions.add({ parent: 7, fractIndex: "b" });
346
+ db.transactions.move({ entity: c, fractIndex: "d" });
347
+ await Promise.resolve();
348
+ // Initial emit + exactly one coalesced emit reflecting the final state.
349
+ expect(emissions).toHaveLength(2);
350
+ expect(emissions[1]).toEqual([a, b, c]);
351
+ unsub();
352
+ });
353
+ it("does not re-emit when a touched bucket ends a transaction unchanged", async () => {
354
+ const db = Database.create(sortedPlugin());
355
+ const a = db.transactions.add({ parent: 7, fractIndex: "a" });
356
+ const emissions = [];
357
+ const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((e) => emissions.push([...e]));
358
+ // Move the only child to a new sort key and back within the same
359
+ // microtask window: the bucket is woken but its final sequence is
360
+ // identical, so no second emission.
361
+ db.transactions.move({ entity: a, fractIndex: "z" });
362
+ db.transactions.move({ entity: a, fractIndex: "a" });
363
+ await Promise.resolve();
364
+ expect(emissions).toHaveLength(1);
365
+ unsub();
366
+ });
367
+ it("works on a non-sorted index — membership changes still emit", async () => {
368
+ const db = Database.create(sortedPlugin());
369
+ const a = db.transactions.add({ parent: 7, fractIndex: "a" });
370
+ const emissions = [];
371
+ const unsub = db.indexes.childrenOf.observe({ parent: 7 })((e) => emissions.push([...e].sort()));
372
+ expect(emissions[0]).toEqual([a]);
373
+ const b = db.transactions.add({ parent: 7, fractIndex: "b" });
374
+ await Promise.resolve();
375
+ expect(emissions[emissions.length - 1]).toEqual([a, b].sort());
376
+ unsub();
377
+ });
378
+ const priorityPlugin = () => Database.Plugin.create({
379
+ components: {
380
+ owner: { type: "number" },
381
+ priority: { type: "number" },
382
+ due: { type: "number" },
383
+ },
384
+ archetypes: { Task: ["owner", "priority", "due"] },
385
+ indexes: {
386
+ tasksByPriority: {
387
+ key: "owner",
388
+ order: {
389
+ by: ["priority", "due"],
390
+ compare: (a, b) => b.priority - a.priority || a.due - b.due,
391
+ },
392
+ },
393
+ },
394
+ transactions: {
395
+ add: (t, args) => t.archetypes.Task.insert(args),
396
+ setPriority: (t, args) => t.update(args.entity, { priority: args.priority }),
397
+ setDue: (t, args) => t.update(args.entity, { due: args.due }),
398
+ },
399
+ });
400
+ it("re-sorts under a custom comparator when the primary sort key changes", async () => {
401
+ const db = Database.create(priorityPlugin());
402
+ const low = db.transactions.add({ owner: 1, priority: 1, due: 100 });
403
+ const high = db.transactions.add({ owner: 1, priority: 3, due: 50 });
404
+ const emissions = [];
405
+ const unsub = db.indexes.tasksByPriority.observe({ owner: 1 })((e) => emissions.push([...e]));
406
+ // priority desc → [high, low].
407
+ expect(emissions[0]).toEqual([high, low]);
408
+ db.transactions.setPriority({ entity: low, priority: 5 });
409
+ await Promise.resolve();
410
+ expect(emissions[emissions.length - 1]).toEqual([low, high]);
411
+ unsub();
412
+ });
413
+ it("re-sorts when only the secondary (tie-break) sort component changes", async () => {
414
+ const db = Database.create(priorityPlugin());
415
+ // Equal priority → ordered by `due` asc.
416
+ const early = db.transactions.add({ owner: 1, priority: 2, due: 10 });
417
+ const late = db.transactions.add({ owner: 1, priority: 2, due: 20 });
418
+ const emissions = [];
419
+ const unsub = db.indexes.tasksByPriority.observe({ owner: 1 })((e) => emissions.push([...e]));
420
+ expect(emissions[0]).toEqual([early, late]);
421
+ // Push `early`'s due past `late`'s — flips the tie-break order.
422
+ db.transactions.setDue({ entity: early, due: 30 });
423
+ await Promise.resolve();
424
+ expect(emissions[emissions.length - 1]).toEqual([late, early]);
425
+ unsub();
175
426
  });
176
427
  });
177
428
  describe("Pattern 5 — multi-value (array column → fan-out): tasksByAssignee", () => {
@@ -193,36 +444,36 @@ describe("Pattern 5 — multi-value (array column → fan-out): tasksByAssignee"
193
444
  it("each array element becomes its own bucket entry", () => {
194
445
  const db = Database.create(plugin());
195
446
  const e = db.transactions.add({ title: "ship", assigned: ["joe", "bob"] });
196
- expect(db.indexes.tasksByAssignee.find("joe")).toEqual([e]);
197
- expect(db.indexes.tasksByAssignee.find("bob")).toEqual([e]);
198
- 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([]);
199
450
  });
200
451
  it("multiple entities sharing an element are both returned", () => {
201
452
  const db = Database.create(plugin());
202
453
  const a = db.transactions.add({ title: "A", assigned: ["joe", "bob"] });
203
454
  const b = db.transactions.add({ title: "B", assigned: ["joe", "carol"] });
204
455
  db.transactions.add({ title: "C", assigned: ["diane"] });
205
- expect([...db.indexes.tasksByAssignee.find("joe")].sort()).toEqual([a, b].sort());
456
+ expect([...db.indexes.tasksByAssignee.find({ assigned: "joe" })].sort()).toEqual([a, b].sort());
206
457
  });
207
458
  it("empty array contributes no bucket entries", () => {
208
459
  const db = Database.create(plugin());
209
460
  db.transactions.add({ title: "Unassigned", assigned: [] });
210
- expect(db.indexes.tasksByAssignee.find("joe")).toEqual([]);
461
+ expect(db.indexes.tasksByAssignee.find({ assigned: "joe" })).toEqual([]);
211
462
  });
212
463
  it("update set-diffs the bucket membership", () => {
213
464
  const db = Database.create(plugin());
214
465
  const e = db.transactions.add({ title: "T", assigned: ["joe", "bob"] });
215
466
  db.transactions.reassign({ entity: e, assigned: ["joe", "carol"] });
216
- expect(db.indexes.tasksByAssignee.find("joe")).toEqual([e]);
217
- expect(db.indexes.tasksByAssignee.find("bob")).toEqual([]);
218
- 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]);
219
470
  });
220
471
  it("delete drops the entity from every bucket it was in", () => {
221
472
  const db = Database.create(plugin());
222
473
  const e = db.transactions.add({ title: "T", assigned: ["joe", "bob"] });
223
474
  db.transactions.delete(e);
224
- expect(db.indexes.tasksByAssignee.find("joe")).toEqual([]);
225
- 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([]);
226
477
  });
227
478
  });
228
479
  describe("Pattern 6 — computed scalar (byEmailCi)", () => {
@@ -230,7 +481,7 @@ describe("Pattern 6 — computed scalar (byEmailCi)", () => {
230
481
  components: { email: { type: "string" } },
231
482
  archetypes: { User: ["email"] },
232
483
  indexes: {
233
- byEmailCi: { key: (email) => email.toLowerCase(), components: ["email"] },
484
+ byEmailCi: { key: { email: (c) => c.email.toLowerCase() }, components: ["email"] },
234
485
  },
235
486
  transactions: {
236
487
  add: (t, email) => t.archetypes.User.insert({ email }),
@@ -241,23 +492,23 @@ describe("Pattern 6 — computed scalar (byEmailCi)", () => {
241
492
  it("looks up by the computed (lowercased) key", () => {
242
493
  const db = Database.create(plugin());
243
494
  const e = db.transactions.add("Alice@Example.com");
244
- expect(db.indexes.byEmailCi.find("alice@example.com")).toEqual([e]);
495
+ expect(db.indexes.byEmailCi.find({ email: "alice@example.com" })).toEqual([e]);
245
496
  // The original-case input is not a key.
246
- expect(db.indexes.byEmailCi.find("Alice@Example.com")).toEqual([]);
497
+ expect(db.indexes.byEmailCi.find({ email: "Alice@Example.com" })).toEqual([]);
247
498
  });
248
499
  it("update re-derives the key", () => {
249
500
  const db = Database.create(plugin());
250
501
  const e = db.transactions.add("alice@a.com");
251
- expect(db.indexes.byEmailCi.find("alice@a.com")).toEqual([e]);
502
+ expect(db.indexes.byEmailCi.find({ email: "alice@a.com" })).toEqual([e]);
252
503
  db.transactions.rename({ entity: e, email: "alice@b.com" });
253
- expect(db.indexes.byEmailCi.find("alice@a.com")).toEqual([]);
254
- 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]);
255
506
  });
256
507
  it("delete removes the computed bucket entry", () => {
257
508
  const db = Database.create(plugin());
258
509
  const e = db.transactions.add("alice@a.com");
259
510
  db.transactions.delete(e);
260
- expect(db.indexes.byEmailCi.find("alice@a.com")).toEqual([]);
511
+ expect(db.indexes.byEmailCi.find({ email: "alice@a.com" })).toEqual([]);
261
512
  });
262
513
  });
263
514
  describe("Pattern 7 — multi-value computed (docsByKeyword)", () => {
@@ -266,7 +517,9 @@ describe("Pattern 7 — multi-value computed (docsByKeyword)", () => {
266
517
  archetypes: { Doc: ["body"] },
267
518
  indexes: {
268
519
  docsByKeyword: {
269
- 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
+ },
270
523
  components: ["body"],
271
524
  },
272
525
  },
@@ -277,14 +530,14 @@ describe("Pattern 7 — multi-value computed (docsByKeyword)", () => {
277
530
  it("fans out each computed array element into its own bucket", () => {
278
531
  const db = Database.create(plugin());
279
532
  const d = db.transactions.add("the quick brown fox");
280
- expect(db.indexes.docsByKeyword.find("quick")).toEqual([d]);
281
- expect(db.indexes.docsByKeyword.find("brown")).toEqual([d]);
282
- 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([]);
283
536
  });
284
537
  it("findRange supports operator filters on the scalar element", () => {
285
538
  const db = Database.create(plugin());
286
539
  const d = db.transactions.add("alpha beta gamma");
287
- const inRange = [...db.indexes.docsByKeyword.findRange({ ">=": "b", "<": "z" })].sort();
540
+ const inRange = [...db.indexes.docsByKeyword.findRange({ keyword: { ">=": "b", "<": "z" } })].sort();
288
541
  expect(inRange).toEqual([d]);
289
542
  });
290
543
  });
@@ -305,8 +558,8 @@ describe("Pattern 8 — compound from nested data (playerByRoster)", () => {
305
558
  indexes: {
306
559
  playerByRoster: {
307
560
  key: {
308
- team: (r) => r.team,
309
- position: (r) => r.position,
561
+ team: (c) => c.roster.team,
562
+ position: (c) => c.roster.position,
310
563
  },
311
564
  unique: true,
312
565
  components: ["roster"],
@@ -342,7 +595,7 @@ describe("Pattern 10 — mixed identity + derived slots (playerByTeamRole)", ()
342
595
  playerByTeamRole: {
343
596
  key: {
344
597
  team: "team",
345
- role: (r) => r.role,
598
+ role: (c) => c.roster.role,
346
599
  },
347
600
  unique: true,
348
601
  components: ["roster"],
@@ -398,12 +651,135 @@ describe("Pattern 12 — custom comparator (tasksByPriority)", () => {
398
651
  const e2a = db.transactions.add({ owner: 1, priority: 2, due: 10 });
399
652
  const e2b = db.transactions.add({ owner: 1, priority: 2, due: 20 });
400
653
  // Priority desc, then due asc.
401
- expect(db.indexes.tasksByPriority.find(1)).toEqual([e3, e2a, e2b, e1]);
654
+ expect(db.indexes.tasksByPriority.find({ owner: 1 })).toEqual([e3, e2a, e2b, e1]);
402
655
  });
403
656
  });
404
657
  // ============================================================================
405
658
  // Cross-cutting concerns
406
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
+ });
407
783
  describe("findRange — operator filters on the bucket key", () => {
408
784
  it("filters by range operators on a tuple-keyed index", () => {
409
785
  const plugin = Database.Plugin.create({
@@ -490,7 +866,7 @@ describe("auto-routing of db.select", () => {
490
866
  archetypes: { U: ["name"] },
491
867
  indexes: {
492
868
  byLowerName: {
493
- key: (name) => name.toLowerCase(),
869
+ key: { name: (c) => c.name.toLowerCase() },
494
870
  components: ["name"],
495
871
  },
496
872
  },
@@ -569,6 +945,135 @@ describe("auto-routing of db.observe.select", () => {
569
945
  }
570
946
  });
571
947
  });
948
+ describe("auto-routing of ordered select through a sorted index", () => {
949
+ // Two indexes on the same key, distinct shapes: one unsorted, one sorted
950
+ // ascending by `priority` (default comparator). An ordered query must route
951
+ // to the *sorted* one (never the unsorted one), and a query whose order the
952
+ // index can't serve must fall back to the scan.
953
+ const plugin = () => Database.Plugin.create({
954
+ components: {
955
+ owner: { type: "number" },
956
+ priority: { type: "number" },
957
+ due: { type: "number" },
958
+ title: { type: "string" },
959
+ },
960
+ archetypes: { Task: ["owner", "priority", "due", "title"] },
961
+ indexes: {
962
+ byOwner: { key: "owner" },
963
+ byOwnerSorted: { key: "owner", order: { by: ["priority"] } },
964
+ },
965
+ transactions: {
966
+ add: (t, a) => t.archetypes.Task.insert(a),
967
+ },
968
+ });
969
+ const spyFind = (handle) => {
970
+ const original = handle.find;
971
+ const state = { calls: 0 };
972
+ handle.find = (v) => { state.calls += 1; return original(v); };
973
+ return { state, restore: () => { handle.find = original; } };
974
+ };
975
+ const seed = (db) => {
976
+ const mid = db.transactions.add({ owner: 1, priority: 2, due: 10, title: "mid" });
977
+ const low = db.transactions.add({ owner: 1, priority: 1, due: 20, title: "low" });
978
+ const high = db.transactions.add({ owner: 1, priority: 3, due: 30, title: "high" });
979
+ db.transactions.add({ owner: 2, priority: 1, due: 40, title: "other" });
980
+ return { low, mid, high };
981
+ };
982
+ it("routes an ascending ordered query to the sorted index (and returns it sorted)", () => {
983
+ const db = Database.create(plugin());
984
+ const { low, mid, high } = seed(db);
985
+ const sorted = spyFind(db.indexes.byOwnerSorted);
986
+ try {
987
+ const result = db.select(["owner", "priority"], { where: { owner: 1 }, order: { priority: true } });
988
+ // GREEN: the ordered query was served by the sorted index...
989
+ expect(sorted.state.calls).toBe(1);
990
+ // ...and the result is in the index's ascending priority order.
991
+ expect([...result]).toEqual([low, mid, high]);
992
+ }
993
+ finally {
994
+ sorted.restore();
995
+ }
996
+ });
997
+ it("routes the ordered query to the sorted index, never the unsorted one on the same key", () => {
998
+ const db = Database.create(plugin());
999
+ seed(db);
1000
+ const unsorted = spyFind(db.indexes.byOwner);
1001
+ const sorted = spyFind(db.indexes.byOwnerSorted);
1002
+ try {
1003
+ db.select(["owner", "priority"], { where: { owner: 1 }, order: { priority: true } });
1004
+ expect(sorted.state.calls).toBe(1);
1005
+ expect(unsorted.state.calls).toBe(0);
1006
+ }
1007
+ finally {
1008
+ unsorted.restore();
1009
+ sorted.restore();
1010
+ }
1011
+ });
1012
+ it("falls back to scan for a descending order (sorted index only materializes ascending)", () => {
1013
+ const db = Database.create(plugin());
1014
+ const { low, mid, high } = seed(db);
1015
+ const sorted = spyFind(db.indexes.byOwnerSorted);
1016
+ try {
1017
+ const result = db.select(["owner", "priority"], { where: { owner: 1 }, order: { priority: false } });
1018
+ // Not routed — the index can't serve descending without re-sorting.
1019
+ expect(sorted.state.calls).toBe(0);
1020
+ // The scan still produces a correct descending result.
1021
+ expect([...result]).toEqual([high, mid, low]);
1022
+ }
1023
+ finally {
1024
+ sorted.restore();
1025
+ }
1026
+ });
1027
+ it("falls back to scan when the requested order column is not the index's sort column", () => {
1028
+ const db = Database.create(plugin());
1029
+ seed(db);
1030
+ const sorted = spyFind(db.indexes.byOwnerSorted);
1031
+ try {
1032
+ // Index sorts by `priority`; this asks for `due`.
1033
+ const result = db.select(["owner", "due"], { where: { owner: 1 }, order: { due: true } });
1034
+ expect(sorted.state.calls).toBe(0);
1035
+ expect(result).toHaveLength(3);
1036
+ }
1037
+ finally {
1038
+ sorted.restore();
1039
+ }
1040
+ });
1041
+ it("still routes a plain (no-order) equality query — unaffected by the order support", () => {
1042
+ const db = Database.create(plugin());
1043
+ seed(db);
1044
+ // No order: the first matching key index (byOwner) serves it.
1045
+ const unsorted = spyFind(db.indexes.byOwner);
1046
+ try {
1047
+ const result = db.select(["owner"], { where: { owner: 1 } });
1048
+ expect(unsorted.state.calls).toBe(1);
1049
+ expect(result).toHaveLength(3);
1050
+ }
1051
+ finally {
1052
+ unsorted.restore();
1053
+ }
1054
+ });
1055
+ it("routes the ordered query through db.observe.select too (initial snapshot + requery)", async () => {
1056
+ const db = Database.create(plugin());
1057
+ const { low, mid, high } = seed(db);
1058
+ const sorted = spyFind(db.indexes.byOwnerSorted);
1059
+ try {
1060
+ const emissions = [];
1061
+ const unsub = db.observe.select(["owner", "priority"], { where: { owner: 1 }, order: { priority: true } })((entities) => { emissions.push([...entities]); });
1062
+ // Initial snapshot routed and sorted.
1063
+ expect(sorted.state.calls).toBe(1);
1064
+ expect(emissions[0]).toEqual([low, mid, high]);
1065
+ // A new task for owner 1 with the lowest priority sorts to the front.
1066
+ const lowest = db.transactions.add({ owner: 1, priority: 0, due: 5, title: "lowest" });
1067
+ await Promise.resolve();
1068
+ expect(sorted.state.calls).toBe(2);
1069
+ expect(emissions[emissions.length - 1]).toEqual([lowest, low, mid, high]);
1070
+ unsub();
1071
+ }
1072
+ finally {
1073
+ sorted.restore();
1074
+ }
1075
+ });
1076
+ });
572
1077
  describe("t.indexes — eager maintenance inside transactions", () => {
573
1078
  const plugin = () => Database.Plugin.create({
574
1079
  components: {
@@ -583,21 +1088,21 @@ describe("t.indexes — eager maintenance inside transactions", () => {
583
1088
  transactions: {
584
1089
  add: (t, args) => t.archetypes.User.insert(args),
585
1090
  addIfNew: (t, args) => {
586
- const existing = t.indexes.uniqueByEmail.get(args.email);
1091
+ const existing = t.indexes.uniqueByEmail.get({ email: args.email });
587
1092
  if (existing !== null)
588
1093
  return existing;
589
1094
  return t.archetypes.User.insert(args);
590
1095
  },
591
1096
  renameAndLookup: (t, args) => {
592
1097
  t.update(args.entity, { name: args.newName });
593
- const found = t.indexes.byName.find(args.newName);
1098
+ const found = t.indexes.byName.find({ name: args.newName });
594
1099
  if (!found.includes(args.entity)) {
595
1100
  throw new Error("name index stale after t.update");
596
1101
  }
597
1102
  },
598
1103
  deleteAndLookup: (t, args) => {
599
1104
  t.delete(args.entity);
600
- const stillThere = t.indexes.byName.find(args.name);
1105
+ const stillThere = t.indexes.byName.find({ name: args.name });
601
1106
  if (stillThere.includes(args.entity)) {
602
1107
  throw new Error("index still references deleted entity");
603
1108
  }
@@ -616,14 +1121,14 @@ describe("t.indexes — eager maintenance inside transactions", () => {
616
1121
  const db = Database.create(plugin());
617
1122
  const e = db.transactions.add({ name: "alice", email: "a@a.com" });
618
1123
  expect(() => db.transactions.renameAndLookup({ entity: e, newName: "alex" })).not.toThrow();
619
- expect(db.indexes.byName.find("alex")).toEqual([e]);
620
- 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([]);
621
1126
  });
622
1127
  it("find does not see a row deleted earlier in the same transaction body", () => {
623
1128
  const db = Database.create(plugin());
624
1129
  const e = db.transactions.add({ name: "alice", email: "a@a.com" });
625
1130
  expect(() => db.transactions.deleteAndLookup({ entity: e, name: "alice" })).not.toThrow();
626
- expect(db.indexes.byName.find("alice")).toEqual([]);
1131
+ expect(db.indexes.byName.find({ name: "alice" })).toEqual([]);
627
1132
  });
628
1133
  it("unique conflict on insert is caught up-front — no partial store or index mutation", () => {
629
1134
  const db = Database.create(plugin());
@@ -632,11 +1137,11 @@ describe("t.indexes — eager maintenance inside transactions", () => {
632
1137
  const beforeRows = db.select(["email"]);
633
1138
  expect(() => db.transactions.add({ name: "alex", email: "shared@x.com" })).toThrow(/Unique index conflict/);
634
1139
  // The unique-key bucket still points at the original entity.
635
- expect(db.indexes.uniqueByEmail.get("shared@x.com")).toBe(first);
1140
+ expect(db.indexes.uniqueByEmail.get({ email: "shared@x.com" })).toBe(first);
636
1141
  // No phantom row landed in either index or store. byName.find("alex")
637
1142
  // returning [] proves the secondary index never saw the row;
638
1143
  // db.select asserts the store itself never grew.
639
- expect(db.indexes.byName.find("alex")).toEqual([]);
1144
+ expect(db.indexes.byName.find({ name: "alex" })).toEqual([]);
640
1145
  const afterRows = db.select(["email"]);
641
1146
  expect([...afterRows].sort()).toEqual([...beforeRows].sort());
642
1147
  });
@@ -649,8 +1154,8 @@ describe("t.indexes — eager maintenance inside transactions", () => {
649
1154
  const e2BeforeRead = db.read(e2);
650
1155
  expect(() => db.transactions.updateEmail({ entity: e1, newEmail: "b@b.com" })).toThrow(/Unique index conflict/);
651
1156
  // Index untouched on both keys.
652
- expect(db.indexes.uniqueByEmail.get("a@a.com")).toBe(e1);
653
- 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);
654
1159
  // Underlying store untouched — e1's email is still "a@a.com",
655
1160
  // proving the pre-check fired before `core.update` ran.
656
1161
  expect(db.read(e1)).toEqual(e1BeforeRead);
@@ -756,7 +1261,7 @@ describe("registry maintenance", () => {
756
1261
  const e1 = db.transactions.add("alice");
757
1262
  const e2 = db.transactions.add("alice");
758
1263
  const ext = db.extend(indexed);
759
- expect([...ext.indexes.byName.find("alice")].sort()).toEqual([e1, e2].sort());
1264
+ expect([...ext.indexes.byName.find({ name: "alice" })].sort()).toEqual([e1, e2].sort());
760
1265
  });
761
1266
  });
762
1267
  //# sourceMappingURL=database.index.test.js.map