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/index.mjs CHANGED
@@ -24,30 +24,41 @@ const failTransportInitialization = (transport, error) => {
24
24
  }
25
25
  throw error;
26
26
  };
27
- const readyStores = /* @__PURE__ */ new WeakSet();
28
- const readyCallbacks = /* @__PURE__ */ new WeakMap();
27
+ const storeReadyRuntimeSymbol = Symbol.for("coaction.lifecycle.ready");
28
+ const getStoreReadyRuntime = (store, create = false) => {
29
+ const target = store;
30
+ const existing = target[storeReadyRuntimeSymbol];
31
+ if (existing || !create) return existing;
32
+ const runtime = {
33
+ callbacks: /* @__PURE__ */ new Set(),
34
+ ready: false
35
+ };
36
+ Object.defineProperty(target, storeReadyRuntimeSymbol, {
37
+ configurable: true,
38
+ enumerable: true,
39
+ value: runtime,
40
+ writable: true
41
+ });
42
+ return runtime;
43
+ };
29
44
  const onStoreReady = (store, callback) => {
30
- if (readyStores.has(store)) {
45
+ const runtime = getStoreReadyRuntime(store, true);
46
+ if (runtime.ready) {
31
47
  callback();
32
48
  return () => void 0;
33
49
  }
34
- let callbacks = readyCallbacks.get(store);
35
- if (!callbacks) {
36
- callbacks = /* @__PURE__ */ new Set();
37
- readyCallbacks.set(store, callbacks);
38
- }
39
- callbacks.add(callback);
50
+ runtime.callbacks.add(callback);
40
51
  return () => {
41
- callbacks?.delete(callback);
52
+ runtime.callbacks.delete(callback);
42
53
  };
43
54
  };
44
55
  const markStoreReady = (store) => {
45
- readyStores.add(store);
46
- const callbacks = readyCallbacks.get(store);
47
- if (!callbacks) return;
48
- readyCallbacks.delete(store);
56
+ const runtime = getStoreReadyRuntime(store, true);
57
+ if (runtime.ready) return;
58
+ runtime.ready = true;
59
+ const callbacks = [...runtime.callbacks];
60
+ runtime.callbacks.clear();
49
61
  callbacks.forEach((callback) => callback());
50
- callbacks.clear();
51
62
  };
52
63
  //#endregion
53
64
  //#region packages/core/src/utils.ts
@@ -82,15 +93,64 @@ const warnDroppedUnsafePatches = (unsafePatches, source) => {
82
93
  };
83
94
  const sanitizePatches = (patches, options = {}) => {
84
95
  if (options.warnOnDropped) warnDroppedUnsafePatches(getUnsafePatchPaths(patches), options.source ?? "patches");
96
+ const seen = /* @__PURE__ */ new WeakMap();
85
97
  return patches?.filter((patch) => !hasUnsafePatchPath(patch.path)).map((patch) => Object.prototype.hasOwnProperty.call(patch, "value") ? {
86
98
  ...patch,
87
- value: sanitizeReplacementState(patch.value)
99
+ value: sanitizeReplacementState(patch.value, seen)
88
100
  } : patch);
89
101
  };
90
102
  const sanitizeCheckedPatches = (patches, source) => {
91
103
  assertSafePatches(patches, source);
92
104
  return sanitizePatches(patches) ?? [];
93
105
  };
106
+ const createRootReplacementPatches = (currentState, nextState) => {
107
+ const patches = [];
108
+ const inversePatches = [];
109
+ const nextKeys = new Set(getOwnEnumerableKeys(nextState));
110
+ for (const key of getOwnEnumerableKeys(currentState)) {
111
+ if (typeof key === "string" && isUnsafeKey(key)) continue;
112
+ if (nextKeys.has(key)) continue;
113
+ patches.push({
114
+ op: "remove",
115
+ path: [key]
116
+ });
117
+ inversePatches.push({
118
+ op: "add",
119
+ path: [key],
120
+ value: currentState[key]
121
+ });
122
+ }
123
+ for (const key of nextKeys) {
124
+ if (typeof key === "string" && isUnsafeKey(key)) continue;
125
+ if (!Object.prototype.hasOwnProperty.call(currentState, key)) {
126
+ patches.push({
127
+ op: "add",
128
+ path: [key],
129
+ value: nextState[key]
130
+ });
131
+ inversePatches.push({
132
+ op: "remove",
133
+ path: [key]
134
+ });
135
+ continue;
136
+ }
137
+ if (Object.is(currentState[key], nextState[key])) continue;
138
+ patches.push({
139
+ op: "replace",
140
+ path: [key],
141
+ value: nextState[key]
142
+ });
143
+ inversePatches.push({
144
+ op: "replace",
145
+ path: [key],
146
+ value: currentState[key]
147
+ });
148
+ }
149
+ return {
150
+ patches,
151
+ inversePatches
152
+ };
153
+ };
94
154
  const setOwnEnumerable = (target, key, value) => {
95
155
  if (typeof key === "string" && isUnsafeKey(key)) return;
96
156
  target[key] = value;
@@ -1128,20 +1188,92 @@ const getInitialState = (store, createState, internal) => {
1128
1188
  }, {});
1129
1189
  };
1130
1190
  //#endregion
1191
+ //#region packages/core/src/storeCommit.ts
1192
+ const storeCommitRuntimeSymbol = Symbol.for("coaction.storeCommit.runtime");
1193
+ const getStoreCommitRuntime = (store, create = false) => {
1194
+ const target = store;
1195
+ const existing = target[storeCommitRuntimeSymbol];
1196
+ if (existing || !create) return existing;
1197
+ const runtime = {
1198
+ disposed: false,
1199
+ listeners: /* @__PURE__ */ new Set(),
1200
+ prepareListeners: /* @__PURE__ */ new Set()
1201
+ };
1202
+ Object.defineProperty(target, storeCommitRuntimeSymbol, {
1203
+ configurable: true,
1204
+ enumerable: true,
1205
+ value: runtime,
1206
+ writable: true
1207
+ });
1208
+ return runtime;
1209
+ };
1210
+ /** @internal */
1211
+ const hasStoreCommitListeners = (store) => Boolean(getStoreCommitRuntime(store)?.listeners.size);
1212
+ /** @internal */
1213
+ const publishStoreCommit = (store, commit) => {
1214
+ const runtime = getStoreCommitRuntime(store);
1215
+ if (!runtime || runtime.disposed || !runtime.listeners.size) return;
1216
+ for (const listener of [...runtime.listeners]) listener(commit);
1217
+ };
1218
+ /** @internal */
1219
+ const prepareStoreCommit = (store, commit) => {
1220
+ const runtime = getStoreCommitRuntime(store);
1221
+ if (!runtime || runtime.disposed || !runtime.prepareListeners.size) return false;
1222
+ let replace = false;
1223
+ for (const listener of runtime.prepareListeners) replace = listener(commit) === true || replace;
1224
+ return replace;
1225
+ };
1226
+ /** @internal */
1227
+ const getStoreCommitSource = (store, fallback) => getStoreCommitRuntime(store)?.source ?? fallback;
1228
+ /** @internal */
1229
+ const runWithStoreCommitSource = (store, source, callback) => {
1230
+ const runtime = getStoreCommitRuntime(store, true);
1231
+ const previousSource = runtime.source;
1232
+ runtime.source = source;
1233
+ try {
1234
+ return callback();
1235
+ } finally {
1236
+ runtime.source = previousSource;
1237
+ }
1238
+ };
1239
+ /** @internal */
1240
+ const registerStorePatchReplayer = (store, replay) => {
1241
+ const runtime = getStoreCommitRuntime(store, true);
1242
+ runtime.replay = replay;
1243
+ };
1244
+ /** @internal */
1245
+ const disposeStoreCommitRuntime = (store) => {
1246
+ const runtime = getStoreCommitRuntime(store);
1247
+ if (!runtime) return;
1248
+ runtime.disposed = true;
1249
+ runtime.listeners.clear();
1250
+ runtime.prepareListeners.clear();
1251
+ runtime.source = void 0;
1252
+ runtime.replay = void 0;
1253
+ };
1254
+ //#endregion
1131
1255
  //#region packages/core/src/handleDraft.ts
1132
1256
  const handleDraft = (store, internal) => {
1133
1257
  internal.rootState = internal.backupState;
1134
1258
  const [, patches, inversePatches] = internal.finalizeDraft();
1135
- const safePatches = sanitizeCheckedPatches((store.patch ? store.patch({
1259
+ const finalPatches = store.patch ? store.patch({
1136
1260
  patches,
1137
1261
  inversePatches
1138
1262
  }) : {
1139
1263
  patches,
1140
1264
  inversePatches
1141
- }).patches, "store.patch()");
1265
+ };
1266
+ const safePatches = sanitizeCheckedPatches(finalPatches.patches, "store.patch()");
1267
+ const safeInversePatches = sanitizeCheckedPatches(finalPatches.inversePatches, "store.patch() inverse patches");
1142
1268
  if (safePatches.length) {
1143
1269
  store.apply(internal.rootState, safePatches);
1144
1270
  internal.emitPatches?.(safePatches);
1271
+ publishStoreCommit(store, {
1272
+ state: internal.rootState,
1273
+ patches: safePatches,
1274
+ inversePatches: safeInversePatches,
1275
+ source: "mutableAction"
1276
+ });
1145
1277
  }
1146
1278
  };
1147
1279
  //#endregion
@@ -1188,7 +1320,7 @@ const createLocalAction = ({ fn, internal, key, options, store, sliceKey }) => {
1188
1320
  throw error;
1189
1321
  }
1190
1322
  };
1191
- const enablePatches = store.transport ?? options.enablePatches;
1323
+ const enablePatches = Boolean(store.transport ?? options.enablePatches) || hasStoreCommitListeners(store);
1192
1324
  return traceAction(() => {
1193
1325
  if (internal.mutableInstance && !internal.isBatching && enablePatches) {
1194
1326
  let result;
@@ -1496,8 +1628,10 @@ const getRawState = (store, internal, initialState, options, createClientAction)
1496
1628
  //#region packages/core/src/handleState.ts
1497
1629
  const handleState = (store, internal, options) => {
1498
1630
  let defaultResultValidated = false;
1631
+ let pendingCommitSource;
1499
1632
  const defaultUpdater = (next) => {
1500
1633
  defaultResultValidated = false;
1634
+ let producedState;
1501
1635
  const merge = (_next = next) => {
1502
1636
  if (_next !== next) internal.validateState?.(_next);
1503
1637
  assertKnownStateShape(_next, internal.rootState, internal.stateSchema, store.isSliceStore);
@@ -1511,7 +1645,7 @@ const handleState = (store, internal, options) => {
1511
1645
  }
1512
1646
  if (typeof returnValue === "object" && returnValue !== null) merge(returnValue);
1513
1647
  } : merge;
1514
- if (!(store.transport ?? options.enablePatches) && internal.mutableInstance) {
1648
+ if (!(Boolean(store.transport ?? options.enablePatches) || hasStoreCommitListeners(store)) && internal.mutableInstance) {
1515
1649
  if (internal.actMutable) {
1516
1650
  internal.actMutable(() => {
1517
1651
  fn.apply(null);
@@ -1533,6 +1667,7 @@ const handleState = (store, internal, options) => {
1533
1667
  }, { enablePatches: true });
1534
1668
  assertKnownStateShape(result[0], internal.backupState, internal.stateSchema, store.isSliceStore, { requireSliceRoots: true });
1535
1669
  internal.validateState?.(internal.getTransportState?.() ?? result[0]);
1670
+ producedState = result[0];
1536
1671
  patches = result[1];
1537
1672
  inversePatches = result[2];
1538
1673
  } finally {
@@ -1549,6 +1684,18 @@ const handleState = (store, internal, options) => {
1549
1684
  if (!patch) internal.validatePatches?.(finalPatches.patches);
1550
1685
  const safePatches = sanitizeCheckedPatches(finalPatches.patches, "store.patch()");
1551
1686
  const safeInversePatches = sanitizeCheckedPatches(finalPatches.inversePatches, "store.patch() inverse patches");
1687
+ if (producedState && safePatches.some((patch) => typeof patch.value === "object" && patch.value !== null) && prepareStoreCommit(store, {
1688
+ state: producedState,
1689
+ patches: safePatches,
1690
+ inversePatches: safeInversePatches,
1691
+ source: "setState"
1692
+ })) {
1693
+ runWithStoreCommitSource(store, "setState", () => {
1694
+ store.apply(producedState);
1695
+ });
1696
+ defaultResultValidated = true;
1697
+ return [];
1698
+ }
1552
1699
  if (safePatches.length) {
1553
1700
  defaultResultValidated = internal.applyValidatedPatches?.(internal.rootState, safePatches, !patch) ?? false;
1554
1701
  if (!internal.applyValidatedPatches) store.apply(internal.rootState, safePatches);
@@ -1560,6 +1707,8 @@ const handleState = (store, internal, options) => {
1560
1707
  ];
1561
1708
  };
1562
1709
  const setState = (next, updater = defaultUpdater) => {
1710
+ const commitSource = pendingCommitSource ?? "setState";
1711
+ pendingCommitSource = void 0;
1563
1712
  internal.assertAlive?.("setState");
1564
1713
  internal.assertMutationAllowed?.("setState");
1565
1714
  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.`);
@@ -1570,7 +1719,7 @@ const handleState = (store, internal, options) => {
1570
1719
  assertKnownStateShape(next, internal.rootState, internal.stateSchema, store.isSliceStore);
1571
1720
  }
1572
1721
  internal.isBatching = true;
1573
- if (!store.share && !options.enablePatches && !internal.mutableInstance && updater === defaultUpdater) try {
1722
+ if (!store.share && !options.enablePatches && !hasStoreCommitListeners(store) && !internal.mutableInstance && updater === defaultUpdater) try {
1574
1723
  if (typeof next === "function") try {
1575
1724
  internal.backupState = internal.rootState;
1576
1725
  const snapshotCache = internal.computedSnapshotCache;
@@ -1651,13 +1800,60 @@ const handleState = (store, internal, options) => {
1651
1800
  sanitizeCheckedPatches(result[2], "setState updater inverse result")
1652
1801
  ];
1653
1802
  }
1654
- if (result?.[1]) internal.emitPatches?.(result[1]);
1803
+ if (result?.length === 3) {
1804
+ const [, patches, inversePatches] = result;
1805
+ internal.emitPatches?.(patches);
1806
+ if (patches.length || inversePatches.length) publishStoreCommit(store, {
1807
+ state: internal.rootState,
1808
+ patches,
1809
+ inversePatches,
1810
+ source: commitSource
1811
+ });
1812
+ }
1655
1813
  return result;
1656
1814
  };
1815
+ const replayPatches = ({ patches, inversePatches }, replaySetState = setState) => {
1816
+ const previousSource = pendingCommitSource;
1817
+ pendingCommitSource = "replay";
1818
+ try {
1819
+ replaySetState(internal.rootState, () => {
1820
+ const inputPatches = patches.map((patch) => ({
1821
+ ...patch,
1822
+ path: Array.isArray(patch.path) ? [...patch.path] : patch.path
1823
+ }));
1824
+ const inputInversePatches = inversePatches.map((patch) => ({
1825
+ ...patch,
1826
+ path: Array.isArray(patch.path) ? [...patch.path] : patch.path
1827
+ }));
1828
+ const finalPatches = store.patch ? store.patch({
1829
+ patches: inputPatches,
1830
+ inversePatches: inputInversePatches
1831
+ }) : {
1832
+ patches: inputPatches,
1833
+ inversePatches: inputInversePatches
1834
+ };
1835
+ const safePatches = sanitizeCheckedPatches(finalPatches.patches, "store.patch()");
1836
+ const safeInversePatches = sanitizeCheckedPatches(finalPatches.inversePatches, "store.patch() inverse patches");
1837
+ if (safePatches.length) {
1838
+ internal.applyValidatedPatches?.(internal.rootState, safePatches, false);
1839
+ if (!internal.applyValidatedPatches) store.apply(internal.rootState, safePatches);
1840
+ }
1841
+ return [
1842
+ internal.rootState,
1843
+ safePatches,
1844
+ safeInversePatches
1845
+ ];
1846
+ });
1847
+ return internal.rootState;
1848
+ } finally {
1849
+ pendingCommitSource = previousSource;
1850
+ }
1851
+ };
1657
1852
  const getState = (deps, selector) => deps && selector ? new Computed(deps, selector) : internal.module;
1658
1853
  return {
1659
1854
  setState,
1660
- getState
1855
+ getState,
1856
+ replayPatches
1661
1857
  };
1662
1858
  };
1663
1859
  //#endregion
@@ -1700,7 +1896,7 @@ const createStore = (createState, options, runtime = {}) => {
1700
1896
  namespaceMap.set(name, true);
1701
1897
  }
1702
1898
  try {
1703
- const { setState, getState } = handleState(store, internal, options);
1899
+ const { setState, getState, replayPatches } = handleState(store, internal, options);
1704
1900
  const subscribe = (listener) => {
1705
1901
  internal.assertAlive?.("subscribe");
1706
1902
  internal.listeners.add(listener);
@@ -1722,6 +1918,7 @@ const createStore = (createState, options, runtime = {}) => {
1722
1918
  firstError ??= error;
1723
1919
  }
1724
1920
  internal.listeners.clear();
1921
+ disposeStoreCommitRuntime(store);
1725
1922
  try {
1726
1923
  store.transport?.dispose();
1727
1924
  } catch (error) {
@@ -1750,7 +1947,23 @@ const createStore = (createState, options, runtime = {}) => {
1750
1947
  if (internal.updateImmutable) internal.updateImmutable(internal.rootState);
1751
1948
  else internal.listeners.forEach((listener) => listener());
1752
1949
  };
1753
- const apply$1 = (state = internal.rootState, patches) => applyState(state, patches);
1950
+ const apply$1 = (state = internal.rootState, patches) => {
1951
+ const observeReplacement = patches === void 0 && hasStoreCommitListeners(store);
1952
+ const previousState = internal.rootState;
1953
+ applyState(state, patches);
1954
+ if (!observeReplacement) return;
1955
+ const replacement = createRootReplacementPatches(previousState, internal.rootState);
1956
+ const safePatches = sanitizeCheckedPatches(replacement.patches, "store.apply() replacement");
1957
+ const safeInversePatches = sanitizeCheckedPatches(replacement.inversePatches, "store.apply() replacement inverse patches");
1958
+ if (!safePatches.length && !safeInversePatches.length) return;
1959
+ internal.emitPatches?.(safePatches);
1960
+ publishStoreCommit(store, {
1961
+ state: internal.rootState,
1962
+ patches: safePatches,
1963
+ inversePatches: safeInversePatches,
1964
+ source: getStoreCommitSource(store, "external")
1965
+ });
1966
+ };
1754
1967
  internal.applyValidatedPatches = (state, patches, skipFinalValidation) => {
1755
1968
  if (store.apply !== apply$1) {
1756
1969
  store.apply(state, patches);
@@ -1792,6 +2005,7 @@ const createStore = (createState, options, runtime = {}) => {
1792
2005
  });
1793
2006
  const middlewareStore = applyMiddlewares(store, options.middlewares ?? []);
1794
2007
  if (middlewareStore !== store) Object.assign(store, middlewareStore);
2008
+ registerStorePatchReplayer(store, replayPatches);
1795
2009
  internal.assertAlive?.("store initialization");
1796
2010
  if (validatePatches && store.patch) {
1797
2011
  const patch = store.patch.bind(store);