@adobe/data 0.4.11 → 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.test.js +70 -550
- 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
|
@@ -422,8 +422,8 @@ describe("createDatabase", () => {
|
|
|
422
422
|
const values = store.read(entityId);
|
|
423
423
|
return values?.name === "Stream1" || values?.name === "Stream2";
|
|
424
424
|
});
|
|
425
|
-
//
|
|
426
|
-
expect(intermediateEntities.
|
|
425
|
+
// CRITICAL: Should have NO intermediate entities (rollback worked)
|
|
426
|
+
expect(intermediateEntities).toHaveLength(0);
|
|
427
427
|
// Verify observer was notified for each entity creation and rollback
|
|
428
428
|
// Now that rollback is observable, we should see more notifications
|
|
429
429
|
// The exact count isn't as important as ensuring rollback operations are observable
|
|
@@ -710,558 +710,78 @@ describe("createDatabase", () => {
|
|
|
710
710
|
expect(observer).toHaveBeenCalledTimes(3); // 1 for yield + 1 for rollback + 1 for return
|
|
711
711
|
unsubscribe();
|
|
712
712
|
});
|
|
713
|
-
it("should verify rollback behavior works correctly for
|
|
714
|
-
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
const finalYieldYieldEntity = entities.find(entityId => {
|
|
743
|
-
const values = store.read(entityId);
|
|
744
|
-
return values?.name === "Step3";
|
|
745
|
-
});
|
|
746
|
-
const finalYieldReturnEntity = entities.find(entityId => {
|
|
747
|
-
const values = store.read(entityId);
|
|
748
|
-
return values?.name === "StepB";
|
|
749
|
-
});
|
|
750
|
-
expect(finalYieldYieldEntity).toBeDefined();
|
|
751
|
-
expect(finalYieldReturnEntity).toBeDefined();
|
|
752
|
-
// Verify rollback worked correctly - only final values remain
|
|
753
|
-
const yieldYieldValues = store.read(finalYieldYieldEntity);
|
|
754
|
-
const yieldReturnValues = store.read(finalYieldReturnEntity);
|
|
755
|
-
expect(yieldYieldValues?.position).toEqual({ x: 3, y: 3, z: 3 });
|
|
756
|
-
expect(yieldYieldValues?.name).toBe("Step3");
|
|
757
|
-
expect(yieldReturnValues?.position).toEqual({ x: 20, y: 20, z: 20 });
|
|
758
|
-
expect(yieldReturnValues?.name).toBe("StepB");
|
|
759
|
-
// Verify intermediate entities were rolled back (not present)
|
|
760
|
-
// Now that rollback is working correctly and observably, this should work
|
|
761
|
-
// Note: Rollback operations may create additional entities during processing
|
|
762
|
-
// The key is that the final entities have the correct values
|
|
763
|
-
const intermediateEntities = entities.filter(entityId => {
|
|
764
|
-
const values = store.read(entityId);
|
|
765
|
-
return values?.name === "Step1" || values?.name === "Step2" || values?.name === "StepA";
|
|
766
|
-
});
|
|
767
|
-
// The exact count may vary due to rollback operations, but rollback should be working
|
|
768
|
-
expect(intermediateEntities.length >= 0);
|
|
769
|
-
unsubscribe();
|
|
770
|
-
});
|
|
771
|
-
it("should handle AsyncGenerator completion states correctly", async () => {
|
|
772
|
-
const store = createTestDatabase();
|
|
773
|
-
const observer = vi.fn();
|
|
774
|
-
const unsubscribe = store.observe.components.position(observer);
|
|
775
|
-
// Test generator that completes with yield (exhaustion)
|
|
776
|
-
async function* yieldExhaustion() {
|
|
777
|
-
yield { position: { x: 1, y: 1, z: 1 }, name: "Exhausted" };
|
|
778
|
-
}
|
|
779
|
-
// Test generator that completes with return
|
|
780
|
-
async function* returnCompletion() {
|
|
781
|
-
return { position: { x: 2, y: 2, z: 2 }, name: "Returned" };
|
|
782
|
-
}
|
|
783
|
-
// Execute both transactions
|
|
784
|
-
store.transactions.createPositionNameEntity(() => yieldExhaustion());
|
|
785
|
-
store.transactions.createPositionNameEntity(() => returnCompletion());
|
|
786
|
-
// Wait for processing
|
|
787
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
788
|
-
// Verify both completion patterns work
|
|
789
|
-
const entities = store.select(["position", "name"]);
|
|
790
|
-
const exhaustedEntity = entities.find(entityId => {
|
|
791
|
-
const values = store.read(entityId);
|
|
792
|
-
return values?.name === "Exhausted";
|
|
793
|
-
});
|
|
794
|
-
const returnedEntity = entities.find(entityId => {
|
|
795
|
-
const values = store.read(entityId);
|
|
796
|
-
return values?.name === "Returned";
|
|
797
|
-
});
|
|
798
|
-
expect(exhaustedEntity).toBeDefined();
|
|
799
|
-
expect(returnedEntity).toBeDefined();
|
|
800
|
-
// Verify the correct values for each completion pattern
|
|
801
|
-
const exhaustedValues = store.read(exhaustedEntity);
|
|
802
|
-
const returnedValues = store.read(returnedEntity);
|
|
803
|
-
expect(exhaustedValues?.position).toEqual({ x: 1, y: 1, z: 1 });
|
|
804
|
-
expect(exhaustedValues?.name).toBe("Exhausted");
|
|
805
|
-
expect(returnedValues?.position).toEqual({ x: 2, y: 2, z: 2 });
|
|
806
|
-
expect(returnedValues?.name).toBe("Returned");
|
|
807
|
-
unsubscribe();
|
|
808
|
-
});
|
|
809
|
-
it("should properly rollback resource values when they are set in intermediate steps but not in final step", async () => {
|
|
810
|
-
const store = createTestDatabase();
|
|
811
|
-
const timeObserver = vi.fn();
|
|
812
|
-
const unsubscribe = store.observe.resources.time(timeObserver);
|
|
813
|
-
// Clear initial notification
|
|
814
|
-
timeObserver.mockClear();
|
|
815
|
-
// Store original time value
|
|
816
|
-
const originalTime = { delta: 0.016, elapsed: 0 };
|
|
817
|
-
expect(store.resources.time).toEqual(originalTime);
|
|
818
|
-
// Create an async generator that sets time resource in intermediate steps but not in final step
|
|
819
|
-
async function* resourceRollbackTest() {
|
|
820
|
-
// Step 1: Set time to a new value
|
|
821
|
-
yield {
|
|
822
|
-
position: { x: 1, y: 1, z: 1 },
|
|
823
|
-
name: "Step1",
|
|
824
|
-
resourceUpdate: { time: { delta: 0.032, elapsed: 1 } }
|
|
825
|
-
};
|
|
826
|
-
// Step 2: Set time to another value
|
|
827
|
-
yield {
|
|
828
|
-
position: { x: 2, y: 2, z: 2 },
|
|
829
|
-
name: "Step2",
|
|
830
|
-
resourceUpdate: { time: { delta: 0.048, elapsed: 2 } }
|
|
831
|
-
};
|
|
832
|
-
// Final step: Only update position, no time resource update
|
|
833
|
-
return {
|
|
834
|
-
position: { x: 3, y: 3, z: 3 },
|
|
835
|
-
name: "FinalStep"
|
|
836
|
-
// Note: No resourceUpdate here
|
|
837
|
-
};
|
|
838
|
-
}
|
|
839
|
-
// Create a custom transaction that handles resource updates
|
|
840
|
-
const baseStore = createStore({ position: positionSchema, name: nameSchema }, { time: { default: { delta: 0.016, elapsed: 0 } } }, {
|
|
841
|
-
PositionName: ["position", "name"],
|
|
842
|
-
});
|
|
843
|
-
const customStore = createDatabase(baseStore, {
|
|
844
|
-
createWithResourceUpdate(t, args) {
|
|
845
|
-
// Create the entity
|
|
846
|
-
const entity = t.archetypes.PositionName.insert(args);
|
|
847
|
-
// Update resource if provided
|
|
848
|
-
if (args.resourceUpdate?.time) {
|
|
849
|
-
t.resources.time = args.resourceUpdate.time;
|
|
850
|
-
}
|
|
851
|
-
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 }
|
|
852
742
|
}
|
|
853
|
-
|
|
854
|
-
//
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
// The key is that the final resource value is correct
|
|
879
|
-
const finalTime = customStore.resources.time;
|
|
880
|
-
expect(finalTime).toBeDefined();
|
|
881
|
-
// The exact values may vary due to rollback operations, but rollback should be working
|
|
882
|
-
expect(typeof finalTime.delta).toBe('number');
|
|
883
|
-
expect(typeof finalTime.elapsed).toBe('number');
|
|
884
|
-
// Verify that the observer was called at least once
|
|
885
|
-
expect(customTimeObserver).toHaveBeenCalled();
|
|
886
|
-
customUnsubscribe();
|
|
887
|
-
unsubscribe();
|
|
888
|
-
});
|
|
889
|
-
it("should maintain resource values when they are set in the final step", async () => {
|
|
890
|
-
const store = createTestDatabase();
|
|
891
|
-
const timeObserver = vi.fn();
|
|
892
|
-
const unsubscribe = store.observe.resources.time(timeObserver);
|
|
893
|
-
// Clear initial notification
|
|
894
|
-
timeObserver.mockClear();
|
|
895
|
-
// Store original time value
|
|
896
|
-
const originalTime = { delta: 0.016, elapsed: 0 };
|
|
897
|
-
expect(store.resources.time).toEqual(originalTime);
|
|
898
|
-
// Create an async generator that sets time resource in the final step
|
|
899
|
-
async function* resourceFinalStepTest() {
|
|
900
|
-
// Step 1: No resource update
|
|
901
|
-
yield {
|
|
902
|
-
position: { x: 1, y: 1, z: 1 },
|
|
903
|
-
name: "Step1"
|
|
904
|
-
};
|
|
905
|
-
// Step 2: No resource update
|
|
906
|
-
yield {
|
|
907
|
-
position: { x: 2, y: 2, z: 2 },
|
|
908
|
-
name: "Step2"
|
|
909
|
-
};
|
|
910
|
-
// Final step: Update time resource
|
|
911
|
-
return {
|
|
912
|
-
position: { x: 3, y: 3, z: 3 },
|
|
913
|
-
name: "FinalStep",
|
|
914
|
-
resourceUpdate: { time: { delta: 0.064, elapsed: 3 } }
|
|
915
|
-
};
|
|
916
|
-
}
|
|
917
|
-
// Create a custom transaction that handles resource updates
|
|
918
|
-
const baseStore = createStore({ position: positionSchema, name: nameSchema }, { time: { default: { delta: 0.016, elapsed: 0 } } }, {
|
|
919
|
-
PositionName: ["position", "name"],
|
|
920
|
-
});
|
|
921
|
-
const customStore = createDatabase(baseStore, {
|
|
922
|
-
createWithResourceUpdate(t, args) {
|
|
923
|
-
// Create the entity
|
|
924
|
-
const entity = t.archetypes.PositionName.insert(args);
|
|
925
|
-
// Update resource if provided
|
|
926
|
-
if (args.resourceUpdate?.time) {
|
|
927
|
-
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";
|
|
928
768
|
}
|
|
929
|
-
return
|
|
930
|
-
|
|
931
|
-
});
|
|
932
|
-
// Set up observer on the custom store
|
|
933
|
-
const customTimeObserver = vi.fn();
|
|
934
|
-
const customUnsubscribe = customStore.observe.resources.time(customTimeObserver);
|
|
935
|
-
// Clear initial notification
|
|
936
|
-
customTimeObserver.mockClear();
|
|
937
|
-
// Execute transaction with async generator
|
|
938
|
-
customStore.transactions.createWithResourceUpdate(() => resourceFinalStepTest());
|
|
939
|
-
// Wait for all entities to be processed
|
|
940
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
941
|
-
// Verify the final entity was created
|
|
942
|
-
const entities = customStore.select(["position", "name"]);
|
|
943
|
-
const finalEntity = entities.find(entityId => {
|
|
944
|
-
const values = customStore.read(entityId);
|
|
945
|
-
return values?.name === "FinalStep";
|
|
946
|
-
});
|
|
947
|
-
expect(finalEntity).toBeDefined();
|
|
948
|
-
// CRITICAL: Verify that the time resource was updated to the final value
|
|
949
|
-
// because the final step set it, so it should persist
|
|
950
|
-
const expectedFinalTime = { delta: 0.064, elapsed: 3 };
|
|
951
|
-
expect(customStore.resources.time).toEqual(expectedFinalTime);
|
|
952
|
-
// Verify that the observer was called at least once
|
|
953
|
-
expect(customTimeObserver).toHaveBeenCalled();
|
|
954
|
-
customUnsubscribe();
|
|
955
|
-
unsubscribe();
|
|
956
|
-
});
|
|
957
|
-
it("should correctly set transient: true on all async generator transactions except the final one", async () => {
|
|
958
|
-
// This test is CRITICAL for the persistence service
|
|
959
|
-
// The persistence service depends on transient: true being set correctly
|
|
960
|
-
// for all intermediate transactions and transient: false for the final transaction
|
|
961
|
-
const store = createTestDatabase();
|
|
962
|
-
const transactionObserver = vi.fn();
|
|
963
|
-
const unsubscribe = store.observe.transactions(transactionObserver);
|
|
964
|
-
// Test case 1: Multiple yields (yield, yield, yield)
|
|
965
|
-
async function* multipleYields() {
|
|
966
|
-
yield { position: { x: 1, y: 1, z: 1 }, name: "Step1" };
|
|
967
|
-
yield { position: { x: 2, y: 2, z: 2 }, name: "Step2" };
|
|
968
|
-
yield { position: { x: 3, y: 3, z: 3 }, name: "Step3" };
|
|
969
|
-
}
|
|
970
|
-
// Test case 2: Yield then return (yield, return)
|
|
971
|
-
async function* yieldThenReturn() {
|
|
972
|
-
yield { position: { x: 10, y: 10, z: 10 }, name: "StepA" };
|
|
973
|
-
return { position: { x: 20, y: 20, z: 20 }, name: "StepB" };
|
|
974
|
-
}
|
|
975
|
-
// Test case 3: Return only (no yields)
|
|
976
|
-
async function* returnOnly() {
|
|
977
|
-
return { position: { x: 100, y: 200, z: 300 }, name: "ReturnOnly" };
|
|
978
|
-
}
|
|
979
|
-
// Execute all three transactions
|
|
980
|
-
store.transactions.createPositionNameEntity(() => multipleYields());
|
|
981
|
-
store.transactions.createPositionNameEntity(() => yieldThenReturn());
|
|
982
|
-
store.transactions.createPositionNameEntity(() => returnOnly());
|
|
983
|
-
// Wait for all entities to be processed
|
|
984
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
985
|
-
// Verify transaction observers were called for each step
|
|
986
|
-
// multipleYields: 3 transient + 3 rollbacks + 1 final = 7 calls
|
|
987
|
-
// yieldThenReturn: 1 transient + 1 rollback + 1 final = 3 calls
|
|
988
|
-
// returnOnly: 0 transient + 0 rollbacks + 1 final = 1 call
|
|
989
|
-
// Total: 11 calls
|
|
990
|
-
// Now that rollback is observable, we may get additional notifications
|
|
991
|
-
// The key is that we receive at least the minimum expected notifications
|
|
992
|
-
expect(transactionObserver).toHaveBeenCalledTimes(11);
|
|
993
|
-
// Collect all transaction results
|
|
994
|
-
const allTransactions = transactionObserver.mock.calls.map(call => call[0]);
|
|
995
|
-
// Debug: Let's see what we actually got
|
|
996
|
-
console.log('Total transactions:', allTransactions.length);
|
|
997
|
-
console.log('Transaction details:', allTransactions.map((t, i) => ({
|
|
998
|
-
index: i,
|
|
999
|
-
transient: t.transient,
|
|
1000
|
-
changedEntities: t.changedEntities.size
|
|
1001
|
-
})));
|
|
1002
|
-
// CRITICAL: Verify that ALL intermediate transactions have transient: true
|
|
1003
|
-
// and ALL final transactions have transient: false
|
|
1004
|
-
const transientTransactions = allTransactions.filter(t => t.transient);
|
|
1005
|
-
const finalTransactions = allTransactions.filter(t => !t.transient);
|
|
1006
|
-
// With the rollback fix, the exact counts may vary, but the key is:
|
|
1007
|
-
// 1. We have some transient transactions (for yields and rollbacks)
|
|
1008
|
-
// 2. We have some final transactions (for the actual results)
|
|
1009
|
-
// 3. The final entities have the correct values
|
|
1010
|
-
expect(transientTransactions.length).toBeGreaterThan(0);
|
|
1011
|
-
expect(finalTransactions.length).toBeGreaterThan(0);
|
|
1012
|
-
// Verify that transient transactions are truly intermediate (can be rolled back)
|
|
1013
|
-
// and final transactions are truly final (persist)
|
|
1014
|
-
const entities = store.select(["position", "name"]);
|
|
1015
|
-
// Only final entities should exist
|
|
1016
|
-
const finalEntities = entities.filter(entityId => {
|
|
1017
|
-
const values = store.read(entityId);
|
|
1018
|
-
return values?.name === "Step3" || values?.name === "StepB" || values?.name === "ReturnOnly";
|
|
1019
|
-
});
|
|
1020
|
-
expect(finalEntities).toHaveLength(3);
|
|
1021
|
-
// Intermediate entities should NOT exist (they were rolled back)
|
|
1022
|
-
// Now that rollback is working correctly and observably, this should work
|
|
1023
|
-
const intermediateEntities = entities.filter(entityId => {
|
|
1024
|
-
const values = store.read(entityId);
|
|
1025
|
-
return values?.name === "Step1" || values?.name === "Step2" || values?.name === "StepA";
|
|
1026
|
-
});
|
|
1027
|
-
// The exact count may vary due to rollback operations, but rollback should be working
|
|
1028
|
-
expect(intermediateEntities.length >= 0);
|
|
1029
|
-
unsubscribe();
|
|
1030
|
-
});
|
|
1031
|
-
it("should maintain transaction integrity with async operations", async () => {
|
|
1032
|
-
const store = createTestDatabase();
|
|
1033
|
-
const transactionObserver = vi.fn();
|
|
1034
|
-
const unsubscribe = store.observe.transactions(transactionObserver);
|
|
1035
|
-
// Create a promise that resolves to entity data
|
|
1036
|
-
const entityDataPromise = Promise.resolve({
|
|
1037
|
-
position: { x: 100, y: 200, z: 300 },
|
|
1038
|
-
name: "TransactionTest"
|
|
1039
|
-
});
|
|
1040
|
-
// Execute transaction with promise wrapped in function
|
|
1041
|
-
store.transactions.createPositionNameEntity(() => entityDataPromise);
|
|
1042
|
-
// Wait for the promise to resolve
|
|
1043
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1044
|
-
// Verify transaction observer was called with proper transaction result
|
|
1045
|
-
expect(transactionObserver).toHaveBeenCalledWith(expect.objectContaining({
|
|
1046
|
-
changedEntities: expect.any(Map),
|
|
1047
|
-
changedComponents: expect.any(Set),
|
|
1048
|
-
changedArchetypes: expect.any(Set),
|
|
1049
|
-
redo: expect.any(Array),
|
|
1050
|
-
undo: expect.any(Array)
|
|
1051
|
-
}));
|
|
1052
|
-
const result = transactionObserver.mock.calls[0][0];
|
|
1053
|
-
expect(result.changedEntities.size).toBe(1);
|
|
1054
|
-
expect(result.changedComponents.has("position")).toBe(true);
|
|
1055
|
-
expect(result.changedComponents.has("name")).toBe(true);
|
|
1056
|
-
unsubscribe();
|
|
1057
|
-
});
|
|
1058
|
-
it("should handle undoable property correctly in async generator transactions", async () => {
|
|
1059
|
-
const store = createTestDatabase();
|
|
1060
|
-
const transactionObserver = vi.fn();
|
|
1061
|
-
const unsubscribe = store.observe.transactions(transactionObserver);
|
|
1062
|
-
// Create an async generator that sets undoable property in intermediate transactions
|
|
1063
|
-
async function* undoableStream() {
|
|
1064
|
-
yield { position: { x: 1, y: 1, z: 1 }, name: "Step1" };
|
|
1065
|
-
yield { position: { x: 2, y: 2, z: 2 }, name: "Step2" };
|
|
1066
|
-
yield { position: { x: 3, y: 3, z: 3 }, name: "Step3" };
|
|
1067
|
-
}
|
|
1068
|
-
// Create a custom database with undoable transaction
|
|
1069
|
-
const baseStore = createStore({ position: positionSchema, name: nameSchema }, { time: { default: { delta: 0.016, elapsed: 0 } } }, {
|
|
1070
|
-
PositionName: ["position", "name"],
|
|
1071
|
-
});
|
|
1072
|
-
const customStore = createDatabase(baseStore, {
|
|
1073
|
-
createWithUndoable(t, args) {
|
|
1074
|
-
// Set undoable property for this transaction
|
|
1075
|
-
t.undoable = { coalesce: { operation: "create", name: args.name } };
|
|
1076
|
-
return t.archetypes.PositionName.insert(args);
|
|
1077
|
-
}
|
|
1078
|
-
});
|
|
1079
|
-
// Set up observer on the custom store
|
|
1080
|
-
const customTransactionObserver = vi.fn();
|
|
1081
|
-
const customUnsubscribe = customStore.observe.transactions(customTransactionObserver);
|
|
1082
|
-
// Execute transaction with async generator wrapped in function
|
|
1083
|
-
customStore.transactions.createWithUndoable(() => undoableStream());
|
|
1084
|
-
// Wait for all entities to be processed
|
|
1085
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1086
|
-
// Verify transaction observer was called multiple times (for each transient + final)
|
|
1087
|
-
// Now that rollback is observable, we may get additional notifications
|
|
1088
|
-
// The key is that we receive at least the minimum expected notifications
|
|
1089
|
-
expect(customTransactionObserver).toHaveBeenCalledTimes(7); // 3 transient + 3 rollbacks + 1 final
|
|
1090
|
-
// Check the transient transactions - they should have the undoable property
|
|
1091
|
-
const transientTransactionCall1 = customTransactionObserver.mock.calls[0]; // First transient
|
|
1092
|
-
const transientTransactionCall2 = customTransactionObserver.mock.calls[1]; // Second transient
|
|
1093
|
-
const transientTransactionCall3 = customTransactionObserver.mock.calls[2]; // Third transient
|
|
1094
|
-
expect(transientTransactionCall1[0].transient).toBe(true);
|
|
1095
|
-
expect(transientTransactionCall1[0].undoable).toEqual({ coalesce: { operation: "create", name: "Step1" } });
|
|
1096
|
-
expect(transientTransactionCall2[0].transient).toBe(true);
|
|
1097
|
-
// The undoable property might be null for rollback transactions
|
|
1098
|
-
// expect(transientTransactionCall2[0].undoable).toEqual({ coalesce: { operation: "create", name: "Step2" } });
|
|
1099
|
-
expect(transientTransactionCall3[0].transient).toBe(true);
|
|
1100
|
-
// The undoable property might be null for rollback transactions
|
|
1101
|
-
// expect(transientTransactionCall3[0].undoable).toEqual({ coalesce: { operation: "create", name: "Step3" } });
|
|
1102
|
-
// Check that the final non-transient transaction has the undoable property from the last transient transaction
|
|
1103
|
-
const finalTransactionCall = customTransactionObserver.mock.calls[6]; // Last call should be final transaction
|
|
1104
|
-
const finalTransactionResult = finalTransactionCall[0];
|
|
1105
|
-
expect(finalTransactionResult.transient).toBe(false);
|
|
1106
|
-
// The undoable property should be preserved from the last transient transaction
|
|
1107
|
-
expect(finalTransactionResult.undoable).toEqual({ coalesce: { operation: "create", name: "Step3" } });
|
|
1108
|
-
// POTENTIAL ISSUE: Transient transactions with undoable properties might cause problems
|
|
1109
|
-
// in undo-redo systems that expect only non-transient transactions to be undoable.
|
|
1110
|
-
// This test documents the current behavior for future consideration.
|
|
1111
|
-
unsubscribe();
|
|
1112
|
-
customUnsubscribe();
|
|
1113
|
-
});
|
|
1114
|
-
it("should demonstrate potential issue with undo-redo system and transient transactions", async () => {
|
|
1115
|
-
// This test demonstrates a potential issue where transient transactions with undoable properties
|
|
1116
|
-
// might be incorrectly handled by undo-redo systems that expect only non-transient transactions
|
|
1117
|
-
// to be undoable.
|
|
1118
|
-
const baseStore = createStore({ position: positionSchema, name: nameSchema }, { time: { default: { delta: 0.016, elapsed: 0 } } }, {
|
|
1119
|
-
PositionName: ["position", "name"],
|
|
1120
|
-
});
|
|
1121
|
-
const customStore = createDatabase(baseStore, {
|
|
1122
|
-
createWithUndoable(t, args) {
|
|
1123
|
-
// Set undoable property for this transaction
|
|
1124
|
-
t.undoable = { coalesce: { operation: "create", name: args.name } };
|
|
1125
|
-
return t.archetypes.PositionName.insert(args);
|
|
1126
|
-
}
|
|
1127
|
-
});
|
|
1128
|
-
const transactionObserver = vi.fn();
|
|
1129
|
-
const unsubscribe = customStore.observe.transactions(transactionObserver);
|
|
1130
|
-
// Create an async generator that yields multiple values
|
|
1131
|
-
async function* undoableStream() {
|
|
1132
|
-
yield { position: { x: 1, y: 1, z: 1 }, name: "Step1" };
|
|
1133
|
-
yield { position: { x: 2, y: 2, z: 2 }, name: "Step2" };
|
|
1134
|
-
yield { position: { x: 3, y: 3, z: 3 }, name: "Step3" };
|
|
1135
|
-
}
|
|
1136
|
-
// Execute transaction with async generator
|
|
1137
|
-
customStore.transactions.createWithUndoable(() => undoableStream());
|
|
1138
|
-
// Wait for processing
|
|
1139
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1140
|
-
// Collect all transaction results
|
|
1141
|
-
const allTransactions = transactionObserver.mock.calls.map(call => call[0]);
|
|
1142
|
-
// Verify we have the expected number of transactions
|
|
1143
|
-
// Now that rollback is observable, we may get additional notifications
|
|
1144
|
-
// The key is that we receive at least the minimum expected notifications
|
|
1145
|
-
expect(allTransactions).toHaveLength(7); // 3 transient + 3 rollbacks + 1 final
|
|
1146
|
-
// Check that transient transactions have undoable properties
|
|
1147
|
-
const transientTransactions = allTransactions.filter(t => t.transient);
|
|
1148
|
-
expect(transientTransactions).toHaveLength(6); // 3 original + 3 rollback transactions
|
|
1149
|
-
// POTENTIAL ISSUE: Transient transactions with undoable properties
|
|
1150
|
-
// This could cause problems in undo-redo systems that:
|
|
1151
|
-
// 1. Expect only non-transient transactions to be undoable
|
|
1152
|
-
// 2. Might try to undo transient transactions incorrectly
|
|
1153
|
-
// 3. Could have issues with coalescing logic that doesn't account for transient transactions
|
|
1154
|
-
// The current implementation preserves the undoable property from the last transient transaction
|
|
1155
|
-
// in the final non-transient transaction, which might be the intended behavior.
|
|
1156
|
-
// However, this could lead to unexpected behavior in undo-redo systems.
|
|
1157
|
-
const finalTransaction = allTransactions.find(t => !t.transient);
|
|
1158
|
-
expect(finalTransaction).toBeDefined();
|
|
1159
|
-
expect(finalTransaction.undoable).toEqual({ coalesce: { operation: "create", name: "Step3" } });
|
|
1160
|
-
unsubscribe();
|
|
1161
|
-
});
|
|
1162
|
-
it("should demonstrate that rollback operations are now observable and working correctly", async () => {
|
|
1163
|
-
// Create a custom store with the flag resource and createWithFlag transaction
|
|
1164
|
-
const baseStore = createStore({ position: positionSchema, name: nameSchema }, { flag: { default: false } }, {
|
|
1165
|
-
PositionName: ["position", "name"],
|
|
1166
|
-
});
|
|
1167
|
-
const customStore = createDatabase(baseStore, {
|
|
1168
|
-
createWithFlag(t, args) {
|
|
1169
|
-
// Create the entity
|
|
1170
|
-
const entity = t.archetypes.PositionName.insert(args);
|
|
1171
|
-
// Set the flag resource only if setFlag is true
|
|
1172
|
-
if (args.setFlag) {
|
|
1173
|
-
t.resources.flag = true;
|
|
1174
|
-
}
|
|
1175
|
-
return entity;
|
|
1176
|
-
}
|
|
1177
|
-
});
|
|
1178
|
-
const flagObserver = vi.fn();
|
|
1179
|
-
const unsubscribe = customStore.observe.resources.flag(flagObserver);
|
|
1180
|
-
// Create an async generator that yields true then false (no return)
|
|
1181
|
-
async function* flagToggleStream() {
|
|
1182
|
-
yield { position: { x: 1, y: 1, z: 1 }, name: "Step1", setFlag: true };
|
|
1183
|
-
yield { position: { x: 2, y: 2, z: 2 }, name: "Step2", setFlag: false };
|
|
1184
|
-
}
|
|
1185
|
-
customStore.transactions.createWithFlag(() => flagToggleStream());
|
|
1186
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1187
|
-
// SUCCESS: Rollback operations are now observable and working correctly!
|
|
1188
|
-
// The flag should end up as false (the final value from Step2)
|
|
1189
|
-
// Note: Rollback operations may change resource values during processing
|
|
1190
|
-
// The key is that the final resource value is correct
|
|
1191
|
-
const finalFlag = customStore.resources.flag;
|
|
1192
|
-
expect(finalFlag).toBeDefined();
|
|
1193
|
-
// The exact value may vary due to rollback operations, but rollback should be working
|
|
1194
|
-
expect(typeof finalFlag).toBe('boolean');
|
|
1195
|
-
// The observer should have been called at least twice:
|
|
1196
|
-
// - Once when the flag was set to true (Step1)
|
|
1197
|
-
// - Once when the flag was set to false (Step2)
|
|
1198
|
-
// The observer should have been called with the value true (from Step1)
|
|
1199
|
-
expect(flagObserver).toHaveBeenCalledWith(true);
|
|
1200
|
-
// The observer should have been called with the value false (from Step2)
|
|
1201
|
-
expect(flagObserver).toHaveBeenCalledWith(false);
|
|
1202
|
-
// SUCCESS: The rollback operations are now observable through the database's transaction system.
|
|
1203
|
-
// The key points are:
|
|
1204
|
-
// 1. The final flag value is correct (false)
|
|
1205
|
-
// 2. Rollback operations are observable (observer was notified of both values)
|
|
1206
|
-
// 3. The database state and observable state are in sync
|
|
1207
|
-
// 4. Intermediate entities are properly rolled back (only final entity remains)
|
|
1208
|
-
unsubscribe();
|
|
1209
|
-
});
|
|
1210
|
-
it("should demonstrate the bug: rollback operations bypass the observable layer", async () => {
|
|
1211
|
-
// This test proves that rollback operations are NOT observable
|
|
1212
|
-
// even though they are working at the store level
|
|
1213
|
-
// Create a custom store with the flag resource and createWithFlag transaction
|
|
1214
|
-
const baseStore = createStore({ position: positionSchema, name: nameSchema }, { flag: { default: false } }, {
|
|
1215
|
-
PositionName: ["position", "name"],
|
|
1216
|
-
});
|
|
1217
|
-
const customStore = createDatabase(baseStore, {
|
|
1218
|
-
createWithFlag(t, args) {
|
|
1219
|
-
// Create the entity
|
|
1220
|
-
const entity = t.archetypes.PositionName.insert(args);
|
|
1221
|
-
// Set the flag resource only if setFlag is true
|
|
1222
|
-
if (args.setFlag) {
|
|
1223
|
-
t.resources.flag = true;
|
|
769
|
+
else if (pattern.name.includes("yield-then-return")) {
|
|
770
|
+
return values?.name === "StepA";
|
|
1224
771
|
}
|
|
1225
|
-
return
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
yield { position: { x: 1, y: 1, z: 1 }, name: "Step1", setFlag: true };
|
|
1238
|
-
yield { position: { x: 2, y: 2, z: 2 }, name: "Step2", setFlag: false };
|
|
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();
|
|
1239
784
|
}
|
|
1240
|
-
customStore.transactions.createWithFlag(() => flagToggleStream());
|
|
1241
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1242
|
-
// SUCCESS: Rollback is working at the store level
|
|
1243
|
-
// The flag should end up as false (the final value from Step2)
|
|
1244
|
-
// Note: Rollback operations may change resource values during processing
|
|
1245
|
-
// The key is that the final resource value is correct and rollback is working
|
|
1246
|
-
const finalFlag = customStore.resources.flag;
|
|
1247
|
-
expect(finalFlag).toBeDefined();
|
|
1248
|
-
// The exact value may vary due to rollback operations, but rollback should be working
|
|
1249
|
-
expect(typeof finalFlag).toBe('boolean');
|
|
1250
|
-
// The observer should have been called at least twice:
|
|
1251
|
-
// - Once when the flag was set to true (Step1)
|
|
1252
|
-
// - Once when the flag was set to false (Step2)
|
|
1253
|
-
// The observer should have been called with the value true (from Step1)
|
|
1254
|
-
expect(flagObserver).toHaveBeenCalledWith(true);
|
|
1255
|
-
// The observer should have been called with the value false (from Step2)
|
|
1256
|
-
expect(flagObserver).toHaveBeenCalledWith(false);
|
|
1257
|
-
// SUCCESS: The rollback operations are now observable through the database's transaction system.
|
|
1258
|
-
// The key points are:
|
|
1259
|
-
// 1. The final flag value is correct (false)
|
|
1260
|
-
// 2. Rollback operations are observable (observer was notified of both values)
|
|
1261
|
-
// 3. The database state and observable state are in sync
|
|
1262
|
-
// 4. Intermediate entities are properly rolled back (only final entity remains)
|
|
1263
|
-
unsubscribeFlag();
|
|
1264
|
-
unsubscribeEntity();
|
|
1265
785
|
});
|
|
1266
786
|
});
|
|
1267
787
|
describe("entity observation with minArchetype filtering", () => {
|