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