coaction 3.0.0 → 3.1.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/local.js CHANGED
@@ -2,30 +2,41 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let mutative = require("mutative");
3
3
  let alien_signals = require("alien-signals");
4
4
  //#region packages/core/src/lifecycle.ts
5
- const readyStores = /* @__PURE__ */ new WeakSet();
6
- const readyCallbacks = /* @__PURE__ */ new WeakMap();
5
+ const storeReadyRuntimeSymbol = Symbol.for("coaction.lifecycle.ready");
6
+ const getStoreReadyRuntime = (store, create = false) => {
7
+ const target = store;
8
+ const existing = target[storeReadyRuntimeSymbol];
9
+ if (existing || !create) return existing;
10
+ const runtime = {
11
+ callbacks: /* @__PURE__ */ new Set(),
12
+ ready: false
13
+ };
14
+ Object.defineProperty(target, storeReadyRuntimeSymbol, {
15
+ configurable: true,
16
+ enumerable: true,
17
+ value: runtime,
18
+ writable: true
19
+ });
20
+ return runtime;
21
+ };
7
22
  const onStoreReady = (store, callback) => {
8
- if (readyStores.has(store)) {
23
+ const runtime = getStoreReadyRuntime(store, true);
24
+ if (runtime.ready) {
9
25
  callback();
10
26
  return () => void 0;
11
27
  }
12
- let callbacks = readyCallbacks.get(store);
13
- if (!callbacks) {
14
- callbacks = /* @__PURE__ */ new Set();
15
- readyCallbacks.set(store, callbacks);
16
- }
17
- callbacks.add(callback);
28
+ runtime.callbacks.add(callback);
18
29
  return () => {
19
- callbacks?.delete(callback);
30
+ runtime.callbacks.delete(callback);
20
31
  };
21
32
  };
22
33
  const markStoreReady = (store) => {
23
- readyStores.add(store);
24
- const callbacks = readyCallbacks.get(store);
25
- if (!callbacks) return;
26
- readyCallbacks.delete(store);
34
+ const runtime = getStoreReadyRuntime(store, true);
35
+ if (runtime.ready) return;
36
+ runtime.ready = true;
37
+ const callbacks = [...runtime.callbacks];
38
+ runtime.callbacks.clear();
27
39
  callbacks.forEach((callback) => callback());
28
- callbacks.clear();
29
40
  };
30
41
  //#endregion
31
42
  //#region packages/core/src/applyMiddlewares.ts
@@ -78,15 +89,64 @@ const warnDroppedUnsafePatches = (unsafePatches, source) => {
78
89
  };
79
90
  const sanitizePatches = (patches, options = {}) => {
80
91
  if (options.warnOnDropped) warnDroppedUnsafePatches(getUnsafePatchPaths(patches), options.source ?? "patches");
92
+ const seen = /* @__PURE__ */ new WeakMap();
81
93
  return patches?.filter((patch) => !hasUnsafePatchPath(patch.path)).map((patch) => Object.prototype.hasOwnProperty.call(patch, "value") ? {
82
94
  ...patch,
83
- value: sanitizeReplacementState(patch.value)
95
+ value: sanitizeReplacementState(patch.value, seen)
84
96
  } : patch);
85
97
  };
86
98
  const sanitizeCheckedPatches = (patches, source) => {
87
99
  assertSafePatches(patches, source);
88
100
  return sanitizePatches(patches) ?? [];
89
101
  };
102
+ const createRootReplacementPatches = (currentState, nextState) => {
103
+ const patches = [];
104
+ const inversePatches = [];
105
+ const nextKeys = new Set(getOwnEnumerableKeys(nextState));
106
+ for (const key of getOwnEnumerableKeys(currentState)) {
107
+ if (typeof key === "string" && isUnsafeKey(key)) continue;
108
+ if (nextKeys.has(key)) continue;
109
+ patches.push({
110
+ op: "remove",
111
+ path: [key]
112
+ });
113
+ inversePatches.push({
114
+ op: "add",
115
+ path: [key],
116
+ value: currentState[key]
117
+ });
118
+ }
119
+ for (const key of nextKeys) {
120
+ if (typeof key === "string" && isUnsafeKey(key)) continue;
121
+ if (!Object.prototype.hasOwnProperty.call(currentState, key)) {
122
+ patches.push({
123
+ op: "add",
124
+ path: [key],
125
+ value: nextState[key]
126
+ });
127
+ inversePatches.push({
128
+ op: "remove",
129
+ path: [key]
130
+ });
131
+ continue;
132
+ }
133
+ if (Object.is(currentState[key], nextState[key])) continue;
134
+ patches.push({
135
+ op: "replace",
136
+ path: [key],
137
+ value: nextState[key]
138
+ });
139
+ inversePatches.push({
140
+ op: "replace",
141
+ path: [key],
142
+ value: currentState[key]
143
+ });
144
+ }
145
+ return {
146
+ patches,
147
+ inversePatches
148
+ };
149
+ };
90
150
  const setOwnEnumerable = (target, key, value) => {
91
151
  if (typeof key === "string" && isUnsafeKey(key)) return;
92
152
  target[key] = value;
@@ -378,20 +438,92 @@ const getInitialState = (store, createState, internal) => {
378
438
  }, {});
379
439
  };
380
440
  //#endregion
441
+ //#region packages/core/src/storeCommit.ts
442
+ const storeCommitRuntimeSymbol = Symbol.for("coaction.storeCommit.runtime");
443
+ const getStoreCommitRuntime = (store, create = false) => {
444
+ const target = store;
445
+ const existing = target[storeCommitRuntimeSymbol];
446
+ if (existing || !create) return existing;
447
+ const runtime = {
448
+ disposed: false,
449
+ listeners: /* @__PURE__ */ new Set(),
450
+ prepareListeners: /* @__PURE__ */ new Set()
451
+ };
452
+ Object.defineProperty(target, storeCommitRuntimeSymbol, {
453
+ configurable: true,
454
+ enumerable: true,
455
+ value: runtime,
456
+ writable: true
457
+ });
458
+ return runtime;
459
+ };
460
+ /** @internal */
461
+ const hasStoreCommitListeners = (store) => Boolean(getStoreCommitRuntime(store)?.listeners.size);
462
+ /** @internal */
463
+ const publishStoreCommit = (store, commit) => {
464
+ const runtime = getStoreCommitRuntime(store);
465
+ if (!runtime || runtime.disposed || !runtime.listeners.size) return;
466
+ for (const listener of [...runtime.listeners]) listener(commit);
467
+ };
468
+ /** @internal */
469
+ const prepareStoreCommit = (store, commit) => {
470
+ const runtime = getStoreCommitRuntime(store);
471
+ if (!runtime || runtime.disposed || !runtime.prepareListeners.size) return false;
472
+ let replace = false;
473
+ for (const listener of runtime.prepareListeners) replace = listener(commit) === true || replace;
474
+ return replace;
475
+ };
476
+ /** @internal */
477
+ const getStoreCommitSource = (store, fallback) => getStoreCommitRuntime(store)?.source ?? fallback;
478
+ /** @internal */
479
+ const runWithStoreCommitSource = (store, source, callback) => {
480
+ const runtime = getStoreCommitRuntime(store, true);
481
+ const previousSource = runtime.source;
482
+ runtime.source = source;
483
+ try {
484
+ return callback();
485
+ } finally {
486
+ runtime.source = previousSource;
487
+ }
488
+ };
489
+ /** @internal */
490
+ const registerStorePatchReplayer = (store, replay) => {
491
+ const runtime = getStoreCommitRuntime(store, true);
492
+ runtime.replay = replay;
493
+ };
494
+ /** @internal */
495
+ const disposeStoreCommitRuntime = (store) => {
496
+ const runtime = getStoreCommitRuntime(store);
497
+ if (!runtime) return;
498
+ runtime.disposed = true;
499
+ runtime.listeners.clear();
500
+ runtime.prepareListeners.clear();
501
+ runtime.source = void 0;
502
+ runtime.replay = void 0;
503
+ };
504
+ //#endregion
381
505
  //#region packages/core/src/handleDraft.ts
382
506
  const handleDraft = (store, internal) => {
383
507
  internal.rootState = internal.backupState;
384
508
  const [, patches, inversePatches] = internal.finalizeDraft();
385
- const safePatches = sanitizeCheckedPatches((store.patch ? store.patch({
509
+ const finalPatches = store.patch ? store.patch({
386
510
  patches,
387
511
  inversePatches
388
512
  }) : {
389
513
  patches,
390
514
  inversePatches
391
- }).patches, "store.patch()");
515
+ };
516
+ const safePatches = sanitizeCheckedPatches(finalPatches.patches, "store.patch()");
517
+ const safeInversePatches = sanitizeCheckedPatches(finalPatches.inversePatches, "store.patch() inverse patches");
392
518
  if (safePatches.length) {
393
519
  store.apply(internal.rootState, safePatches);
394
520
  internal.emitPatches?.(safePatches);
521
+ publishStoreCommit(store, {
522
+ state: internal.rootState,
523
+ patches: safePatches,
524
+ inversePatches: safeInversePatches,
525
+ source: "mutableAction"
526
+ });
395
527
  }
396
528
  };
397
529
  //#endregion
@@ -438,7 +570,7 @@ const createLocalAction = ({ fn, internal, key, options, store, sliceKey }) => {
438
570
  throw error;
439
571
  }
440
572
  };
441
- const enablePatches = store.transport ?? options.enablePatches;
573
+ const enablePatches = Boolean(store.transport ?? options.enablePatches) || hasStoreCommitListeners(store);
442
574
  return traceAction(() => {
443
575
  if (internal.mutableInstance && !internal.isBatching && enablePatches) {
444
576
  let result;
@@ -746,8 +878,10 @@ const getRawState = (store, internal, initialState, options, createClientAction)
746
878
  //#region packages/core/src/handleState.ts
747
879
  const handleState = (store, internal, options) => {
748
880
  let defaultResultValidated = false;
881
+ let pendingCommitSource;
749
882
  const defaultUpdater = (next) => {
750
883
  defaultResultValidated = false;
884
+ let producedState;
751
885
  const merge = (_next = next) => {
752
886
  if (_next !== next) internal.validateState?.(_next);
753
887
  assertKnownStateShape(_next, internal.rootState, internal.stateSchema, store.isSliceStore);
@@ -761,7 +895,7 @@ const handleState = (store, internal, options) => {
761
895
  }
762
896
  if (typeof returnValue === "object" && returnValue !== null) merge(returnValue);
763
897
  } : merge;
764
- if (!(store.transport ?? options.enablePatches) && internal.mutableInstance) {
898
+ if (!(Boolean(store.transport ?? options.enablePatches) || hasStoreCommitListeners(store)) && internal.mutableInstance) {
765
899
  if (internal.actMutable) {
766
900
  internal.actMutable(() => {
767
901
  fn.apply(null);
@@ -783,6 +917,7 @@ const handleState = (store, internal, options) => {
783
917
  }, { enablePatches: true });
784
918
  assertKnownStateShape(result[0], internal.backupState, internal.stateSchema, store.isSliceStore, { requireSliceRoots: true });
785
919
  internal.validateState?.(internal.getTransportState?.() ?? result[0]);
920
+ producedState = result[0];
786
921
  patches = result[1];
787
922
  inversePatches = result[2];
788
923
  } finally {
@@ -799,6 +934,18 @@ const handleState = (store, internal, options) => {
799
934
  if (!patch) internal.validatePatches?.(finalPatches.patches);
800
935
  const safePatches = sanitizeCheckedPatches(finalPatches.patches, "store.patch()");
801
936
  const safeInversePatches = sanitizeCheckedPatches(finalPatches.inversePatches, "store.patch() inverse patches");
937
+ if (producedState && safePatches.some((patch) => typeof patch.value === "object" && patch.value !== null) && prepareStoreCommit(store, {
938
+ state: producedState,
939
+ patches: safePatches,
940
+ inversePatches: safeInversePatches,
941
+ source: "setState"
942
+ })) {
943
+ runWithStoreCommitSource(store, "setState", () => {
944
+ store.apply(producedState);
945
+ });
946
+ defaultResultValidated = true;
947
+ return [];
948
+ }
802
949
  if (safePatches.length) {
803
950
  defaultResultValidated = internal.applyValidatedPatches?.(internal.rootState, safePatches, !patch) ?? false;
804
951
  if (!internal.applyValidatedPatches) store.apply(internal.rootState, safePatches);
@@ -810,6 +957,8 @@ const handleState = (store, internal, options) => {
810
957
  ];
811
958
  };
812
959
  const setState = (next, updater = defaultUpdater) => {
960
+ const commitSource = pendingCommitSource ?? "setState";
961
+ pendingCommitSource = void 0;
813
962
  internal.assertAlive?.("setState");
814
963
  internal.assertMutationAllowed?.("setState");
815
964
  if (store.share === "client") throw new Error(`setState() cannot be called in the client store. To update the state, please trigger a store method with setState() instead.`);
@@ -820,7 +969,7 @@ const handleState = (store, internal, options) => {
820
969
  assertKnownStateShape(next, internal.rootState, internal.stateSchema, store.isSliceStore);
821
970
  }
822
971
  internal.isBatching = true;
823
- if (!store.share && !options.enablePatches && !internal.mutableInstance && updater === defaultUpdater) try {
972
+ if (!store.share && !options.enablePatches && !hasStoreCommitListeners(store) && !internal.mutableInstance && updater === defaultUpdater) try {
824
973
  if (typeof next === "function") try {
825
974
  internal.backupState = internal.rootState;
826
975
  const snapshotCache = internal.computedSnapshotCache;
@@ -901,13 +1050,60 @@ const handleState = (store, internal, options) => {
901
1050
  sanitizeCheckedPatches(result[2], "setState updater inverse result")
902
1051
  ];
903
1052
  }
904
- if (result?.[1]) internal.emitPatches?.(result[1]);
1053
+ if (result?.length === 3) {
1054
+ const [, patches, inversePatches] = result;
1055
+ internal.emitPatches?.(patches);
1056
+ if (patches.length || inversePatches.length) publishStoreCommit(store, {
1057
+ state: internal.rootState,
1058
+ patches,
1059
+ inversePatches,
1060
+ source: commitSource
1061
+ });
1062
+ }
905
1063
  return result;
906
1064
  };
1065
+ const replayPatches = ({ patches, inversePatches }, replaySetState = setState) => {
1066
+ const previousSource = pendingCommitSource;
1067
+ pendingCommitSource = "replay";
1068
+ try {
1069
+ replaySetState(internal.rootState, () => {
1070
+ const inputPatches = patches.map((patch) => ({
1071
+ ...patch,
1072
+ path: Array.isArray(patch.path) ? [...patch.path] : patch.path
1073
+ }));
1074
+ const inputInversePatches = inversePatches.map((patch) => ({
1075
+ ...patch,
1076
+ path: Array.isArray(patch.path) ? [...patch.path] : patch.path
1077
+ }));
1078
+ const finalPatches = store.patch ? store.patch({
1079
+ patches: inputPatches,
1080
+ inversePatches: inputInversePatches
1081
+ }) : {
1082
+ patches: inputPatches,
1083
+ inversePatches: inputInversePatches
1084
+ };
1085
+ const safePatches = sanitizeCheckedPatches(finalPatches.patches, "store.patch()");
1086
+ const safeInversePatches = sanitizeCheckedPatches(finalPatches.inversePatches, "store.patch() inverse patches");
1087
+ if (safePatches.length) {
1088
+ internal.applyValidatedPatches?.(internal.rootState, safePatches, false);
1089
+ if (!internal.applyValidatedPatches) store.apply(internal.rootState, safePatches);
1090
+ }
1091
+ return [
1092
+ internal.rootState,
1093
+ safePatches,
1094
+ safeInversePatches
1095
+ ];
1096
+ });
1097
+ return internal.rootState;
1098
+ } finally {
1099
+ pendingCommitSource = previousSource;
1100
+ }
1101
+ };
907
1102
  const getState = (deps, selector) => deps && selector ? new Computed(deps, selector) : internal.module;
908
1103
  return {
909
1104
  setState,
910
- getState
1105
+ getState,
1106
+ replayPatches
911
1107
  };
912
1108
  };
913
1109
  //#endregion
@@ -950,7 +1146,7 @@ const createStore = (createState, options, runtime = {}) => {
950
1146
  namespaceMap.set(name, true);
951
1147
  }
952
1148
  try {
953
- const { setState, getState } = handleState(store, internal, options);
1149
+ const { setState, getState, replayPatches } = handleState(store, internal, options);
954
1150
  const subscribe = (listener) => {
955
1151
  internal.assertAlive?.("subscribe");
956
1152
  internal.listeners.add(listener);
@@ -972,6 +1168,7 @@ const createStore = (createState, options, runtime = {}) => {
972
1168
  firstError ??= error;
973
1169
  }
974
1170
  internal.listeners.clear();
1171
+ disposeStoreCommitRuntime(store);
975
1172
  try {
976
1173
  store.transport?.dispose();
977
1174
  } catch (error) {
@@ -1000,7 +1197,23 @@ const createStore = (createState, options, runtime = {}) => {
1000
1197
  if (internal.updateImmutable) internal.updateImmutable(internal.rootState);
1001
1198
  else internal.listeners.forEach((listener) => listener());
1002
1199
  };
1003
- const apply = (state = internal.rootState, patches) => applyState(state, patches);
1200
+ const apply = (state = internal.rootState, patches) => {
1201
+ const observeReplacement = patches === void 0 && hasStoreCommitListeners(store);
1202
+ const previousState = internal.rootState;
1203
+ applyState(state, patches);
1204
+ if (!observeReplacement) return;
1205
+ const replacement = createRootReplacementPatches(previousState, internal.rootState);
1206
+ const safePatches = sanitizeCheckedPatches(replacement.patches, "store.apply() replacement");
1207
+ const safeInversePatches = sanitizeCheckedPatches(replacement.inversePatches, "store.apply() replacement inverse patches");
1208
+ if (!safePatches.length && !safeInversePatches.length) return;
1209
+ internal.emitPatches?.(safePatches);
1210
+ publishStoreCommit(store, {
1211
+ state: internal.rootState,
1212
+ patches: safePatches,
1213
+ inversePatches: safeInversePatches,
1214
+ source: getStoreCommitSource(store, "external")
1215
+ });
1216
+ };
1004
1217
  internal.applyValidatedPatches = (state, patches, skipFinalValidation) => {
1005
1218
  if (store.apply !== apply) {
1006
1219
  store.apply(state, patches);
@@ -1042,6 +1255,7 @@ const createStore = (createState, options, runtime = {}) => {
1042
1255
  });
1043
1256
  const middlewareStore = applyMiddlewares(store, options.middlewares ?? []);
1044
1257
  if (middlewareStore !== store) Object.assign(store, middlewareStore);
1258
+ registerStorePatchReplayer(store, replayPatches);
1045
1259
  internal.assertAlive?.("store initialization");
1046
1260
  if (validatePatches && store.patch) {
1047
1261
  const patch = store.patch.bind(store);