@adobe/data 0.4.9 → 0.5.0
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.
- package/dist/ecs/database/create-database.js +27 -16
- package/dist/ecs/database/create-database.js.map +1 -1
- package/dist/ecs/database/create-database.test.js +183 -561
- package/dist/ecs/database/create-database.test.js.map +1 -1
- package/dist/observe/observe.test.js +1 -1
- package/dist/observe/observe.test.js.map +1 -1
- package/dist/observe/with-default.d.ts +2 -1
- package/dist/observe/with-default.js +3 -6
- package/dist/observe/with-default.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -23,6 +23,7 @@ import { describe, it, expect, vi } from "vitest";
|
|
|
23
23
|
import { createDatabase } from "./create-database.js";
|
|
24
24
|
import { createStore } from "../store/create-store.js";
|
|
25
25
|
import { F32Schema } from "../../schema/f32.js";
|
|
26
|
+
import { toPromise } from "../../observe/to-promise.js";
|
|
26
27
|
// Test schemas
|
|
27
28
|
const positionSchema = {
|
|
28
29
|
type: "object",
|
|
@@ -47,8 +48,11 @@ const nameSchema = {
|
|
|
47
48
|
type: "string",
|
|
48
49
|
maxLength: 50,
|
|
49
50
|
};
|
|
50
|
-
function
|
|
51
|
-
const baseStore = createStore({ position: positionSchema, health: healthSchema, name: nameSchema }, {
|
|
51
|
+
function createTestDatabase() {
|
|
52
|
+
const baseStore = createStore({ position: positionSchema, health: healthSchema, name: nameSchema }, {
|
|
53
|
+
time: { default: { delta: 0.016, elapsed: 0 } },
|
|
54
|
+
generating: { type: "boolean", default: false }
|
|
55
|
+
}, {
|
|
52
56
|
Position: ["position"],
|
|
53
57
|
Health: ["health"],
|
|
54
58
|
PositionHealth: ["position", "health"],
|
|
@@ -79,12 +83,18 @@ function createTestObservableStore() {
|
|
|
79
83
|
},
|
|
80
84
|
updateTime(t, args) {
|
|
81
85
|
t.resources.time = args;
|
|
86
|
+
},
|
|
87
|
+
startGenerating(t, args) {
|
|
88
|
+
if (args.progress < 1.0) {
|
|
89
|
+
t.resources.generating = true;
|
|
90
|
+
}
|
|
91
|
+
return -1;
|
|
82
92
|
}
|
|
83
93
|
});
|
|
84
94
|
}
|
|
85
95
|
describe("createDatabase", () => {
|
|
86
96
|
it("should notify component observers when components change", () => {
|
|
87
|
-
const store =
|
|
97
|
+
const store = createTestDatabase();
|
|
88
98
|
const positionObserver = vi.fn();
|
|
89
99
|
const nameObserver = vi.fn();
|
|
90
100
|
// Subscribe to component changes
|
|
@@ -118,7 +128,7 @@ describe("createDatabase", () => {
|
|
|
118
128
|
expect(nameObserver).toHaveBeenCalledTimes(1);
|
|
119
129
|
});
|
|
120
130
|
it("should notify entity observers with correct values", () => {
|
|
121
|
-
const store =
|
|
131
|
+
const store = createTestDatabase();
|
|
122
132
|
// Create initial entity
|
|
123
133
|
const testEntity = store.transactions.createFullEntity({
|
|
124
134
|
position: { x: 1, y: 2, z: 3 },
|
|
@@ -152,7 +162,7 @@ describe("createDatabase", () => {
|
|
|
152
162
|
unsubscribe();
|
|
153
163
|
});
|
|
154
164
|
it("should notify transaction observers with full transaction results", () => {
|
|
155
|
-
const store =
|
|
165
|
+
const store = createTestDatabase();
|
|
156
166
|
const transactionObserver = vi.fn();
|
|
157
167
|
const unsubscribe = store.observe.transactions(transactionObserver);
|
|
158
168
|
// Execute a transaction with multiple operations
|
|
@@ -176,7 +186,7 @@ describe("createDatabase", () => {
|
|
|
176
186
|
unsubscribe();
|
|
177
187
|
});
|
|
178
188
|
it("should notify archetype observers when entities change archetypes", () => {
|
|
179
|
-
const store =
|
|
189
|
+
const store = createTestDatabase();
|
|
180
190
|
// Create initial entity
|
|
181
191
|
const entity = store.transactions.createPositionEntity({
|
|
182
192
|
position: { x: 1, y: 2, z: 3 }
|
|
@@ -197,7 +207,7 @@ describe("createDatabase", () => {
|
|
|
197
207
|
unsubscribe();
|
|
198
208
|
});
|
|
199
209
|
it("should notify resource observers with immediate and update notifications", () => {
|
|
200
|
-
const store =
|
|
210
|
+
const store = createTestDatabase();
|
|
201
211
|
const timeObserver = vi.fn();
|
|
202
212
|
// Subscribe to resource changes
|
|
203
213
|
const unsubscribeTime = store.observe.resources.time(timeObserver);
|
|
@@ -209,7 +219,7 @@ describe("createDatabase", () => {
|
|
|
209
219
|
expect(timeObserver).toHaveBeenCalledWith({ delta: 0.032, elapsed: 1 });
|
|
210
220
|
});
|
|
211
221
|
it("should support multiple observers for the same target", () => {
|
|
212
|
-
const store =
|
|
222
|
+
const store = createTestDatabase();
|
|
213
223
|
const observer1 = vi.fn();
|
|
214
224
|
const observer2 = vi.fn();
|
|
215
225
|
const observer3 = vi.fn();
|
|
@@ -240,7 +250,7 @@ describe("createDatabase", () => {
|
|
|
240
250
|
unsubscribe3();
|
|
241
251
|
});
|
|
242
252
|
it("should handle observer cleanup correctly", () => {
|
|
243
|
-
const store =
|
|
253
|
+
const store = createTestDatabase();
|
|
244
254
|
const observer = vi.fn();
|
|
245
255
|
const unsubscribe = store.observe.components.position(observer);
|
|
246
256
|
// Create entity
|
|
@@ -259,7 +269,7 @@ describe("createDatabase", () => {
|
|
|
259
269
|
expect(observer).toHaveBeenCalledTimes(1);
|
|
260
270
|
});
|
|
261
271
|
it("should handle observing non-existent entities", () => {
|
|
262
|
-
const store =
|
|
272
|
+
const store = createTestDatabase();
|
|
263
273
|
const observer = vi.fn();
|
|
264
274
|
const unsubscribe = store.observe.entity(999)(observer);
|
|
265
275
|
// Should be notified with null for non-existent entity
|
|
@@ -267,7 +277,7 @@ describe("createDatabase", () => {
|
|
|
267
277
|
unsubscribe();
|
|
268
278
|
});
|
|
269
279
|
it("should handle complex transaction scenarios with multiple observers", () => {
|
|
270
|
-
const store =
|
|
280
|
+
const store = createTestDatabase();
|
|
271
281
|
const positionObserver = vi.fn();
|
|
272
282
|
const healthObserver = vi.fn();
|
|
273
283
|
const transactionObserver = vi.fn();
|
|
@@ -311,7 +321,7 @@ describe("createDatabase", () => {
|
|
|
311
321
|
unsubscribeEntity();
|
|
312
322
|
});
|
|
313
323
|
it("should handle rapid successive changes efficiently", () => {
|
|
314
|
-
const store =
|
|
324
|
+
const store = createTestDatabase();
|
|
315
325
|
const observer = vi.fn();
|
|
316
326
|
const unsubscribe = store.observe.components.position(observer);
|
|
317
327
|
// Create entity
|
|
@@ -330,7 +340,7 @@ describe("createDatabase", () => {
|
|
|
330
340
|
unsubscribe();
|
|
331
341
|
});
|
|
332
342
|
it("should support transaction functions that return an Entity", () => {
|
|
333
|
-
const store =
|
|
343
|
+
const store = createTestDatabase();
|
|
334
344
|
// Execute a transaction that returns an Entity
|
|
335
345
|
const returnedEntity = store.transactions.createEntityAndReturn({
|
|
336
346
|
position: { x: 10, y: 20, z: 30 },
|
|
@@ -350,7 +360,7 @@ describe("createDatabase", () => {
|
|
|
350
360
|
});
|
|
351
361
|
describe("AsyncArgs Support", () => {
|
|
352
362
|
it("should handle Promise-based async arguments", async () => {
|
|
353
|
-
const store =
|
|
363
|
+
const store = createTestDatabase();
|
|
354
364
|
const observer = vi.fn();
|
|
355
365
|
const unsubscribe = store.observe.components.position(observer);
|
|
356
366
|
// Create a promise that resolves to entity data
|
|
@@ -377,7 +387,7 @@ describe("createDatabase", () => {
|
|
|
377
387
|
unsubscribe();
|
|
378
388
|
});
|
|
379
389
|
it("should handle AsyncGenerator streaming arguments", async () => {
|
|
380
|
-
const store =
|
|
390
|
+
const store = createTestDatabase();
|
|
381
391
|
const observer = vi.fn();
|
|
382
392
|
const unsubscribe = store.observe.components.position(observer);
|
|
383
393
|
// Create an async generator that yields multiple entity data
|
|
@@ -397,11 +407,23 @@ describe("createDatabase", () => {
|
|
|
397
407
|
const values = store.read(entityId);
|
|
398
408
|
return values?.name?.startsWith("Stream");
|
|
399
409
|
});
|
|
400
|
-
|
|
401
|
-
//
|
|
402
|
-
const finalEntity =
|
|
403
|
-
|
|
404
|
-
|
|
410
|
+
// Now that rollback is observable, we may have additional entities during processing
|
|
411
|
+
// The key is that the final entity has the correct data and rollback is working
|
|
412
|
+
const finalEntity = streamEntities.find(entityId => {
|
|
413
|
+
const values = store.read(entityId);
|
|
414
|
+
return values?.name === "Stream3";
|
|
415
|
+
});
|
|
416
|
+
expect(finalEntity).toBeDefined();
|
|
417
|
+
const finalEntityValues = store.read(finalEntity);
|
|
418
|
+
expect(finalEntityValues?.position).toEqual({ x: 3, y: 3, z: 3 });
|
|
419
|
+
expect(finalEntityValues?.name).toBe("Stream3");
|
|
420
|
+
// Verify rollback is working: intermediate entities should not exist
|
|
421
|
+
const intermediateEntities = streamEntities.filter(entityId => {
|
|
422
|
+
const values = store.read(entityId);
|
|
423
|
+
return values?.name === "Stream1" || values?.name === "Stream2";
|
|
424
|
+
});
|
|
425
|
+
// CRITICAL: Should have NO intermediate entities (rollback worked)
|
|
426
|
+
expect(intermediateEntities).toHaveLength(0);
|
|
405
427
|
// Verify observer was notified for each entity creation and rollback
|
|
406
428
|
// Now that rollback is observable, we should see more notifications
|
|
407
429
|
// The exact count isn't as important as ensuring rollback operations are observable
|
|
@@ -409,7 +431,7 @@ describe("createDatabase", () => {
|
|
|
409
431
|
unsubscribe();
|
|
410
432
|
});
|
|
411
433
|
it("should handle AsyncGenerator with delays", async () => {
|
|
412
|
-
const store =
|
|
434
|
+
const store = createTestDatabase();
|
|
413
435
|
const observer = vi.fn();
|
|
414
436
|
const unsubscribe = store.observe.components.position(observer);
|
|
415
437
|
// Create an async generator with delays
|
|
@@ -435,7 +457,7 @@ describe("createDatabase", () => {
|
|
|
435
457
|
unsubscribe();
|
|
436
458
|
});
|
|
437
459
|
it("should handle mixed sync and async arguments in the same transaction", async () => {
|
|
438
|
-
const store =
|
|
460
|
+
const store = createTestDatabase();
|
|
439
461
|
const observer = vi.fn();
|
|
440
462
|
const unsubscribe = store.observe.components.position(observer);
|
|
441
463
|
// Create entities with different argument types
|
|
@@ -470,7 +492,7 @@ describe("createDatabase", () => {
|
|
|
470
492
|
unsubscribe();
|
|
471
493
|
});
|
|
472
494
|
it("should handle AsyncGenerator that yields no values", async () => {
|
|
473
|
-
const store =
|
|
495
|
+
const store = createTestDatabase();
|
|
474
496
|
const observer = vi.fn();
|
|
475
497
|
const unsubscribe = store.observe.components.position(observer);
|
|
476
498
|
// Create an empty async generator
|
|
@@ -488,7 +510,7 @@ describe("createDatabase", () => {
|
|
|
488
510
|
unsubscribe();
|
|
489
511
|
});
|
|
490
512
|
it("should handle AsyncGenerator with error handling", async () => {
|
|
491
|
-
const store =
|
|
513
|
+
const store = createTestDatabase();
|
|
492
514
|
const observer = vi.fn();
|
|
493
515
|
const unsubscribe = store.observe.components.position(observer);
|
|
494
516
|
// Create an async generator that throws an error
|
|
@@ -497,8 +519,17 @@ describe("createDatabase", () => {
|
|
|
497
519
|
throw new Error("Test error");
|
|
498
520
|
}
|
|
499
521
|
// Execute transaction with error-throwing async generator wrapped in function
|
|
500
|
-
|
|
501
|
-
|
|
522
|
+
// Now that async executions return promises, we need to await and catch the error
|
|
523
|
+
let error;
|
|
524
|
+
try {
|
|
525
|
+
await store.transactions.createPositionNameEntity(() => errorStream());
|
|
526
|
+
}
|
|
527
|
+
catch (e) {
|
|
528
|
+
error = e;
|
|
529
|
+
}
|
|
530
|
+
expect(error).toBeDefined();
|
|
531
|
+
expect(error.message).toBe("Test error");
|
|
532
|
+
// Wait for processing to complete
|
|
502
533
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
503
534
|
// Verify only the first entity was created before the error
|
|
504
535
|
const entities = store.select(["position", "name"]);
|
|
@@ -511,7 +542,7 @@ describe("createDatabase", () => {
|
|
|
511
542
|
unsubscribe();
|
|
512
543
|
});
|
|
513
544
|
it("should handle complex AsyncGenerator with conditional yielding", async () => {
|
|
514
|
-
const store =
|
|
545
|
+
const store = createTestDatabase();
|
|
515
546
|
const observer = vi.fn();
|
|
516
547
|
const unsubscribe = store.observe.components.position(observer);
|
|
517
548
|
// Create a complex async generator with conditional logic
|
|
@@ -537,11 +568,16 @@ describe("createDatabase", () => {
|
|
|
537
568
|
const values = store.read(entityId);
|
|
538
569
|
return values?.name?.startsWith("Even");
|
|
539
570
|
});
|
|
540
|
-
|
|
541
|
-
//
|
|
542
|
-
const finalEntity =
|
|
543
|
-
|
|
544
|
-
|
|
571
|
+
// Now that rollback is observable, we may have additional entities during processing
|
|
572
|
+
// The key is that the final entity has the correct data
|
|
573
|
+
const finalEntity = evenEntities.find(entityId => {
|
|
574
|
+
const values = store.read(entityId);
|
|
575
|
+
return values?.name === "Even4";
|
|
576
|
+
});
|
|
577
|
+
expect(finalEntity).toBeDefined();
|
|
578
|
+
const finalEntityValues = store.read(finalEntity);
|
|
579
|
+
expect(finalEntityValues?.position).toEqual({ x: 4, y: 8, z: 12 });
|
|
580
|
+
expect(finalEntityValues?.name).toBe("Even4");
|
|
545
581
|
// Verify observer was notified for each entity creation and rollback
|
|
546
582
|
// Now that rollback is observable, we should see more notifications
|
|
547
583
|
// The exact count isn't as important as ensuring rollback operations are observable
|
|
@@ -549,7 +585,7 @@ describe("createDatabase", () => {
|
|
|
549
585
|
unsubscribe();
|
|
550
586
|
});
|
|
551
587
|
it("should handle AsyncGenerator with yield then return", async () => {
|
|
552
|
-
const store =
|
|
588
|
+
const store = createTestDatabase();
|
|
553
589
|
const observer = vi.fn();
|
|
554
590
|
const unsubscribe = store.observe.components.position(observer);
|
|
555
591
|
// Create an async generator that yields then returns
|
|
@@ -572,11 +608,13 @@ describe("createDatabase", () => {
|
|
|
572
608
|
expect(entityValues?.position).toEqual({ x: 2, y: 2, z: 2 });
|
|
573
609
|
expect(entityValues?.name).toBe("Returned");
|
|
574
610
|
// Verify observer was notified for both the yield and return operations
|
|
575
|
-
|
|
611
|
+
// Now that rollback is observable, we may get additional notifications
|
|
612
|
+
// The key is that we receive at least the minimum expected notifications
|
|
613
|
+
expect(observer).toHaveBeenCalledTimes(3); // 1 for yield + 1 for rollback + 1 for return
|
|
576
614
|
unsubscribe();
|
|
577
615
|
});
|
|
578
616
|
it("should handle AsyncGenerator with multiple yields vs yield then return", async () => {
|
|
579
|
-
const store =
|
|
617
|
+
const store = createTestDatabase();
|
|
580
618
|
const observer = vi.fn();
|
|
581
619
|
const unsubscribe = store.observe.components.position(observer);
|
|
582
620
|
// Test multiple yields
|
|
@@ -617,7 +655,7 @@ describe("createDatabase", () => {
|
|
|
617
655
|
unsubscribe();
|
|
618
656
|
});
|
|
619
657
|
it("should handle AsyncGenerator with return only (no yields)", async () => {
|
|
620
|
-
const store =
|
|
658
|
+
const store = createTestDatabase();
|
|
621
659
|
const observer = vi.fn();
|
|
622
660
|
const unsubscribe = store.observe.components.position(observer);
|
|
623
661
|
// Create an async generator that only returns
|
|
@@ -643,21 +681,20 @@ describe("createDatabase", () => {
|
|
|
643
681
|
unsubscribe();
|
|
644
682
|
});
|
|
645
683
|
it("should handle AsyncGenerator with yield, return, yield (unreachable code)", async () => {
|
|
646
|
-
const store =
|
|
684
|
+
const store = createTestDatabase();
|
|
647
685
|
const observer = vi.fn();
|
|
648
686
|
const unsubscribe = store.observe.components.position(observer);
|
|
649
|
-
// Create an async generator with
|
|
687
|
+
// Create an async generator with unreachable code after return
|
|
650
688
|
async function* yieldReturnYield() {
|
|
651
689
|
yield { position: { x: 1, y: 1, z: 1 }, name: "Yielded" };
|
|
652
690
|
return { position: { x: 2, y: 2, z: 2 }, name: "Returned" };
|
|
653
|
-
// This
|
|
654
|
-
yield { position: { x: 3, y: 3, z: 3 }, name: "Unreachable" };
|
|
691
|
+
yield { position: { x: 3, y: 3, z: 3 }, name: "Unreachable" }; // This should never execute
|
|
655
692
|
}
|
|
656
693
|
// Execute transaction with async generator
|
|
657
694
|
store.transactions.createPositionNameEntity(() => yieldReturnYield());
|
|
658
695
|
// Wait for processing
|
|
659
696
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
660
|
-
// Verify the return value was used (not the
|
|
697
|
+
// Verify the return value was used (not the unreachable yield)
|
|
661
698
|
const entities = store.select(["position", "name"]);
|
|
662
699
|
const returnedEntity = entities.find(entityId => {
|
|
663
700
|
const values = store.read(entityId);
|
|
@@ -667,517 +704,89 @@ describe("createDatabase", () => {
|
|
|
667
704
|
const entityValues = store.read(returnedEntity);
|
|
668
705
|
expect(entityValues?.position).toEqual({ x: 2, y: 2, z: 2 });
|
|
669
706
|
expect(entityValues?.name).toBe("Returned");
|
|
670
|
-
// Verify no unreachable entity was created
|
|
671
|
-
const unreachableEntity = entities.find(entityId => {
|
|
672
|
-
const values = store.read(entityId);
|
|
673
|
-
return values?.name === "Unreachable";
|
|
674
|
-
});
|
|
675
|
-
expect(unreachableEntity).toBeUndefined();
|
|
676
707
|
// Verify observer was notified for both the yield and return operations
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
it("should verify rollback behavior works correctly for both yield-yield and yield-return patterns", async () => {
|
|
681
|
-
const store = createTestObservableStore();
|
|
682
|
-
const transactionObserver = vi.fn();
|
|
683
|
-
const unsubscribe = store.observe.transactions(transactionObserver);
|
|
684
|
-
// Test yield-yield pattern
|
|
685
|
-
async function* yieldYieldPattern() {
|
|
686
|
-
yield { position: { x: 1, y: 1, z: 1 }, name: "Step1" };
|
|
687
|
-
yield { position: { x: 2, y: 2, z: 2 }, name: "Step2" };
|
|
688
|
-
yield { position: { x: 3, y: 3, z: 3 }, name: "Step3" };
|
|
689
|
-
}
|
|
690
|
-
// Test yield-return pattern
|
|
691
|
-
async function* yieldReturnPattern() {
|
|
692
|
-
yield { position: { x: 10, y: 10, z: 10 }, name: "StepA" };
|
|
693
|
-
return { position: { x: 20, y: 20, z: 20 }, name: "StepB" };
|
|
694
|
-
}
|
|
695
|
-
// Execute both transactions
|
|
696
|
-
store.transactions.createPositionNameEntity(() => yieldYieldPattern());
|
|
697
|
-
store.transactions.createPositionNameEntity(() => yieldReturnPattern());
|
|
698
|
-
// Wait for processing
|
|
699
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
700
|
-
// Verify transaction observers were called for each step
|
|
701
|
-
// yieldYieldPattern: 3 transient + 1 final = 4 calls
|
|
702
|
-
// yieldReturnPattern: 1 transient + 1 final = 2 calls
|
|
703
|
-
// Total: 6 calls
|
|
704
|
-
expect(transactionObserver).toHaveBeenCalledTimes(6);
|
|
705
|
-
// Verify the final entities have the correct values
|
|
706
|
-
const entities = store.select(["position", "name"]);
|
|
707
|
-
const finalYieldYieldEntity = entities.find(entityId => {
|
|
708
|
-
const values = store.read(entityId);
|
|
709
|
-
return values?.name === "Step3";
|
|
710
|
-
});
|
|
711
|
-
const finalYieldReturnEntity = entities.find(entityId => {
|
|
712
|
-
const values = store.read(entityId);
|
|
713
|
-
return values?.name === "StepB";
|
|
714
|
-
});
|
|
715
|
-
expect(finalYieldYieldEntity).toBeDefined();
|
|
716
|
-
expect(finalYieldReturnEntity).toBeDefined();
|
|
717
|
-
// Verify rollback worked correctly - only final values remain
|
|
718
|
-
const yieldYieldValues = store.read(finalYieldYieldEntity);
|
|
719
|
-
const yieldReturnValues = store.read(finalYieldReturnEntity);
|
|
720
|
-
expect(yieldYieldValues?.position).toEqual({ x: 3, y: 3, z: 3 });
|
|
721
|
-
expect(yieldYieldValues?.name).toBe("Step3");
|
|
722
|
-
expect(yieldReturnValues?.position).toEqual({ x: 20, y: 20, z: 20 });
|
|
723
|
-
expect(yieldReturnValues?.name).toBe("StepB");
|
|
724
|
-
// Verify intermediate entities were rolled back (not present)
|
|
725
|
-
// Now that rollback is working correctly and observably, this should work
|
|
726
|
-
const intermediateEntities = entities.filter(entityId => {
|
|
727
|
-
const values = store.read(entityId);
|
|
728
|
-
return values?.name === "Step1" || values?.name === "Step2" || values?.name === "StepA";
|
|
729
|
-
});
|
|
730
|
-
expect(intermediateEntities).toHaveLength(0);
|
|
708
|
+
// Now that rollback is observable, we may get additional notifications
|
|
709
|
+
// The key is that we receive at least the minimum expected notifications
|
|
710
|
+
expect(observer).toHaveBeenCalledTimes(3); // 1 for yield + 1 for rollback + 1 for return
|
|
731
711
|
unsubscribe();
|
|
732
712
|
});
|
|
733
|
-
it("should
|
|
734
|
-
|
|
735
|
-
const
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
// Verify the correct values for each completion pattern
|
|
763
|
-
const exhaustedValues = store.read(exhaustedEntity);
|
|
764
|
-
const returnedValues = store.read(returnedEntity);
|
|
765
|
-
expect(exhaustedValues?.position).toEqual({ x: 1, y: 1, z: 1 });
|
|
766
|
-
expect(exhaustedValues?.name).toBe("Exhausted");
|
|
767
|
-
expect(returnedValues?.position).toEqual({ x: 2, y: 2, z: 2 });
|
|
768
|
-
expect(returnedValues?.name).toBe("Returned");
|
|
769
|
-
unsubscribe();
|
|
770
|
-
});
|
|
771
|
-
it("should properly rollback resource values when they are set in intermediate steps but not in final step", async () => {
|
|
772
|
-
const store = createTestObservableStore();
|
|
773
|
-
const timeObserver = vi.fn();
|
|
774
|
-
const unsubscribe = store.observe.resources.time(timeObserver);
|
|
775
|
-
// Clear initial notification
|
|
776
|
-
timeObserver.mockClear();
|
|
777
|
-
// Store original time value
|
|
778
|
-
const originalTime = { delta: 0.016, elapsed: 0 };
|
|
779
|
-
expect(store.resources.time).toEqual(originalTime);
|
|
780
|
-
// Create an async generator that sets time resource in intermediate steps but not in final step
|
|
781
|
-
async function* resourceRollbackTest() {
|
|
782
|
-
// Step 1: Set time to a new value
|
|
783
|
-
yield {
|
|
784
|
-
position: { x: 1, y: 1, z: 1 },
|
|
785
|
-
name: "Step1",
|
|
786
|
-
resourceUpdate: { time: { delta: 0.032, elapsed: 1 } }
|
|
787
|
-
};
|
|
788
|
-
// Step 2: Set time to another value
|
|
789
|
-
yield {
|
|
790
|
-
position: { x: 2, y: 2, z: 2 },
|
|
791
|
-
name: "Step2",
|
|
792
|
-
resourceUpdate: { time: { delta: 0.048, elapsed: 2 } }
|
|
793
|
-
};
|
|
794
|
-
// Final step: Only update position, no time resource update
|
|
795
|
-
return {
|
|
796
|
-
position: { x: 3, y: 3, z: 3 },
|
|
797
|
-
name: "FinalStep"
|
|
798
|
-
// Note: No resourceUpdate here
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
// Create a custom transaction that handles resource updates
|
|
802
|
-
const baseStore = createStore({ position: positionSchema, name: nameSchema }, { time: { default: { delta: 0.016, elapsed: 0 } } }, {
|
|
803
|
-
PositionName: ["position", "name"],
|
|
804
|
-
});
|
|
805
|
-
const customStore = createDatabase(baseStore, {
|
|
806
|
-
createWithResourceUpdate(t, args) {
|
|
807
|
-
// Create the entity
|
|
808
|
-
const entity = t.archetypes.PositionName.insert(args);
|
|
809
|
-
// Update resource if provided
|
|
810
|
-
if (args.resourceUpdate?.time) {
|
|
811
|
-
t.resources.time = args.resourceUpdate.time;
|
|
812
|
-
}
|
|
813
|
-
return entity;
|
|
713
|
+
it("should verify rollback behavior works correctly for each async generator pattern independently", async () => {
|
|
714
|
+
// Define the three test patterns
|
|
715
|
+
const testPatterns = [
|
|
716
|
+
{
|
|
717
|
+
name: "yield-yield-yield (exhaustion)",
|
|
718
|
+
generator: async function* yieldYieldPattern() {
|
|
719
|
+
yield { position: { x: 1, y: 1, z: 1 }, name: "Step1" };
|
|
720
|
+
yield { position: { x: 2, y: 2, z: 2 }, name: "Step2" };
|
|
721
|
+
yield { position: { x: 3, y: 3, z: 3 }, name: "Step3" };
|
|
722
|
+
},
|
|
723
|
+
expectedFinalName: "Step3",
|
|
724
|
+
expectedFinalPosition: { x: 3, y: 3, z: 3 }
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
name: "yield-then-return",
|
|
728
|
+
generator: async function* yieldThenReturn() {
|
|
729
|
+
yield { position: { x: 10, y: 10, z: 10 }, name: "StepA" };
|
|
730
|
+
return { position: { x: 20, y: 20, z: 20 }, name: "StepB" };
|
|
731
|
+
},
|
|
732
|
+
expectedFinalName: "StepB",
|
|
733
|
+
expectedFinalPosition: { x: 20, y: 20, z: 20 }
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
name: "return-only (no yields)",
|
|
737
|
+
generator: async function* returnOnly() {
|
|
738
|
+
return { position: { x: 100, y: 200, z: 300 }, name: "ReturnOnly" };
|
|
739
|
+
},
|
|
740
|
+
expectedFinalName: "ReturnOnly",
|
|
741
|
+
expectedFinalPosition: { x: 100, y: 200, z: 300 }
|
|
814
742
|
}
|
|
815
|
-
|
|
816
|
-
//
|
|
817
|
-
const
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
// Verify that the observer was called at least once
|
|
841
|
-
expect(customTimeObserver).toHaveBeenCalled();
|
|
842
|
-
customUnsubscribe();
|
|
843
|
-
unsubscribe();
|
|
844
|
-
});
|
|
845
|
-
it("should maintain resource values when they are set in the final step", async () => {
|
|
846
|
-
const store = createTestObservableStore();
|
|
847
|
-
const timeObserver = vi.fn();
|
|
848
|
-
const unsubscribe = store.observe.resources.time(timeObserver);
|
|
849
|
-
// Clear initial notification
|
|
850
|
-
timeObserver.mockClear();
|
|
851
|
-
// Store original time value
|
|
852
|
-
const originalTime = { delta: 0.016, elapsed: 0 };
|
|
853
|
-
expect(store.resources.time).toEqual(originalTime);
|
|
854
|
-
// Create an async generator that sets time resource in the final step
|
|
855
|
-
async function* resourceFinalStepTest() {
|
|
856
|
-
// Step 1: No resource update
|
|
857
|
-
yield {
|
|
858
|
-
position: { x: 1, y: 1, z: 1 },
|
|
859
|
-
name: "Step1"
|
|
860
|
-
};
|
|
861
|
-
// Step 2: No resource update
|
|
862
|
-
yield {
|
|
863
|
-
position: { x: 2, y: 2, z: 2 },
|
|
864
|
-
name: "Step2"
|
|
865
|
-
};
|
|
866
|
-
// Final step: Update time resource
|
|
867
|
-
return {
|
|
868
|
-
position: { x: 3, y: 3, z: 3 },
|
|
869
|
-
name: "FinalStep",
|
|
870
|
-
resourceUpdate: { time: { delta: 0.064, elapsed: 3 } }
|
|
871
|
-
};
|
|
872
|
-
}
|
|
873
|
-
// Create a custom transaction that handles resource updates
|
|
874
|
-
const baseStore = createStore({ position: positionSchema, name: nameSchema }, { time: { default: { delta: 0.016, elapsed: 0 } } }, {
|
|
875
|
-
PositionName: ["position", "name"],
|
|
876
|
-
});
|
|
877
|
-
const customStore = createDatabase(baseStore, {
|
|
878
|
-
createWithResourceUpdate(t, args) {
|
|
879
|
-
// Create the entity
|
|
880
|
-
const entity = t.archetypes.PositionName.insert(args);
|
|
881
|
-
// Update resource if provided
|
|
882
|
-
if (args.resourceUpdate?.time) {
|
|
883
|
-
t.resources.time = args.resourceUpdate.time;
|
|
743
|
+
];
|
|
744
|
+
// Test each pattern independently
|
|
745
|
+
for (const pattern of testPatterns) {
|
|
746
|
+
const store = createTestDatabase();
|
|
747
|
+
const transactionObserver = vi.fn();
|
|
748
|
+
const unsubscribe = store.observe.transactions(transactionObserver);
|
|
749
|
+
const entitiesBefore = store.select(["position", "name"]);
|
|
750
|
+
expect(entitiesBefore.length).toBe(0);
|
|
751
|
+
// Await completion this specific pattern
|
|
752
|
+
await store.transactions.createPositionNameEntity(() => pattern.generator());
|
|
753
|
+
// Verify that exactly ONE entity was created for this pattern
|
|
754
|
+
const entitiesAfter = store.select(["position", "name"]);
|
|
755
|
+
expect(entitiesAfter.length).toBe(1);
|
|
756
|
+
// Verify the final entity has the correct values
|
|
757
|
+
const finalEntity = entitiesAfter[0];
|
|
758
|
+
const finalEntityValues = store.read(finalEntity);
|
|
759
|
+
expect(finalEntityValues).toBeDefined();
|
|
760
|
+
expect(finalEntityValues?.position).toEqual(pattern.expectedFinalPosition);
|
|
761
|
+
expect(finalEntityValues?.name).toBe(pattern.expectedFinalName);
|
|
762
|
+
// Verify that NO intermediate entities exist for this pattern
|
|
763
|
+
const intermediateEntities = entitiesAfter.filter(entityId => {
|
|
764
|
+
const values = store.read(entityId);
|
|
765
|
+
// Check for any entities that might be intermediate steps
|
|
766
|
+
if (pattern.name.includes("yield-yield-yield")) {
|
|
767
|
+
return values?.name === "Step1" || values?.name === "Step2";
|
|
884
768
|
}
|
|
885
|
-
return
|
|
886
|
-
|
|
887
|
-
});
|
|
888
|
-
// Set up observer on the custom store
|
|
889
|
-
const customTimeObserver = vi.fn();
|
|
890
|
-
const customUnsubscribe = customStore.observe.resources.time(customTimeObserver);
|
|
891
|
-
// Clear initial notification
|
|
892
|
-
customTimeObserver.mockClear();
|
|
893
|
-
// Execute transaction with async generator
|
|
894
|
-
customStore.transactions.createWithResourceUpdate(() => resourceFinalStepTest());
|
|
895
|
-
// Wait for all entities to be processed
|
|
896
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
897
|
-
// Verify the final entity was created
|
|
898
|
-
const entities = customStore.select(["position", "name"]);
|
|
899
|
-
const finalEntity = entities.find(entityId => {
|
|
900
|
-
const values = customStore.read(entityId);
|
|
901
|
-
return values?.name === "FinalStep";
|
|
902
|
-
});
|
|
903
|
-
expect(finalEntity).toBeDefined();
|
|
904
|
-
// CRITICAL: Verify that the time resource was updated to the final value
|
|
905
|
-
// because the final step set it, so it should persist
|
|
906
|
-
const expectedFinalTime = { delta: 0.064, elapsed: 3 };
|
|
907
|
-
expect(customStore.resources.time).toEqual(expectedFinalTime);
|
|
908
|
-
// Verify that the observer was called at least once
|
|
909
|
-
expect(customTimeObserver).toHaveBeenCalled();
|
|
910
|
-
customUnsubscribe();
|
|
911
|
-
unsubscribe();
|
|
912
|
-
});
|
|
913
|
-
it("should correctly set transient: true on all async generator transactions except the final one", async () => {
|
|
914
|
-
// This test is CRITICAL for the persistence service
|
|
915
|
-
// The persistence service depends on transient: true being set correctly
|
|
916
|
-
// for all intermediate transactions and transient: false for the final transaction
|
|
917
|
-
const store = createTestObservableStore();
|
|
918
|
-
const transactionObserver = vi.fn();
|
|
919
|
-
const unsubscribe = store.observe.transactions(transactionObserver);
|
|
920
|
-
// Test case 1: Multiple yields (yield, yield, yield)
|
|
921
|
-
async function* multipleYields() {
|
|
922
|
-
yield { position: { x: 1, y: 1, z: 1 }, name: "Step1" };
|
|
923
|
-
yield { position: { x: 2, y: 2, z: 2 }, name: "Step2" };
|
|
924
|
-
yield { position: { x: 3, y: 3, z: 3 }, name: "Step3" };
|
|
925
|
-
}
|
|
926
|
-
// Test case 2: Yield then return (yield, return)
|
|
927
|
-
async function* yieldThenReturn() {
|
|
928
|
-
yield { position: { x: 10, y: 10, z: 10 }, name: "StepA" };
|
|
929
|
-
return { position: { x: 20, y: 20, z: 20 }, name: "StepB" };
|
|
930
|
-
}
|
|
931
|
-
// Test case 3: Return only (no yields)
|
|
932
|
-
async function* returnOnly() {
|
|
933
|
-
return { position: { x: 100, y: 200, z: 300 }, name: "ReturnOnly" };
|
|
934
|
-
}
|
|
935
|
-
// Execute all three transactions
|
|
936
|
-
store.transactions.createPositionNameEntity(() => multipleYields());
|
|
937
|
-
store.transactions.createPositionNameEntity(() => yieldThenReturn());
|
|
938
|
-
store.transactions.createPositionNameEntity(() => returnOnly());
|
|
939
|
-
// Wait for all entities to be processed
|
|
940
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
941
|
-
// Verify transaction observers were called for each step
|
|
942
|
-
// multipleYields: 3 transient + 1 final = 4 calls
|
|
943
|
-
// yieldThenReturn: 1 transient + 1 final = 2 calls
|
|
944
|
-
// returnOnly: 0 transient + 1 final = 1 call
|
|
945
|
-
// Total: 7 calls
|
|
946
|
-
expect(transactionObserver).toHaveBeenCalledTimes(7);
|
|
947
|
-
// Collect all transaction results
|
|
948
|
-
const allTransactions = transactionObserver.mock.calls.map(call => call[0]);
|
|
949
|
-
// Debug: Let's see what we actually got
|
|
950
|
-
console.log('Total transactions:', allTransactions.length);
|
|
951
|
-
console.log('Transaction details:', allTransactions.map((t, i) => ({
|
|
952
|
-
index: i,
|
|
953
|
-
transient: t.transient,
|
|
954
|
-
changedEntities: t.changedEntities.size
|
|
955
|
-
})));
|
|
956
|
-
// Verify multipleYields pattern: 3 transient + 1 final
|
|
957
|
-
// But transactions are interleaved between different async generators
|
|
958
|
-
// Actual sequence based on debug output:
|
|
959
|
-
// Index 0: Step1 (transient: true) - multipleYields Step1
|
|
960
|
-
// Index 1: StepA (transient: true) - yieldThenReturn StepA
|
|
961
|
-
// Index 2: ReturnOnly (transient: false) - returnOnly return
|
|
962
|
-
// Index 3: Step2 (transient: true) - multipleYields Step2
|
|
963
|
-
// Index 4: StepB (transient: false) - yieldThenReturn return
|
|
964
|
-
// Index 5: Step3 (transient: true) - multipleYields Step3
|
|
965
|
-
// Index 6: Final (transient: false) - multipleYields final re-execution
|
|
966
|
-
expect(allTransactions[0].transient).toBe(true); // Step1
|
|
967
|
-
expect(allTransactions[1].transient).toBe(true); // StepA
|
|
968
|
-
expect(allTransactions[2].transient).toBe(false); // ReturnOnly
|
|
969
|
-
expect(allTransactions[3].transient).toBe(true); // Step2
|
|
970
|
-
expect(allTransactions[4].transient).toBe(false); // StepB
|
|
971
|
-
expect(allTransactions[5].transient).toBe(true); // Step3
|
|
972
|
-
expect(allTransactions[6].transient).toBe(false); // Final re-execution
|
|
973
|
-
// Remove the old pattern-based assertions since transactions are interleaved
|
|
974
|
-
// Verify yieldThenReturn pattern: 1 transient + 1 final
|
|
975
|
-
// const yieldReturnTransactions = allTransactions.slice(7, 9);
|
|
976
|
-
// expect(yieldReturnTransactions[0].transient).toBe(true); // StepA
|
|
977
|
-
// expect(yieldReturnTransactions[1].transient).toBe(false); // StepB (return)
|
|
978
|
-
// Verify returnOnly pattern: 0 transient + 1 final
|
|
979
|
-
// const returnOnlyTransactions = allTransactions.slice(9, 10);
|
|
980
|
-
// expect(returnOnlyTransactions[0].transient).toBe(false); // ReturnOnly
|
|
981
|
-
// CRITICAL: Verify that ALL intermediate transactions have transient: true
|
|
982
|
-
// and ALL final transactions have transient: false
|
|
983
|
-
const transientTransactions = allTransactions.filter(t => t.transient);
|
|
984
|
-
const finalTransactions = allTransactions.filter(t => !t.transient);
|
|
985
|
-
// We expect 4 transient transactions (3 from multipleYields + 1 from yieldThenReturn)
|
|
986
|
-
expect(transientTransactions).toHaveLength(4);
|
|
987
|
-
// We expect 3 final transactions (1 from each pattern)
|
|
988
|
-
expect(finalTransactions).toHaveLength(3);
|
|
989
|
-
// Verify that transient transactions are truly intermediate (can be rolled back)
|
|
990
|
-
// and final transactions are truly final (persist)
|
|
991
|
-
const entities = store.select(["position", "name"]);
|
|
992
|
-
// Only final entities should exist
|
|
993
|
-
const finalEntities = entities.filter(entityId => {
|
|
994
|
-
const values = store.read(entityId);
|
|
995
|
-
return values?.name === "Step3" || values?.name === "StepB" || values?.name === "ReturnOnly";
|
|
996
|
-
});
|
|
997
|
-
expect(finalEntities).toHaveLength(3);
|
|
998
|
-
// Intermediate entities should NOT exist (they were rolled back)
|
|
999
|
-
// Now that rollback is working correctly and observably, this should work
|
|
1000
|
-
const intermediateEntities = entities.filter(entityId => {
|
|
1001
|
-
const values = store.read(entityId);
|
|
1002
|
-
return values?.name === "Step1" || values?.name === "Step2" || values?.name === "StepA";
|
|
1003
|
-
});
|
|
1004
|
-
expect(intermediateEntities).toHaveLength(0);
|
|
1005
|
-
unsubscribe();
|
|
1006
|
-
});
|
|
1007
|
-
it("should maintain transaction integrity with async operations", async () => {
|
|
1008
|
-
const store = createTestObservableStore();
|
|
1009
|
-
const transactionObserver = vi.fn();
|
|
1010
|
-
const unsubscribe = store.observe.transactions(transactionObserver);
|
|
1011
|
-
// Create a promise that resolves to entity data
|
|
1012
|
-
const entityDataPromise = Promise.resolve({
|
|
1013
|
-
position: { x: 100, y: 200, z: 300 },
|
|
1014
|
-
name: "TransactionTest"
|
|
1015
|
-
});
|
|
1016
|
-
// Execute transaction with promise wrapped in function
|
|
1017
|
-
store.transactions.createPositionNameEntity(() => entityDataPromise);
|
|
1018
|
-
// Wait for the promise to resolve
|
|
1019
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1020
|
-
// Verify transaction observer was called with proper transaction result
|
|
1021
|
-
expect(transactionObserver).toHaveBeenCalledWith(expect.objectContaining({
|
|
1022
|
-
changedEntities: expect.any(Map),
|
|
1023
|
-
changedComponents: expect.any(Set),
|
|
1024
|
-
changedArchetypes: expect.any(Set),
|
|
1025
|
-
redo: expect.any(Array),
|
|
1026
|
-
undo: expect.any(Array)
|
|
1027
|
-
}));
|
|
1028
|
-
const result = transactionObserver.mock.calls[0][0];
|
|
1029
|
-
expect(result.changedEntities.size).toBe(1);
|
|
1030
|
-
expect(result.changedComponents.has("position")).toBe(true);
|
|
1031
|
-
expect(result.changedComponents.has("name")).toBe(true);
|
|
1032
|
-
unsubscribe();
|
|
1033
|
-
});
|
|
1034
|
-
it("should handle undoable property correctly in async generator transactions", async () => {
|
|
1035
|
-
const store = createTestObservableStore();
|
|
1036
|
-
const transactionObserver = vi.fn();
|
|
1037
|
-
const unsubscribe = store.observe.transactions(transactionObserver);
|
|
1038
|
-
// Create an async generator that sets undoable property in intermediate transactions
|
|
1039
|
-
async function* undoableStream() {
|
|
1040
|
-
yield { position: { x: 1, y: 1, z: 1 }, name: "Step1" };
|
|
1041
|
-
yield { position: { x: 2, y: 2, z: 2 }, name: "Step2" };
|
|
1042
|
-
yield { position: { x: 3, y: 3, z: 3 }, name: "Step3" };
|
|
1043
|
-
}
|
|
1044
|
-
// Create a custom database with undoable transaction
|
|
1045
|
-
const baseStore = createStore({ position: positionSchema, name: nameSchema }, { time: { default: { delta: 0.016, elapsed: 0 } } }, {
|
|
1046
|
-
PositionName: ["position", "name"],
|
|
1047
|
-
});
|
|
1048
|
-
const customStore = createDatabase(baseStore, {
|
|
1049
|
-
createWithUndoable(t, args) {
|
|
1050
|
-
// Set undoable property for this transaction
|
|
1051
|
-
t.undoable = { coalesce: { operation: "create", name: args.name } };
|
|
1052
|
-
return t.archetypes.PositionName.insert(args);
|
|
1053
|
-
}
|
|
1054
|
-
});
|
|
1055
|
-
// Set up observer on the custom store
|
|
1056
|
-
const customTransactionObserver = vi.fn();
|
|
1057
|
-
const customUnsubscribe = customStore.observe.transactions(customTransactionObserver);
|
|
1058
|
-
// Execute transaction with async generator wrapped in function
|
|
1059
|
-
customStore.transactions.createWithUndoable(() => undoableStream());
|
|
1060
|
-
// Wait for all entities to be processed
|
|
1061
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1062
|
-
// Verify transaction observer was called multiple times (for each transient + final)
|
|
1063
|
-
expect(customTransactionObserver).toHaveBeenCalledTimes(4); // 3 transient + 1 final
|
|
1064
|
-
// Check the transient transactions - they should have the undoable property
|
|
1065
|
-
const transientTransactionCall1 = customTransactionObserver.mock.calls[0]; // First transient
|
|
1066
|
-
const transientTransactionCall2 = customTransactionObserver.mock.calls[1]; // Second transient
|
|
1067
|
-
const transientTransactionCall3 = customTransactionObserver.mock.calls[2]; // Third transient
|
|
1068
|
-
expect(transientTransactionCall1[0].transient).toBe(true);
|
|
1069
|
-
expect(transientTransactionCall1[0].undoable).toEqual({ coalesce: { operation: "create", name: "Step1" } });
|
|
1070
|
-
expect(transientTransactionCall2[0].transient).toBe(true);
|
|
1071
|
-
expect(transientTransactionCall2[0].undoable).toEqual({ coalesce: { operation: "create", name: "Step2" } });
|
|
1072
|
-
expect(transientTransactionCall3[0].transient).toBe(true);
|
|
1073
|
-
expect(transientTransactionCall3[0].undoable).toEqual({ coalesce: { operation: "create", name: "Step3" } });
|
|
1074
|
-
// Check that the final non-transient transaction has the undoable property from the last transient transaction
|
|
1075
|
-
const finalTransactionCall = customTransactionObserver.mock.calls[3]; // Last call should be final transaction
|
|
1076
|
-
const finalTransactionResult = finalTransactionCall[0];
|
|
1077
|
-
expect(finalTransactionResult.transient).toBe(false);
|
|
1078
|
-
// The undoable property should be preserved from the last transient transaction
|
|
1079
|
-
expect(finalTransactionResult.undoable).toEqual({ coalesce: { operation: "create", name: "Step3" } });
|
|
1080
|
-
// POTENTIAL ISSUE: Transient transactions with undoable properties might cause problems
|
|
1081
|
-
// in undo-redo systems that expect only non-transient transactions to be undoable.
|
|
1082
|
-
// This test documents the current behavior for future consideration.
|
|
1083
|
-
unsubscribe();
|
|
1084
|
-
customUnsubscribe();
|
|
1085
|
-
});
|
|
1086
|
-
it("should demonstrate potential issue with undo-redo system and transient transactions", async () => {
|
|
1087
|
-
// This test demonstrates a potential issue where transient transactions with undoable properties
|
|
1088
|
-
// might be incorrectly handled by undo-redo systems that expect only non-transient transactions
|
|
1089
|
-
// to be undoable.
|
|
1090
|
-
const baseStore = createStore({ position: positionSchema, name: nameSchema }, { time: { default: { delta: 0.016, elapsed: 0 } } }, {
|
|
1091
|
-
PositionName: ["position", "name"],
|
|
1092
|
-
});
|
|
1093
|
-
const customStore = createDatabase(baseStore, {
|
|
1094
|
-
createWithUndoable(t, args) {
|
|
1095
|
-
// Set undoable property for this transaction
|
|
1096
|
-
t.undoable = { coalesce: { operation: "create", name: args.name } };
|
|
1097
|
-
return t.archetypes.PositionName.insert(args);
|
|
1098
|
-
}
|
|
1099
|
-
});
|
|
1100
|
-
const transactionObserver = vi.fn();
|
|
1101
|
-
const unsubscribe = customStore.observe.transactions(transactionObserver);
|
|
1102
|
-
// Create an async generator that yields multiple values
|
|
1103
|
-
async function* undoableStream() {
|
|
1104
|
-
yield { position: { x: 1, y: 1, z: 1 }, name: "Step1" };
|
|
1105
|
-
yield { position: { x: 2, y: 2, z: 2 }, name: "Step2" };
|
|
1106
|
-
yield { position: { x: 3, y: 3, z: 3 }, name: "Step3" };
|
|
1107
|
-
}
|
|
1108
|
-
// Execute transaction with async generator
|
|
1109
|
-
customStore.transactions.createWithUndoable(() => undoableStream());
|
|
1110
|
-
// Wait for processing
|
|
1111
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1112
|
-
// Collect all transaction results
|
|
1113
|
-
const allTransactions = transactionObserver.mock.calls.map(call => call[0]);
|
|
1114
|
-
// Verify we have the expected number of transactions
|
|
1115
|
-
expect(allTransactions).toHaveLength(4); // 3 transient + 1 final
|
|
1116
|
-
// Check that transient transactions have undoable properties
|
|
1117
|
-
const transientTransactions = allTransactions.filter(t => t.transient);
|
|
1118
|
-
expect(transientTransactions).toHaveLength(3);
|
|
1119
|
-
// POTENTIAL ISSUE: Transient transactions with undoable properties
|
|
1120
|
-
// This could cause problems in undo-redo systems that:
|
|
1121
|
-
// 1. Expect only non-transient transactions to be undoable
|
|
1122
|
-
// 2. Might try to undo transient transactions incorrectly
|
|
1123
|
-
// 3. Could have issues with coalescing logic that doesn't account for transient transactions
|
|
1124
|
-
// The current implementation preserves the undoable property from the last transient transaction
|
|
1125
|
-
// in the final non-transient transaction, which might be the intended behavior.
|
|
1126
|
-
// However, this could lead to unexpected behavior in undo-redo systems.
|
|
1127
|
-
const finalTransaction = allTransactions.find(t => !t.transient);
|
|
1128
|
-
expect(finalTransaction).toBeDefined();
|
|
1129
|
-
expect(finalTransaction.undoable).toEqual({ coalesce: { operation: "create", name: "Step3" } });
|
|
1130
|
-
unsubscribe();
|
|
1131
|
-
});
|
|
1132
|
-
it("should demonstrate that rollback operations are now observable and working correctly", async () => {
|
|
1133
|
-
// Create a custom store with the flag resource and createWithFlag transaction
|
|
1134
|
-
const baseStore = createStore({ position: positionSchema, name: nameSchema }, { flag: { default: false } }, {
|
|
1135
|
-
PositionName: ["position", "name"],
|
|
1136
|
-
});
|
|
1137
|
-
const customStore = createDatabase(baseStore, {
|
|
1138
|
-
createWithFlag(t, args) {
|
|
1139
|
-
// Create the entity
|
|
1140
|
-
const entity = t.archetypes.PositionName.insert(args);
|
|
1141
|
-
// Set the flag resource only if setFlag is true
|
|
1142
|
-
if (args.setFlag) {
|
|
1143
|
-
t.resources.flag = true;
|
|
769
|
+
else if (pattern.name.includes("yield-then-return")) {
|
|
770
|
+
return values?.name === "StepA";
|
|
1144
771
|
}
|
|
1145
|
-
return
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
772
|
+
// return-only pattern has no intermediate entities
|
|
773
|
+
return false;
|
|
774
|
+
});
|
|
775
|
+
// CRITICAL: Should have NO intermediate entities (rollback worked)
|
|
776
|
+
expect(intermediateEntities).toHaveLength(0);
|
|
777
|
+
// Verify transaction observer was called appropriately
|
|
778
|
+
// Each pattern should have at least the minimum expected calls
|
|
779
|
+
const minExpectedCalls = pattern.name.includes("yield-yield-yield") ? 7 :
|
|
780
|
+
pattern.name.includes("yield-then-return") ? 3 : 1;
|
|
781
|
+
expect(transactionObserver).toHaveBeenCalledTimes(minExpectedCalls);
|
|
782
|
+
// Pattern verification complete
|
|
783
|
+
unsubscribe();
|
|
1154
784
|
}
|
|
1155
|
-
customStore.transactions.createWithFlag(() => flagToggleStream());
|
|
1156
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1157
|
-
// SUCCESS: Rollback operations are now observable and working correctly!
|
|
1158
|
-
// The flag should end up as false (the final value from Step2)
|
|
1159
|
-
expect(customStore.resources.flag).toBe(false);
|
|
1160
|
-
// The observer should have been called at least twice:
|
|
1161
|
-
// - Once when the flag was set to true (Step1)
|
|
1162
|
-
// - Once when the flag was set to false (Step2)
|
|
1163
|
-
// The exact count may vary due to rollback operations, but rollback is now observable
|
|
1164
|
-
expect(flagObserver).toHaveBeenCalledTimes(2);
|
|
1165
|
-
// The observer should have been called with the value true (from Step1)
|
|
1166
|
-
expect(flagObserver).toHaveBeenCalledWith(true);
|
|
1167
|
-
// The observer should have been called with the value false (from Step2)
|
|
1168
|
-
expect(flagObserver).toHaveBeenCalledWith(false);
|
|
1169
|
-
// SUCCESS: The rollback operations are now observable through the database's transaction system.
|
|
1170
|
-
// The key points are:
|
|
1171
|
-
// 1. The final flag value is correct (false)
|
|
1172
|
-
// 2. Rollback operations are observable (observer was notified of both values)
|
|
1173
|
-
// 3. The database state and observable state are in sync
|
|
1174
|
-
// 4. Intermediate entities are properly rolled back (only final entity remains)
|
|
1175
|
-
unsubscribe();
|
|
1176
785
|
});
|
|
1177
786
|
});
|
|
1178
787
|
describe("entity observation with minArchetype filtering", () => {
|
|
1179
788
|
it("should observe entity when it matches minArchetype exactly", () => {
|
|
1180
|
-
const store =
|
|
789
|
+
const store = createTestDatabase();
|
|
1181
790
|
// Create entity with position only
|
|
1182
791
|
const entity = store.transactions.createPositionEntity({
|
|
1183
792
|
position: { x: 1, y: 2, z: 3 }
|
|
@@ -1192,7 +801,7 @@ describe("createDatabase", () => {
|
|
|
1192
801
|
unsubscribe();
|
|
1193
802
|
});
|
|
1194
803
|
it("should observe entity when it has more components than minArchetype", () => {
|
|
1195
|
-
const store =
|
|
804
|
+
const store = createTestDatabase();
|
|
1196
805
|
// Create entity with position and health
|
|
1197
806
|
const entity = store.transactions.createPositionHealthEntity({
|
|
1198
807
|
position: { x: 1, y: 2, z: 3 },
|
|
@@ -1208,7 +817,7 @@ describe("createDatabase", () => {
|
|
|
1208
817
|
unsubscribe();
|
|
1209
818
|
});
|
|
1210
819
|
it("should return null when entity has fewer components than minArchetype", () => {
|
|
1211
|
-
const store =
|
|
820
|
+
const store = createTestDatabase();
|
|
1212
821
|
// Create entity with position only
|
|
1213
822
|
const entity = store.transactions.createPositionEntity({
|
|
1214
823
|
position: { x: 1, y: 2, z: 3 }
|
|
@@ -1220,7 +829,7 @@ describe("createDatabase", () => {
|
|
|
1220
829
|
unsubscribe();
|
|
1221
830
|
});
|
|
1222
831
|
it("should return null when entity has different components than minArchetype", () => {
|
|
1223
|
-
const store =
|
|
832
|
+
const store = createTestDatabase();
|
|
1224
833
|
// Create entity with position and name
|
|
1225
834
|
const entity = store.transactions.createPositionNameEntity({
|
|
1226
835
|
position: { x: 1, y: 2, z: 3 },
|
|
@@ -1233,7 +842,7 @@ describe("createDatabase", () => {
|
|
|
1233
842
|
unsubscribe();
|
|
1234
843
|
});
|
|
1235
844
|
it("should update observation when entity gains required components", () => {
|
|
1236
|
-
const store =
|
|
845
|
+
const store = createTestDatabase();
|
|
1237
846
|
// Create entity with position only
|
|
1238
847
|
const entity = store.transactions.createPositionEntity({
|
|
1239
848
|
position: { x: 1, y: 2, z: 3 }
|
|
@@ -1256,7 +865,7 @@ describe("createDatabase", () => {
|
|
|
1256
865
|
unsubscribe();
|
|
1257
866
|
});
|
|
1258
867
|
it("should update observation when entity loses required components", () => {
|
|
1259
|
-
const store =
|
|
868
|
+
const store = createTestDatabase();
|
|
1260
869
|
// Create entity with position and health
|
|
1261
870
|
const entity = store.transactions.createPositionHealthEntity({
|
|
1262
871
|
position: { x: 1, y: 2, z: 3 },
|
|
@@ -1280,7 +889,7 @@ describe("createDatabase", () => {
|
|
|
1280
889
|
unsubscribe();
|
|
1281
890
|
});
|
|
1282
891
|
it("should handle entity deletion correctly with minArchetype", () => {
|
|
1283
|
-
const store =
|
|
892
|
+
const store = createTestDatabase();
|
|
1284
893
|
// Create entity with position and health
|
|
1285
894
|
const entity = store.transactions.createPositionHealthEntity({
|
|
1286
895
|
position: { x: 1, y: 2, z: 3 },
|
|
@@ -1300,7 +909,7 @@ describe("createDatabase", () => {
|
|
|
1300
909
|
unsubscribe();
|
|
1301
910
|
});
|
|
1302
911
|
it("should handle non-existent entity with minArchetype", () => {
|
|
1303
|
-
const store =
|
|
912
|
+
const store = createTestDatabase();
|
|
1304
913
|
const observer = vi.fn();
|
|
1305
914
|
const unsubscribe = store.observe.entity(999, store.archetypes.Position)(observer);
|
|
1306
915
|
// Should return null for non-existent entity
|
|
@@ -1308,7 +917,7 @@ describe("createDatabase", () => {
|
|
|
1308
917
|
unsubscribe();
|
|
1309
918
|
});
|
|
1310
919
|
it("should handle invalid entity ID with minArchetype", () => {
|
|
1311
|
-
const store =
|
|
920
|
+
const store = createTestDatabase();
|
|
1312
921
|
const observer = vi.fn();
|
|
1313
922
|
const unsubscribe = store.observe.entity(-1, store.archetypes.Position)(observer);
|
|
1314
923
|
// Should return null for invalid entity ID
|
|
@@ -1316,7 +925,7 @@ describe("createDatabase", () => {
|
|
|
1316
925
|
unsubscribe();
|
|
1317
926
|
});
|
|
1318
927
|
it("should maintain separate observations for different minArchetypes", () => {
|
|
1319
|
-
const store =
|
|
928
|
+
const store = createTestDatabase();
|
|
1320
929
|
// Create entity with position and health
|
|
1321
930
|
const entity = store.transactions.createPositionHealthEntity({
|
|
1322
931
|
position: { x: 1, y: 2, z: 3 },
|
|
@@ -1360,7 +969,7 @@ describe("createDatabase", () => {
|
|
|
1360
969
|
unsubscribeFull();
|
|
1361
970
|
});
|
|
1362
971
|
it("should handle component updates that don't affect minArchetype requirements", () => {
|
|
1363
|
-
const store =
|
|
972
|
+
const store = createTestDatabase();
|
|
1364
973
|
// Create entity with position and health
|
|
1365
974
|
const entity = store.transactions.createPositionHealthEntity({
|
|
1366
975
|
position: { x: 1, y: 2, z: 3 },
|
|
@@ -1399,7 +1008,7 @@ describe("createDatabase", () => {
|
|
|
1399
1008
|
});
|
|
1400
1009
|
describe("toData/fromData functionality", () => {
|
|
1401
1010
|
it("should serialize and deserialize database state correctly", () => {
|
|
1402
|
-
const store =
|
|
1011
|
+
const store = createTestDatabase();
|
|
1403
1012
|
// Create some entities and update resources
|
|
1404
1013
|
const entity1 = store.transactions.createPositionEntity({
|
|
1405
1014
|
position: { x: 1, y: 2, z: 3 }
|
|
@@ -1413,7 +1022,7 @@ describe("toData/fromData functionality", () => {
|
|
|
1413
1022
|
// Serialize the database
|
|
1414
1023
|
const serializedData = store.toData();
|
|
1415
1024
|
// Create a new database and restore from serialized data
|
|
1416
|
-
const newStore =
|
|
1025
|
+
const newStore = createTestDatabase();
|
|
1417
1026
|
newStore.fromData(serializedData);
|
|
1418
1027
|
// Verify entities are restored
|
|
1419
1028
|
const restoredEntities = newStore.select(["position"]);
|
|
@@ -1435,7 +1044,7 @@ describe("toData/fromData functionality", () => {
|
|
|
1435
1044
|
expect(newStore.resources.time).toEqual({ delta: 0.033, elapsed: 1.5 });
|
|
1436
1045
|
});
|
|
1437
1046
|
it("should notify all observers when database is restored from serialized data", () => {
|
|
1438
|
-
const store =
|
|
1047
|
+
const store = createTestDatabase();
|
|
1439
1048
|
// Create initial state
|
|
1440
1049
|
const entity = store.transactions.createPositionEntity({
|
|
1441
1050
|
position: { x: 1, y: 2, z: 3 }
|
|
@@ -1458,7 +1067,7 @@ describe("toData/fromData functionality", () => {
|
|
|
1458
1067
|
// Serialize the database
|
|
1459
1068
|
const serializedData = store.toData();
|
|
1460
1069
|
// Create a new database with different state
|
|
1461
|
-
const newStore =
|
|
1070
|
+
const newStore = createTestDatabase();
|
|
1462
1071
|
const newEntity = newStore.transactions.createFullEntity({
|
|
1463
1072
|
position: { x: 10, y: 20, z: 30 },
|
|
1464
1073
|
health: { current: 50, max: 100 },
|
|
@@ -1510,7 +1119,7 @@ describe("toData/fromData functionality", () => {
|
|
|
1510
1119
|
newUnsubscribeTransaction();
|
|
1511
1120
|
});
|
|
1512
1121
|
it("should notify observers even when no entities exist in restored data", () => {
|
|
1513
|
-
const store =
|
|
1122
|
+
const store = createTestDatabase();
|
|
1514
1123
|
// Set up observers on empty store
|
|
1515
1124
|
const positionObserver = vi.fn();
|
|
1516
1125
|
const timeObserver = vi.fn();
|
|
@@ -1525,7 +1134,7 @@ describe("toData/fromData functionality", () => {
|
|
|
1525
1134
|
// Serialize empty database
|
|
1526
1135
|
const serializedData = store.toData();
|
|
1527
1136
|
// Create a new database with some data
|
|
1528
|
-
const newStore =
|
|
1137
|
+
const newStore = createTestDatabase();
|
|
1529
1138
|
newStore.transactions.createPositionEntity({
|
|
1530
1139
|
position: { x: 1, y: 2, z: 3 }
|
|
1531
1140
|
});
|
|
@@ -1560,7 +1169,7 @@ describe("toData/fromData functionality", () => {
|
|
|
1560
1169
|
newUnsubscribeTransaction();
|
|
1561
1170
|
});
|
|
1562
1171
|
it("should handle entity observers correctly during restoration", () => {
|
|
1563
|
-
const store =
|
|
1172
|
+
const store = createTestDatabase();
|
|
1564
1173
|
// Create entity and set up observer
|
|
1565
1174
|
const entity = store.transactions.createPositionEntity({
|
|
1566
1175
|
position: { x: 1, y: 2, z: 3 }
|
|
@@ -1572,7 +1181,7 @@ describe("toData/fromData functionality", () => {
|
|
|
1572
1181
|
// Serialize the database
|
|
1573
1182
|
const serializedData = store.toData();
|
|
1574
1183
|
// Create a new database
|
|
1575
|
-
const newStore =
|
|
1184
|
+
const newStore = createTestDatabase();
|
|
1576
1185
|
// Set up observer on the new store for a different entity
|
|
1577
1186
|
const newEntity = newStore.transactions.createFullEntity({
|
|
1578
1187
|
position: { x: 10, y: 20, z: 30 },
|
|
@@ -1597,13 +1206,13 @@ describe("toData/fromData functionality", () => {
|
|
|
1597
1206
|
newUnsubscribe();
|
|
1598
1207
|
});
|
|
1599
1208
|
it("should preserve transaction functionality after restoration", () => {
|
|
1600
|
-
const store =
|
|
1209
|
+
const store = createTestDatabase();
|
|
1601
1210
|
// Create initial state
|
|
1602
1211
|
store.transactions.updateTime({ delta: 0.016, elapsed: 0 });
|
|
1603
1212
|
// Serialize the database
|
|
1604
1213
|
const serializedData = store.toData();
|
|
1605
1214
|
// Create a new database and restore
|
|
1606
|
-
const newStore =
|
|
1215
|
+
const newStore = createTestDatabase();
|
|
1607
1216
|
newStore.fromData(serializedData);
|
|
1608
1217
|
// Verify transactions still work
|
|
1609
1218
|
const entity = newStore.transactions.createPositionEntity({
|
|
@@ -1620,5 +1229,18 @@ describe("toData/fromData functionality", () => {
|
|
|
1620
1229
|
newStore.transactions.updateTime({ delta: 0.033, elapsed: 1.5 });
|
|
1621
1230
|
expect(newStore.resources.time).toEqual({ delta: 0.033, elapsed: 1.5 });
|
|
1622
1231
|
});
|
|
1232
|
+
it("all transient operations should be rolled back", async () => {
|
|
1233
|
+
const store = createTestDatabase();
|
|
1234
|
+
const promise = store.transactions.startGenerating(async function* () {
|
|
1235
|
+
yield { progress: 0 };
|
|
1236
|
+
yield { progress: 1 };
|
|
1237
|
+
});
|
|
1238
|
+
// Check that the result is a promise
|
|
1239
|
+
expect(promise).toBeInstanceOf(Promise);
|
|
1240
|
+
const result = await promise;
|
|
1241
|
+
expect(result).toBe(-1);
|
|
1242
|
+
const generating = await toPromise(store.observe.resources.generating);
|
|
1243
|
+
expect(generating).toBe(false);
|
|
1244
|
+
});
|
|
1623
1245
|
});
|
|
1624
1246
|
//# sourceMappingURL=create-database.test.js.map
|