@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.
@@ -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
- // The exact count may vary due to rollback operations, but rollback should be working
426
- expect(intermediateEntities.length >= 0);
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 both yield-yield and yield-return patterns", async () => {
714
- const store = createTestDatabase();
715
- const transactionObserver = vi.fn();
716
- const unsubscribe = store.observe.transactions(transactionObserver);
717
- // Test yield-yield pattern
718
- 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
- // Test yield-return pattern
724
- async function* yieldReturnPattern() {
725
- yield { position: { x: 10, y: 10, z: 10 }, name: "StepA" };
726
- return { position: { x: 20, y: 20, z: 20 }, name: "StepB" };
727
- }
728
- // Execute both transactions
729
- store.transactions.createPositionNameEntity(() => yieldYieldPattern());
730
- store.transactions.createPositionNameEntity(() => yieldReturnPattern());
731
- // Wait for processing
732
- await new Promise(resolve => setTimeout(resolve, 10));
733
- // Verify transaction observers were called for each step
734
- // yieldYieldPattern: 3 transient + 3 rollbacks + 1 final = 7 calls
735
- // yieldReturnPattern: 1 transient + 1 rollback + 1 final = 3 calls
736
- // Total: 10 calls
737
- // Now that rollback is observable, we may get additional notifications
738
- // The key is that we receive at least the minimum expected notifications
739
- expect(transactionObserver).toHaveBeenCalledTimes(10);
740
- // Verify the final entities have the correct values
741
- const entities = store.select(["position", "name"]);
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
- // Set up observer on the custom store
855
- const customTimeObserver = vi.fn();
856
- const customUnsubscribe = customStore.observe.resources.time(customTimeObserver);
857
- // Clear initial notification
858
- customTimeObserver.mockClear();
859
- // Execute transaction with async generator
860
- customStore.transactions.createWithResourceUpdate(() => resourceRollbackTest());
861
- // Wait for all entities to be processed
862
- await new Promise(resolve => setTimeout(resolve, 10));
863
- // Verify the final entity was created
864
- const entities = customStore.select(["position", "name"]);
865
- const finalEntity = entities.find(entityId => {
866
- const values = customStore.read(entityId);
867
- return values?.name === "FinalStep";
868
- });
869
- expect(finalEntity).toBeDefined();
870
- const finalEntityValues = customStore.read(finalEntity);
871
- expect(finalEntityValues?.position).toEqual({ x: 3, y: 3, z: 3 });
872
- expect(finalEntityValues?.name).toBe("FinalStep");
873
- // Verify that the time resource was rolled back to its original value
874
- // because the final step didn't set it, so the rollback mechanism should have
875
- // restored the original value
876
- // Now that rollback is working correctly and observably, this should work
877
- // Note: Rollback operations may change resource values during processing
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 entity;
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 entity;
1226
- }
1227
- });
1228
- const flagObserver = vi.fn();
1229
- const entityObserver = vi.fn();
1230
- const unsubscribeFlag = customStore.observe.resources.flag(flagObserver);
1231
- const unsubscribeEntity = customStore.observe.entity(1)(entityObserver);
1232
- // Clear initial notifications
1233
- flagObserver.mockClear();
1234
- entityObserver.mockClear();
1235
- // Create an async generator that yields true then false (no return)
1236
- async function* flagToggleStream() {
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", () => {