@ngstato/core 0.3.0 → 0.4.1

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