@ngstato/core 0.3.0 → 0.4.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
@@ -42,6 +42,7 @@ var StatoStore = class {
42
42
  _publicStore = null;
43
43
  _publicActions = {};
44
44
  _initialized = false;
45
+ _timeTraveling = false;
45
46
  _effects = [];
46
47
  _createMemoizedSelector(fn) {
47
48
  let initialized = false;
@@ -117,7 +118,9 @@ var StatoStore = class {
117
118
  // ── Modifier le state — usage interne uniquement ───
118
119
  _setState(partial) {
119
120
  this._state = { ...this._state, ...partial };
120
- this._runEffects();
121
+ if (!this._timeTraveling) {
122
+ this._runEffects();
123
+ }
121
124
  this._notify();
122
125
  }
123
126
  _normalizeDeps(value) {
@@ -199,7 +202,13 @@ var StatoStore = class {
199
202
  await action(stateProxy, ...args);
200
203
  const duration = Date.now() - start;
201
204
  this._hooks.onActionDone?.(actionName, duration);
202
- this._hooks.onStateChange?.(prevState, { ...this._state });
205
+ const nextState = { ...this._state };
206
+ const hasChanged = Object.keys(nextState).some(
207
+ (k) => !Object.is(prevState[k], nextState[k])
208
+ );
209
+ if (hasChanged) {
210
+ this._hooks.onStateChange?.(prevState, nextState);
211
+ }
203
212
  if (publicAction) {
204
213
  emitActionEvent({
205
214
  action: publicAction,
@@ -247,9 +256,15 @@ var StatoStore = class {
247
256
  hydrate(partial) {
248
257
  this._setState(partial);
249
258
  }
259
+ // ── Time-travel — restaurer un snapshot sans déclencher les effects ──
260
+ hydrateForTimeTravel(fullState) {
261
+ this._timeTraveling = true;
262
+ this._state = { ...fullState };
263
+ this._notify();
264
+ this._timeTraveling = false;
265
+ }
250
266
  setPublicStore(publicStore) {
251
267
  this._publicStore = publicStore;
252
- this._runEffects(true);
253
268
  }
254
269
  // ── Lifecycle — appelé par l'adaptateur Angular ────
255
270
  init(publicStore) {
@@ -274,7 +289,7 @@ var StatoStore = class {
274
289
  this._initialized = false;
275
290
  }
276
291
  };
277
- function createStore(config) {
292
+ function createStore(config, __internal) {
278
293
  const store = new StatoStore(config);
279
294
  const publicStore = {
280
295
  // Accès au store interne — pour les adaptateurs Angular/React/Vue
@@ -320,22 +335,28 @@ function createStore(config) {
320
335
  });
321
336
  }
322
337
  }
323
- store.init(publicStore);
338
+ if (!__internal?.skipInit) {
339
+ store.init(publicStore);
340
+ }
324
341
  return publicStore;
325
342
  }
326
343
  function on(sourceAction, handler) {
327
- return subscribeToAction(sourceAction, (event) => {
328
- try {
329
- void handler(event.store, {
330
- name: event.name,
331
- args: event.args,
332
- status: event.status,
333
- duration: event.duration,
334
- error: event.error
335
- });
336
- } catch {
337
- }
338
- });
344
+ const actions = Array.isArray(sourceAction) ? sourceAction : [sourceAction];
345
+ const unsubs = actions.map(
346
+ (action) => subscribeToAction(action, (event) => {
347
+ try {
348
+ void handler(event.store, {
349
+ name: event.name,
350
+ args: event.args,
351
+ status: event.status,
352
+ duration: event.duration,
353
+ error: event.error
354
+ });
355
+ } catch {
356
+ }
357
+ })
358
+ );
359
+ return () => unsubs.forEach((unsub) => unsub());
339
360
  }
340
361
 
341
362
  // src/types.ts
@@ -462,12 +483,16 @@ function abortable(fn) {
462
483
  // src/helpers/debounced.ts
463
484
  function debounced(fn, ms = 300) {
464
485
  let timer = null;
486
+ let latestState;
487
+ let latestArgs;
465
488
  return (state, ...args) => {
466
489
  if (timer) clearTimeout(timer);
490
+ latestState = state;
491
+ latestArgs = args;
467
492
  return new Promise((resolve, reject) => {
468
493
  timer = setTimeout(async () => {
469
494
  try {
470
- await fn(state, ...args);
495
+ await fn(latestState, ...latestArgs);
471
496
  resolve();
472
497
  } catch (error) {
473
498
  reject(error);
@@ -483,16 +508,20 @@ function debounced(fn, ms = 300) {
483
508
  function throttled(fn, ms = 300) {
484
509
  let lastCall = 0;
485
510
  let timer = null;
511
+ let latestState;
512
+ let latestArgs;
486
513
  return async (state, ...args) => {
487
514
  const now = Date.now();
488
515
  const remaining = ms - (now - lastCall);
516
+ latestState = state;
517
+ latestArgs = args;
489
518
  if (remaining <= 0) {
490
519
  lastCall = now;
491
520
  if (timer) {
492
521
  clearTimeout(timer);
493
522
  timer = null;
494
523
  }
495
- await fn(state, ...args);
524
+ await fn(latestState, ...latestArgs);
496
525
  } else {
497
526
  if (timer) clearTimeout(timer);
498
527
  return new Promise((resolve, reject) => {
@@ -500,7 +529,7 @@ function throttled(fn, ms = 300) {
500
529
  lastCall = Date.now();
501
530
  timer = null;
502
531
  try {
503
- await fn(state, ...args);
532
+ await fn(latestState, ...latestArgs);
504
533
  resolve();
505
534
  } catch (error) {
506
535
  reject(error);
@@ -556,7 +585,7 @@ function fromStream(setupFn, updateFn, options) {
556
585
  // src/helpers/optimistic.ts
557
586
  function optimistic(immediate, confirm) {
558
587
  return async (state, ...args) => {
559
- const snapshot = { ...state };
588
+ const snapshot = structuredClone(state);
560
589
  immediate(state, ...args);
561
590
  try {
562
591
  await confirm(state, ...args);
@@ -1581,30 +1610,261 @@ function withPersist(config, options) {
1581
1610
  };
1582
1611
  }
1583
1612
 
1613
+ // src/helpers/with-feature.ts
1614
+ function mergeHooks(a, b) {
1615
+ if (!a) return b ?? {};
1616
+ if (!b) return a;
1617
+ const hookNames = [
1618
+ "onInit",
1619
+ "onDestroy",
1620
+ "onAction",
1621
+ "onActionDone",
1622
+ "onError",
1623
+ "onStateChange"
1624
+ ];
1625
+ const merged = { ...a };
1626
+ for (const name of hookNames) {
1627
+ const fnA = a[name];
1628
+ const fnB = b[name];
1629
+ if (fnA && fnB) {
1630
+ merged[name] = (...args) => {
1631
+ fnA(...args);
1632
+ fnB(...args);
1633
+ };
1634
+ } else if (fnB) {
1635
+ merged[name] = fnB;
1636
+ }
1637
+ }
1638
+ return merged;
1639
+ }
1640
+ function mergeFeatures(...features) {
1641
+ const result = {};
1642
+ for (const feature of features) {
1643
+ if (feature.state) {
1644
+ Object.assign(result, feature.state);
1645
+ }
1646
+ if (feature.actions) {
1647
+ result.actions = { ...result.actions ?? {}, ...feature.actions };
1648
+ }
1649
+ if (feature.computed) {
1650
+ result.computed = { ...result.computed ?? {}, ...feature.computed };
1651
+ }
1652
+ if (feature.selectors) {
1653
+ result.selectors = { ...result.selectors ?? {}, ...feature.selectors };
1654
+ }
1655
+ if (feature.effects?.length) {
1656
+ result.effects = [...result.effects ?? [], ...feature.effects];
1657
+ }
1658
+ if (feature.hooks) {
1659
+ result.hooks = mergeHooks(result.hooks, feature.hooks);
1660
+ }
1661
+ }
1662
+ return result;
1663
+ }
1664
+
1665
+ // src/helpers/with-props.ts
1666
+ function withProps(store, props) {
1667
+ const enhanced = store;
1668
+ for (const [key, value] of Object.entries(props)) {
1669
+ Object.defineProperty(enhanced, key, {
1670
+ get: () => value,
1671
+ enumerable: true,
1672
+ configurable: false
1673
+ });
1674
+ }
1675
+ return enhanced;
1676
+ }
1677
+
1584
1678
  // src/devtools.ts
1585
- function createDevTools(maxLogs = 50) {
1679
+ function createDevTools(maxLogs = 100) {
1586
1680
  let counter = 0;
1587
1681
  const state = {
1588
1682
  logs: [],
1589
1683
  isOpen: false,
1590
- maxLogs
1684
+ maxLogs,
1685
+ activeLogId: null,
1686
+ isTimeTraveling: false
1591
1687
  };
1592
1688
  const listeners = /* @__PURE__ */ new Set();
1689
+ const storeRegistry = /* @__PURE__ */ new Map();
1593
1690
  function notify() {
1594
- listeners.forEach((cb) => cb({ ...state, logs: [...state.logs] }));
1691
+ const snapshot = {
1692
+ ...state,
1693
+ logs: [...state.logs]
1694
+ };
1695
+ listeners.forEach((cb) => cb(snapshot));
1696
+ }
1697
+ function findLogIndex(logId) {
1698
+ return state.logs.findIndex((l) => l.id === logId);
1699
+ }
1700
+ function getStoreForTravel(storeName) {
1701
+ const entry = storeRegistry.get(storeName);
1702
+ if (!entry) return null;
1703
+ return entry.internalStore;
1595
1704
  }
1596
1705
  return {
1597
1706
  state,
1707
+ // ── Action logging ─────────────────────────────────
1598
1708
  logAction(log) {
1599
1709
  const entry = {
1600
1710
  ...log,
1601
1711
  id: ++counter,
1602
1712
  at: (/* @__PURE__ */ new Date()).toISOString()
1603
1713
  };
1714
+ if (state.isTimeTraveling && state.activeLogId !== null) {
1715
+ const activeIdx = findLogIndex(state.activeLogId);
1716
+ if (activeIdx >= 0) {
1717
+ state.logs = state.logs.slice(activeIdx);
1718
+ }
1719
+ state.activeLogId = null;
1720
+ state.isTimeTraveling = false;
1721
+ }
1604
1722
  state.logs = [entry, ...state.logs].slice(0, maxLogs);
1605
1723
  notify();
1606
1724
  },
1725
+ // ── Travel to a specific action ─────────────────────
1726
+ travelTo(logId) {
1727
+ const idx = findLogIndex(logId);
1728
+ if (idx === -1) return;
1729
+ const log = state.logs[idx];
1730
+ const internalStore = getStoreForTravel(log.storeName);
1731
+ if (!internalStore) return;
1732
+ internalStore.hydrateForTimeTravel(log.nextState);
1733
+ state.activeLogId = logId;
1734
+ state.isTimeTraveling = true;
1735
+ notify();
1736
+ },
1737
+ // ── Undo — step back in time ───────────────────────
1738
+ // activeLogId always points to the log whose nextState is currently displayed.
1739
+ // Logs are newest-first: [idx0=newest, idx1, ..., idxN=oldest]
1740
+ // Undo moves deeper (higher index = older). Redo moves shallower (lower index = newer).
1741
+ undo() {
1742
+ if (!state.logs.length) return;
1743
+ if (!state.isTimeTraveling || state.activeLogId === null) {
1744
+ const latest = state.logs[0];
1745
+ const internalStore2 = getStoreForTravel(latest.storeName);
1746
+ if (!internalStore2) return;
1747
+ if (state.logs.length > 1) {
1748
+ const olderLog = state.logs[1];
1749
+ internalStore2.hydrateForTimeTravel(olderLog.nextState);
1750
+ state.activeLogId = olderLog.id;
1751
+ } else {
1752
+ internalStore2.hydrateForTimeTravel(latest.prevState);
1753
+ state.activeLogId = -1;
1754
+ }
1755
+ state.isTimeTraveling = true;
1756
+ notify();
1757
+ return;
1758
+ }
1759
+ if (state.activeLogId === -1) return;
1760
+ const currentIdx = findLogIndex(state.activeLogId);
1761
+ if (currentIdx === -1) return;
1762
+ const targetIdx = currentIdx + 1;
1763
+ if (targetIdx >= state.logs.length) {
1764
+ const currentLog = state.logs[currentIdx];
1765
+ const internalStore2 = getStoreForTravel(currentLog.storeName);
1766
+ if (!internalStore2) return;
1767
+ internalStore2.hydrateForTimeTravel(currentLog.prevState);
1768
+ state.activeLogId = -1;
1769
+ notify();
1770
+ return;
1771
+ }
1772
+ const targetLog = state.logs[targetIdx];
1773
+ const internalStore = getStoreForTravel(targetLog.storeName);
1774
+ if (!internalStore) return;
1775
+ internalStore.hydrateForTimeTravel(targetLog.nextState);
1776
+ state.activeLogId = targetLog.id;
1777
+ notify();
1778
+ },
1779
+ // ── Redo — step forward in time ─────────────────────
1780
+ redo() {
1781
+ if (!state.isTimeTraveling || state.activeLogId === null) return;
1782
+ if (state.activeLogId === -1) {
1783
+ if (!state.logs.length) return;
1784
+ const oldest = state.logs[state.logs.length - 1];
1785
+ const internalStore2 = getStoreForTravel(oldest.storeName);
1786
+ if (!internalStore2) return;
1787
+ internalStore2.hydrateForTimeTravel(oldest.nextState);
1788
+ state.activeLogId = oldest.id;
1789
+ notify();
1790
+ return;
1791
+ }
1792
+ const currentIdx = findLogIndex(state.activeLogId);
1793
+ if (currentIdx === -1) return;
1794
+ const targetIdx = currentIdx - 1;
1795
+ if (targetIdx <= 0) {
1796
+ this.resume();
1797
+ return;
1798
+ }
1799
+ const targetLog = state.logs[targetIdx];
1800
+ const internalStore = getStoreForTravel(targetLog.storeName);
1801
+ if (!internalStore) return;
1802
+ internalStore.hydrateForTimeTravel(targetLog.nextState);
1803
+ state.activeLogId = targetLog.id;
1804
+ notify();
1805
+ },
1806
+ // ── Resume — back to live state ─────────────────────
1807
+ resume() {
1808
+ if (!state.isTimeTraveling) return;
1809
+ if (state.logs.length) {
1810
+ const latest = state.logs[0];
1811
+ const internalStore = getStoreForTravel(latest.storeName);
1812
+ if (internalStore) {
1813
+ internalStore.hydrateForTimeTravel(latest.nextState);
1814
+ }
1815
+ }
1816
+ state.activeLogId = null;
1817
+ state.isTimeTraveling = false;
1818
+ notify();
1819
+ },
1820
+ // ── Replay — re-execute an action ───────────────────
1821
+ replay(logId) {
1822
+ const idx = findLogIndex(logId);
1823
+ if (idx === -1) return;
1824
+ const log = state.logs[idx];
1825
+ const entry = storeRegistry.get(log.storeName);
1826
+ if (!entry) return;
1827
+ const rawName = log.name.replace(/^\[.*?\]\s*/, "");
1828
+ if (state.isTimeTraveling) {
1829
+ this.resume();
1830
+ }
1831
+ const publicStore = entry.store;
1832
+ if (typeof publicStore[rawName] === "function") {
1833
+ void publicStore[rawName](...log.args);
1834
+ }
1835
+ },
1836
+ // ── Export snapshot ──────────────────────────────────
1837
+ exportSnapshot() {
1838
+ const stores = {};
1839
+ for (const [name, { store }] of storeRegistry) {
1840
+ stores[name] = store.getState();
1841
+ }
1842
+ return {
1843
+ version: 1,
1844
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1845
+ stores,
1846
+ logs: [...state.logs]
1847
+ };
1848
+ },
1849
+ // ── Import snapshot ─────────────────────────────────
1850
+ importSnapshot(snapshot) {
1851
+ if (!snapshot || snapshot.version !== 1) return;
1852
+ for (const [name, storeState] of Object.entries(snapshot.stores)) {
1853
+ const entry = storeRegistry.get(name);
1854
+ if (entry) {
1855
+ entry.internalStore.hydrateForTimeTravel(storeState);
1856
+ }
1857
+ }
1858
+ state.logs = snapshot.logs;
1859
+ state.activeLogId = null;
1860
+ state.isTimeTraveling = false;
1861
+ notify();
1862
+ },
1863
+ // ── Basic controls ──────────────────────────────────
1607
1864
  clear() {
1865
+ if (state.isTimeTraveling) {
1866
+ this.resume();
1867
+ }
1608
1868
  state.logs = [];
1609
1869
  notify();
1610
1870
  },
@@ -1623,6 +1883,13 @@ function createDevTools(maxLogs = 50) {
1623
1883
  subscribe(cb) {
1624
1884
  listeners.add(cb);
1625
1885
  return () => listeners.delete(cb);
1886
+ },
1887
+ // ── Store registry ──────────────────────────────────
1888
+ registerStore(name, publicStore, internalStore) {
1889
+ storeRegistry.set(name, { store: publicStore, internalStore });
1890
+ },
1891
+ getStoreRegistry() {
1892
+ return storeRegistry;
1626
1893
  }
1627
1894
  };
1628
1895
  }
@@ -1632,6 +1899,7 @@ function connectDevTools(store, storeName) {
1632
1899
  let prevState = {};
1633
1900
  const internalStore = store.__store__;
1634
1901
  if (!internalStore) return;
1902
+ devTools.registerStore(storeName, store, internalStore);
1635
1903
  const existingHooks = { ...internalStore["_hooks"] };
1636
1904
  internalStore["_hooks"] = {
1637
1905
  ...existingHooks,
@@ -1643,6 +1911,7 @@ function connectDevTools(store, storeName) {
1643
1911
  const nextState = store.getState();
1644
1912
  devTools.logAction({
1645
1913
  name: `[${storeName}] ${name}`,
1914
+ storeName,
1646
1915
  args: [],
1647
1916
  duration,
1648
1917
  status: "success",
@@ -1654,6 +1923,7 @@ function connectDevTools(store, storeName) {
1654
1923
  onError(error, actionName) {
1655
1924
  devTools.logAction({
1656
1925
  name: `[${storeName}] ${actionName}`,
1926
+ storeName,
1657
1927
  args: [],
1658
1928
  duration: 0,
1659
1929
  status: "error",
@@ -1692,6 +1962,7 @@ exports.forkJoin = forkJoin;
1692
1962
  exports.fromStream = fromStream;
1693
1963
  exports.http = http;
1694
1964
  exports.mapStream = mapStream;
1965
+ exports.mergeFeatures = mergeFeatures;
1695
1966
  exports.mergeMapStream = mergeMapStream;
1696
1967
  exports.on = on;
1697
1968
  exports.optimistic = optimistic;
@@ -1705,5 +1976,6 @@ exports.throttleStream = throttleStream;
1705
1976
  exports.throttled = throttled;
1706
1977
  exports.withEntities = withEntities;
1707
1978
  exports.withPersist = withPersist;
1979
+ exports.withProps = withProps;
1708
1980
  //# sourceMappingURL=index.js.map
1709
1981
  //# sourceMappingURL=index.js.map