@voidhash/mimic-react 1.0.0-beta.14 → 1.0.0-beta.15

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @voidhash/mimic-react@1.0.0-beta.14 build /home/runner/work/mimic/mimic/packages/mimic-react
2
+ > @voidhash/mimic-react@1.0.0-beta.15 build /home/runner/work/mimic/mimic/packages/mimic-react
3
3
  > tsdown
4
4
 
5
5
  ℹ tsdown v0.18.2 powered by rolldown v1.0.0-beta.55
@@ -38,7 +38,7 @@
38
38
  ℹ [CJS] dist/zustand/middleware.d.cts 2.13 kB │ gzip: 0.85 kB
39
39
  ℹ [CJS] dist/zustand/useDraft.d.cts 1.54 kB │ gzip: 0.66 kB
40
40
  ℹ [CJS] 15 files, total: 28.71 kB
41
- ✔ Build complete in 4092ms
41
+ ✔ Build complete in 3497ms
42
42
  ℹ [ESM] dist/zustand-commander/index.mjs  0.53 kB │ gzip: 0.21 kB
43
43
  ℹ [ESM] dist/zustand/index.mjs  0.11 kB │ gzip: 0.09 kB
44
44
  ℹ [ESM] dist/index.mjs  0.01 kB │ gzip: 0.03 kB
@@ -73,4 +73,4 @@
73
73
  ℹ [ESM] dist/zustand/middleware.d.mts  2.13 kB │ gzip: 0.85 kB
74
74
  ℹ [ESM] dist/zustand/useDraft.d.mts  1.54 kB │ gzip: 0.66 kB
75
75
  ℹ [ESM] 33 files, total: 97.77 kB
76
- ✔ Build complete in 4138ms
76
+ ✔ Build complete in 3504ms
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voidhash/mimic-react",
3
- "version": "1.0.0-beta.14",
3
+ "version": "1.0.0-beta.15",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,12 +37,12 @@
37
37
  "typescript": "5.8.3",
38
38
  "vite-tsconfig-paths": "^5.1.4",
39
39
  "vitest": "^3.2.4",
40
- "@voidhash/tsconfig": "1.0.0-beta.14"
40
+ "@voidhash/tsconfig": "1.0.0-beta.15"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "effect": "^3.16.0",
44
44
  "react": "^18.0.0 || ^19.0.0",
45
- "@voidhash/mimic": "1.0.0-beta.14"
45
+ "@voidhash/mimic": "1.0.0-beta.15"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "tsdown",
@@ -668,6 +668,327 @@ describe("Command Dispatch in Context", () => {
668
668
  });
669
669
  });
670
670
 
671
+ describe("ctx.transaction routing (draft vs document)", () => {
672
+ it("should route ctx.transaction to document.transaction when no draft is active", () => {
673
+ const commander = createCommander<TestState>();
674
+
675
+ // Track calls to document.transaction
676
+ const transactionCalls: Array<(root: any) => void> = [];
677
+ const mockDocument = {
678
+ transaction: (fn: (root: any) => void) => {
679
+ transactionCalls.push(fn);
680
+ },
681
+ };
682
+
683
+ // Create store with mimic slice containing the mock document
684
+ const store = createStore<TestStore & { mimic: { document: typeof mockDocument } }>(
685
+ commander.middleware(() => ({
686
+ count: 0,
687
+ items: [],
688
+ selectedId: null,
689
+ mimic: { document: mockDocument },
690
+ }))
691
+ );
692
+
693
+ // Create a command that uses ctx.transaction
694
+ const updateViaTransaction = commander.action<{ value: number }>(
695
+ (ctx, params) => {
696
+ ctx.transaction((root: any) => {
697
+ root.count.set(params.value);
698
+ });
699
+ }
700
+ );
701
+
702
+ const storeApi = store as unknown as StoreApi<TestStore & { mimic: { document: typeof mockDocument } }>;
703
+
704
+ // Build a proper context with transaction routing
705
+ const ctx: CommandContext<TestStore> = {
706
+ getState: () => storeApi.getState(),
707
+ setState: (partial: Partial<TestStore>) => storeApi.setState(partial as any),
708
+ dispatch: <TParams, TReturn>(cmd: Command<TestStore, TParams, TReturn>) => {
709
+ return (params: TParams): TReturn => cmd.fn(ctx, params);
710
+ },
711
+ transaction: (fn: (root: any) => void) => {
712
+ const state = storeApi.getState();
713
+ const draft = state._commander.activeDraft;
714
+ if (draft) {
715
+ draft.update(fn);
716
+ } else {
717
+ (state as any).mimic.document.transaction(fn);
718
+ }
719
+ },
720
+ };
721
+
722
+ // Execute command - should route to document.transaction
723
+ updateViaTransaction.fn(ctx, { value: 42 });
724
+
725
+ expect(transactionCalls.length).toBe(1);
726
+ });
727
+
728
+ it("should route ctx.transaction to draft.update when draft is active", () => {
729
+ const commander = createCommander<TestState>();
730
+
731
+ // Track calls to both document.transaction and draft.update
732
+ const documentTransactionCalls: Array<(root: any) => void> = [];
733
+ const draftUpdateCalls: Array<(root: any) => void> = [];
734
+
735
+ const mockDocument = {
736
+ transaction: (fn: (root: any) => void) => {
737
+ documentTransactionCalls.push(fn);
738
+ },
739
+ };
740
+
741
+ const mockDraft = {
742
+ update: (fn: (root: any) => void) => {
743
+ draftUpdateCalls.push(fn);
744
+ },
745
+ commit: () => {},
746
+ discard: () => {},
747
+ id: "mock-draft-id",
748
+ };
749
+
750
+ // Create store with mimic slice
751
+ const store = createStore<TestStore & { mimic: { document: typeof mockDocument } }>(
752
+ commander.middleware(() => ({
753
+ count: 0,
754
+ items: [],
755
+ selectedId: null,
756
+ mimic: { document: mockDocument },
757
+ }))
758
+ );
759
+
760
+ // Set the active draft
761
+ store.setState((state) => ({
762
+ ...state,
763
+ _commander: {
764
+ ...state._commander,
765
+ activeDraft: mockDraft as any,
766
+ },
767
+ }));
768
+
769
+ // Create a command that uses ctx.transaction
770
+ const updateViaTransaction = commander.action<{ value: number }>(
771
+ (ctx, params) => {
772
+ ctx.transaction((root: any) => {
773
+ root.count.set(params.value);
774
+ });
775
+ }
776
+ );
777
+
778
+ const storeApi = store as unknown as StoreApi<TestStore & { mimic: { document: typeof mockDocument } }>;
779
+
780
+ // Build a proper context with transaction routing
781
+ const ctx: CommandContext<TestStore> = {
782
+ getState: () => storeApi.getState(),
783
+ setState: (partial: Partial<TestStore>) => storeApi.setState(partial as any),
784
+ dispatch: <TParams, TReturn>(cmd: Command<TestStore, TParams, TReturn>) => {
785
+ return (params: TParams): TReturn => cmd.fn(ctx, params);
786
+ },
787
+ transaction: (fn: (root: any) => void) => {
788
+ const state = storeApi.getState();
789
+ const draft = state._commander.activeDraft;
790
+ if (draft) {
791
+ draft.update(fn);
792
+ } else {
793
+ (state as any).mimic.document.transaction(fn);
794
+ }
795
+ },
796
+ };
797
+
798
+ // Execute command - should route to draft.update, NOT document.transaction
799
+ updateViaTransaction.fn(ctx, { value: 42 });
800
+
801
+ expect(draftUpdateCalls.length).toBe(1);
802
+ expect(documentTransactionCalls.length).toBe(0);
803
+ });
804
+
805
+ it("should never call document.transaction while draft is active (explicit verification)", () => {
806
+ const commander = createCommander<TestState>();
807
+
808
+ // Track ALL calls
809
+ const documentTransactionCalls: Array<{ fn: (root: any) => void; timestamp: number }> = [];
810
+ const draftUpdateCalls: Array<{ fn: (root: any) => void; timestamp: number }> = [];
811
+
812
+ const mockDocument = {
813
+ transaction: (fn: (root: any) => void) => {
814
+ documentTransactionCalls.push({ fn, timestamp: Date.now() });
815
+ },
816
+ };
817
+
818
+ const mockDraft = {
819
+ update: (fn: (root: any) => void) => {
820
+ draftUpdateCalls.push({ fn, timestamp: Date.now() });
821
+ },
822
+ commit: () => {},
823
+ discard: () => {},
824
+ id: "mock-draft-id",
825
+ };
826
+
827
+ // Create store
828
+ const store = createStore<TestStore & { mimic: { document: typeof mockDocument } }>(
829
+ commander.middleware(() => ({
830
+ count: 0,
831
+ items: [],
832
+ selectedId: null,
833
+ mimic: { document: mockDocument },
834
+ }))
835
+ );
836
+
837
+ const storeApi = store as unknown as StoreApi<TestStore & { mimic: { document: typeof mockDocument } }>;
838
+
839
+ // Helper to build context
840
+ const buildCtx = (): CommandContext<TestStore> => ({
841
+ getState: () => storeApi.getState(),
842
+ setState: (partial: Partial<TestStore>) => storeApi.setState(partial as any),
843
+ dispatch: <TParams, TReturn>(cmd: Command<TestStore, TParams, TReturn>) => {
844
+ return (params: TParams): TReturn => cmd.fn(buildCtx(), params);
845
+ },
846
+ transaction: (fn: (root: any) => void) => {
847
+ const state = storeApi.getState();
848
+ const draft = state._commander.activeDraft;
849
+ if (draft) {
850
+ draft.update(fn);
851
+ } else {
852
+ (state as any).mimic.document.transaction(fn);
853
+ }
854
+ },
855
+ });
856
+
857
+ // Command that uses transaction
858
+ const doUpdate = commander.action<{ value: number }>(
859
+ (ctx, params) => {
860
+ ctx.transaction((root: any) => {
861
+ root.count.set(params.value);
862
+ });
863
+ }
864
+ );
865
+
866
+ // Test 1: No draft - should go to document.transaction
867
+ doUpdate.fn(buildCtx(), { value: 1 });
868
+ expect(documentTransactionCalls.length).toBe(1);
869
+ expect(draftUpdateCalls.length).toBe(0);
870
+
871
+ // Test 2: Set active draft
872
+ store.setState((state) => ({
873
+ ...state,
874
+ _commander: {
875
+ ...state._commander,
876
+ activeDraft: mockDraft as any,
877
+ },
878
+ }));
879
+
880
+ // Test 3: With draft active - should go to draft.update
881
+ doUpdate.fn(buildCtx(), { value: 2 });
882
+ expect(documentTransactionCalls.length).toBe(1); // Still 1 - no new calls
883
+ expect(draftUpdateCalls.length).toBe(1);
884
+
885
+ // Test 4: Multiple updates while draft is active
886
+ doUpdate.fn(buildCtx(), { value: 3 });
887
+ doUpdate.fn(buildCtx(), { value: 4 });
888
+ doUpdate.fn(buildCtx(), { value: 5 });
889
+
890
+ expect(documentTransactionCalls.length).toBe(1); // Still 1 - no new calls
891
+ expect(draftUpdateCalls.length).toBe(4); // 4 draft updates
892
+
893
+ // Test 5: Clear draft
894
+ store.setState((state) => ({
895
+ ...state,
896
+ _commander: {
897
+ ...state._commander,
898
+ activeDraft: null,
899
+ },
900
+ }));
901
+
902
+ // Test 6: Without draft - should go back to document.transaction
903
+ doUpdate.fn(buildCtx(), { value: 6 });
904
+ expect(documentTransactionCalls.length).toBe(2);
905
+ expect(draftUpdateCalls.length).toBe(4);
906
+ });
907
+
908
+ it("should route nested command dispatches to draft when active", () => {
909
+ const commander = createCommander<TestState>();
910
+
911
+ const documentTransactionCalls: Array<(root: any) => void> = [];
912
+ const draftUpdateCalls: Array<(root: any) => void> = [];
913
+
914
+ const mockDocument = {
915
+ transaction: (fn: (root: any) => void) => {
916
+ documentTransactionCalls.push(fn);
917
+ },
918
+ };
919
+
920
+ const mockDraft = {
921
+ update: (fn: (root: any) => void) => {
922
+ draftUpdateCalls.push(fn);
923
+ },
924
+ commit: () => {},
925
+ discard: () => {},
926
+ id: "mock-draft-id",
927
+ };
928
+
929
+ const store = createStore<TestStore & { mimic: { document: typeof mockDocument } }>(
930
+ commander.middleware(() => ({
931
+ count: 0,
932
+ items: [],
933
+ selectedId: null,
934
+ mimic: { document: mockDocument },
935
+ }))
936
+ );
937
+
938
+ // Set active draft
939
+ store.setState((state) => ({
940
+ ...state,
941
+ _commander: {
942
+ ...state._commander,
943
+ activeDraft: mockDraft as any,
944
+ },
945
+ }));
946
+
947
+ const storeApi = store as unknown as StoreApi<TestStore & { mimic: { document: typeof mockDocument } }>;
948
+
949
+ const buildCtx = (): CommandContext<TestStore> => ({
950
+ getState: () => storeApi.getState(),
951
+ setState: (partial: Partial<TestStore>) => storeApi.setState(partial as any),
952
+ dispatch: <TParams, TReturn>(cmd: Command<TestStore, TParams, TReturn>) => {
953
+ return (params: TParams): TReturn => cmd.fn(buildCtx(), params);
954
+ },
955
+ transaction: (fn: (root: any) => void) => {
956
+ const state = storeApi.getState();
957
+ const draft = state._commander.activeDraft;
958
+ if (draft) {
959
+ draft.update(fn);
960
+ } else {
961
+ (state as any).mimic.document.transaction(fn);
962
+ }
963
+ },
964
+ });
965
+
966
+ // Inner command that uses transaction
967
+ const setCount = commander.action<{ value: number }>(
968
+ (ctx, params) => {
969
+ ctx.transaction((root: any) => {
970
+ root.count.set(params.value);
971
+ });
972
+ }
973
+ );
974
+
975
+ // Outer command that dispatches inner command
976
+ const setMultiple = commander.action<{ values: number[] }>(
977
+ (ctx, params) => {
978
+ for (const value of params.values) {
979
+ ctx.dispatch(setCount)({ value });
980
+ }
981
+ }
982
+ );
983
+
984
+ // Execute outer command - all nested transactions should go to draft
985
+ setMultiple.fn(buildCtx(), { values: [1, 2, 3, 4, 5] });
986
+
987
+ expect(draftUpdateCalls.length).toBe(5);
988
+ expect(documentTransactionCalls.length).toBe(0);
989
+ });
990
+ });
991
+
671
992
  describe("Undo/Redo Integration", () => {
672
993
  it("should handle multiple undo operations", () => {
673
994
  const commander = createCommander<TestState>();