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