@ngstato/core 0.1.2 → 0.3.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
@@ -1,5 +1,29 @@
1
1
  'use strict';
2
2
 
3
+ // src/action-bus.ts
4
+ var listenersByAction = /* @__PURE__ */ new WeakMap();
5
+ function emitActionEvent(event) {
6
+ const set = listenersByAction.get(event.action);
7
+ if (!set?.size) return;
8
+ for (const listener of set) {
9
+ try {
10
+ listener(event);
11
+ } catch {
12
+ }
13
+ }
14
+ }
15
+ function subscribeToAction(action, listener) {
16
+ let set = listenersByAction.get(action);
17
+ if (!set) {
18
+ set = /* @__PURE__ */ new Set();
19
+ listenersByAction.set(action, set);
20
+ }
21
+ set.add(listener);
22
+ return () => {
23
+ set?.delete(listener);
24
+ };
25
+ }
26
+
3
27
  // src/store.ts
4
28
  var StatoStore = class {
5
29
  // Le state interne — jamais accessible directement
@@ -10,12 +34,46 @@ var StatoStore = class {
10
34
  _actions = {};
11
35
  // Les computed enregistrés
12
36
  _computed = {};
37
+ _selectors = {};
13
38
  // Les cleanups à appeler à la destruction
14
39
  _cleanups = [];
15
40
  // Les hooks lifecycle
16
41
  _hooks;
42
+ _publicStore = null;
43
+ _publicActions = {};
44
+ _initialized = false;
45
+ _effects = [];
46
+ _createMemoizedSelector(fn) {
47
+ let initialized = false;
48
+ let cachedResult;
49
+ let trackedKeys = [];
50
+ let trackedValues = [];
51
+ return () => {
52
+ if (initialized && trackedKeys.length) {
53
+ const unchanged = trackedKeys.every(
54
+ (key, index) => Object.is(this._state[key], trackedValues[index])
55
+ );
56
+ if (unchanged) return cachedResult;
57
+ }
58
+ const reads = /* @__PURE__ */ new Set();
59
+ const trackingState = new Proxy(this._state, {
60
+ get: (target, prop, receiver) => {
61
+ if (typeof prop === "string" && prop in target) {
62
+ reads.add(prop);
63
+ }
64
+ return Reflect.get(target, prop, receiver);
65
+ }
66
+ });
67
+ const result = fn(trackingState);
68
+ trackedKeys = Array.from(reads);
69
+ trackedValues = trackedKeys.map((key) => this._state[key]);
70
+ cachedResult = result;
71
+ initialized = true;
72
+ return result;
73
+ };
74
+ }
17
75
  constructor(config) {
18
- const { actions, computed, hooks, ...initialState } = config;
76
+ const { actions, computed, selectors, effects, hooks, ...initialState } = config;
19
77
  this._state = initialState;
20
78
  this._hooks = hooks ?? {};
21
79
  if (actions) {
@@ -30,6 +88,27 @@ var StatoStore = class {
30
88
  }
31
89
  }
32
90
  }
91
+ if (selectors) {
92
+ for (const [name, fn] of Object.entries(selectors)) {
93
+ if (typeof fn === "function") {
94
+ this._selectors[name] = this._createMemoizedSelector(fn);
95
+ }
96
+ }
97
+ }
98
+ if (effects) {
99
+ for (const entry of effects) {
100
+ const [deps, run] = entry;
101
+ if (typeof deps === "function" && typeof run === "function") {
102
+ this._effects.push({
103
+ deps,
104
+ run,
105
+ hasRun: false,
106
+ running: false,
107
+ rerunRequested: false
108
+ });
109
+ }
110
+ }
111
+ }
33
112
  }
34
113
  // ── Lire le state ──────────────────────────────────
35
114
  getState() {
@@ -38,8 +117,56 @@ var StatoStore = class {
38
117
  // ── Modifier le state — usage interne uniquement ───
39
118
  _setState(partial) {
40
119
  this._state = { ...this._state, ...partial };
120
+ this._runEffects();
41
121
  this._notify();
42
122
  }
123
+ _normalizeDeps(value) {
124
+ return Array.isArray(value) ? value : [value];
125
+ }
126
+ _depsChanged(prev, next) {
127
+ if (!prev) return true;
128
+ if (prev.length !== next.length) return true;
129
+ for (let i = 0; i < next.length; i++) {
130
+ if (!Object.is(prev[i], next[i])) return true;
131
+ }
132
+ return false;
133
+ }
134
+ _runEffects(force = false) {
135
+ for (const effect of this._effects) {
136
+ const depsValue = effect.deps(this._state);
137
+ const depsArray = this._normalizeDeps(depsValue);
138
+ const shouldRun = force || this._depsChanged(effect.prevDeps, depsArray);
139
+ if (!shouldRun) continue;
140
+ const execute = async () => {
141
+ if (effect.running) {
142
+ effect.rerunRequested = true;
143
+ return;
144
+ }
145
+ effect.running = true;
146
+ effect.rerunRequested = false;
147
+ const prevDepsValue = effect.prevDeps;
148
+ effect.prevDeps = depsArray;
149
+ try {
150
+ effect.cleanup?.();
151
+ const maybeCleanup = await effect.run(depsValue, {
152
+ state: { ...this._state },
153
+ store: this._publicStore,
154
+ prevDepsValue
155
+ });
156
+ effect.cleanup = typeof maybeCleanup === "function" ? maybeCleanup : void 0;
157
+ effect.hasRun = true;
158
+ } catch (error) {
159
+ this._hooks.onError?.(error, "effect");
160
+ } finally {
161
+ effect.running = false;
162
+ if (effect.rerunRequested) {
163
+ this._runEffects();
164
+ }
165
+ }
166
+ };
167
+ void execute();
168
+ }
169
+ }
43
170
  // ── Notifier tous les abonnés ──────────────────────
44
171
  _notify() {
45
172
  for (const subscriber of this._subscribers) {
@@ -57,6 +184,7 @@ var StatoStore = class {
57
184
  if (!action) {
58
185
  throw new Error(`[Stato] Action "${actionName}" introuvable`);
59
186
  }
187
+ const publicAction = this._publicActions[actionName];
60
188
  this._hooks.onAction?.(actionName, args);
61
189
  const start = Date.now();
62
190
  const prevState = { ...this._state };
@@ -69,10 +197,32 @@ var StatoStore = class {
69
197
  });
70
198
  try {
71
199
  await action(stateProxy, ...args);
72
- this._hooks.onActionDone?.(actionName, Date.now() - start);
200
+ const duration = Date.now() - start;
201
+ this._hooks.onActionDone?.(actionName, duration);
73
202
  this._hooks.onStateChange?.(prevState, { ...this._state });
203
+ if (publicAction) {
204
+ emitActionEvent({
205
+ action: publicAction,
206
+ name: actionName,
207
+ args,
208
+ store: this._publicStore,
209
+ status: "success",
210
+ duration
211
+ });
212
+ }
74
213
  } catch (error) {
75
214
  this._hooks.onError?.(error, actionName);
215
+ if (publicAction) {
216
+ emitActionEvent({
217
+ action: publicAction,
218
+ name: actionName,
219
+ args,
220
+ store: this._publicStore,
221
+ status: "error",
222
+ duration: Date.now() - start,
223
+ error
224
+ });
225
+ }
76
226
  throw error;
77
227
  }
78
228
  }
@@ -82,21 +232,46 @@ var StatoStore = class {
82
232
  if (!fn) throw new Error(`[Stato] Computed "${name}" introuvable`);
83
233
  return fn();
84
234
  }
235
+ getSelector(name) {
236
+ const fn = this._selectors[name];
237
+ if (!fn) throw new Error(`[Stato] Selector "${name}" introuvable`);
238
+ return fn();
239
+ }
85
240
  // ── Enregistrer un cleanup (pour fromStream) ───────
86
241
  registerCleanup(fn) {
87
242
  this._cleanups.push(fn);
88
243
  }
244
+ registerPublicAction(name, fn) {
245
+ this._publicActions[name] = fn;
246
+ }
247
+ hydrate(partial) {
248
+ this._setState(partial);
249
+ }
250
+ setPublicStore(publicStore) {
251
+ this._publicStore = publicStore;
252
+ this._runEffects(true);
253
+ }
89
254
  // ── Lifecycle — appelé par l'adaptateur Angular ────
90
255
  init(publicStore) {
91
- this._hooks.onInit?.(publicStore);
256
+ this._publicStore = publicStore;
257
+ if (!this._initialized) {
258
+ this._initialized = true;
259
+ this._hooks.onInit?.(publicStore);
260
+ }
261
+ this._runEffects(true);
92
262
  }
93
263
  destroy(publicStore) {
94
264
  this._hooks.onDestroy?.(publicStore);
265
+ for (const effect of this._effects) {
266
+ effect.cleanup?.();
267
+ effect.cleanup = void 0;
268
+ }
95
269
  for (const cleanup of this._cleanups) {
96
270
  cleanup();
97
271
  }
98
272
  this._cleanups = [];
99
273
  this._subscribers.clear();
274
+ this._initialized = false;
100
275
  }
101
276
  };
102
277
  function createStore(config) {
@@ -119,10 +294,12 @@ function createStore(config) {
119
294
  configurable: true
120
295
  });
121
296
  }
122
- const { actions, computed } = config;
297
+ const { actions, computed, selectors } = config;
123
298
  if (actions) {
124
299
  for (const name of Object.keys(actions)) {
125
- publicStore[name] = (...args) => store.dispatch(name, ...args);
300
+ const fn = (...args) => store.dispatch(name, ...args);
301
+ publicStore[name] = fn;
302
+ store.registerPublicAction(name, fn);
126
303
  }
127
304
  }
128
305
  if (computed) {
@@ -134,8 +311,32 @@ function createStore(config) {
134
311
  });
135
312
  }
136
313
  }
314
+ if (selectors) {
315
+ for (const name of Object.keys(selectors)) {
316
+ Object.defineProperty(publicStore, name, {
317
+ get: () => store.getSelector(name),
318
+ enumerable: true,
319
+ configurable: true
320
+ });
321
+ }
322
+ }
323
+ store.init(publicStore);
137
324
  return publicStore;
138
325
  }
326
+ 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
+ });
339
+ }
139
340
 
140
341
  // src/types.ts
141
342
  var StatoHttpError = class extends Error {
@@ -366,6 +567,1020 @@ function optimistic(immediate, confirm) {
366
567
  };
367
568
  }
368
569
 
570
+ // src/helpers/exclusive.ts
571
+ function exclusive(fn) {
572
+ let running = false;
573
+ let current = null;
574
+ return (state, ...args) => {
575
+ if (running && current) return current;
576
+ running = true;
577
+ current = (async () => {
578
+ try {
579
+ await fn(state, ...args);
580
+ } finally {
581
+ running = false;
582
+ current = null;
583
+ }
584
+ })();
585
+ return current;
586
+ };
587
+ }
588
+
589
+ // src/helpers/queued.ts
590
+ function queued(fn) {
591
+ const queue = [];
592
+ let processing = false;
593
+ const processNext = () => {
594
+ if (processing) return;
595
+ processing = true;
596
+ const run = async () => {
597
+ while (queue.length) {
598
+ const item = queue.shift();
599
+ if (!item) break;
600
+ try {
601
+ await fn(item.state, ...item.args);
602
+ item.resolve();
603
+ } catch (err) {
604
+ item.reject(err);
605
+ while (queue.length) {
606
+ const rest = queue.shift();
607
+ rest?.reject(err);
608
+ }
609
+ return;
610
+ }
611
+ }
612
+ };
613
+ void run().finally(() => {
614
+ processing = false;
615
+ });
616
+ };
617
+ return (state, ...args) => {
618
+ return new Promise((resolve, reject) => {
619
+ queue.push({ state, args, resolve, reject });
620
+ processNext();
621
+ });
622
+ };
623
+ }
624
+
625
+ // src/helpers/distinct-until-changed.ts
626
+ function distinctUntilChanged(fn, keySelector, comparator = Object.is) {
627
+ let initialized = false;
628
+ let prevKey;
629
+ return async (state, ...args) => {
630
+ const nextKey = keySelector(...args);
631
+ if (initialized && comparator(prevKey, nextKey)) {
632
+ return;
633
+ }
634
+ initialized = true;
635
+ prevKey = nextKey;
636
+ await fn(state, ...args);
637
+ };
638
+ }
639
+
640
+ // src/helpers/fork-join.ts
641
+ async function forkJoin(tasks, options) {
642
+ const controller = new AbortController();
643
+ const signal = options?.signal;
644
+ if (signal) {
645
+ if (signal.aborted) controller.abort();
646
+ else signal.addEventListener("abort", () => controller.abort(), { once: true });
647
+ }
648
+ const entries = Object.entries(tasks);
649
+ const results = await Promise.all(entries.map(async ([key, task]) => {
650
+ const value = await task({ signal: controller.signal });
651
+ return [key, value];
652
+ }));
653
+ return Object.fromEntries(results);
654
+ }
655
+
656
+ // src/helpers/race.ts
657
+ async function race(tasks, options) {
658
+ const controller = new AbortController();
659
+ const outer = options?.signal;
660
+ if (outer) {
661
+ if (outer.aborted) controller.abort();
662
+ else outer.addEventListener("abort", () => controller.abort(), { once: true });
663
+ }
664
+ const wrapped = tasks.map((task) => (async () => task({ signal: controller.signal }))());
665
+ try {
666
+ return await Promise.race(wrapped);
667
+ } finally {
668
+ controller.abort();
669
+ }
670
+ }
671
+
672
+ // src/helpers/combine-latest.ts
673
+ function combineLatest() {
674
+ return (...deps) => {
675
+ return (state) => deps.map((fn) => fn(state));
676
+ };
677
+ }
678
+
679
+ // src/helpers/combine-latest-stream.ts
680
+ function combineLatestStream(...sources) {
681
+ return {
682
+ subscribe(observer) {
683
+ const n = sources.length;
684
+ if (!n) {
685
+ observer.complete?.();
686
+ return { unsubscribe() {
687
+ } };
688
+ }
689
+ const hasValue = new Array(n).fill(false);
690
+ const values = new Array(n);
691
+ let completed = 0;
692
+ let closed = false;
693
+ const subs = [];
694
+ const tryEmit = () => {
695
+ if (closed) return;
696
+ if (hasValue.every(Boolean)) {
697
+ observer.next?.(values.slice());
698
+ }
699
+ };
700
+ const closeAll = () => {
701
+ if (closed) return;
702
+ closed = true;
703
+ for (const s of subs) {
704
+ try {
705
+ s.unsubscribe();
706
+ } catch {
707
+ }
708
+ }
709
+ };
710
+ sources.forEach((src, index) => {
711
+ const sub = src.subscribe({
712
+ next: (v) => {
713
+ if (closed) return;
714
+ values[index] = v;
715
+ hasValue[index] = true;
716
+ tryEmit();
717
+ },
718
+ error: (err) => {
719
+ if (closed) return;
720
+ observer.error?.(err);
721
+ closeAll();
722
+ },
723
+ complete: () => {
724
+ if (closed) return;
725
+ completed++;
726
+ if (completed >= n) {
727
+ observer.complete?.();
728
+ closeAll();
729
+ }
730
+ }
731
+ });
732
+ subs.push(sub);
733
+ });
734
+ return {
735
+ unsubscribe() {
736
+ closeAll();
737
+ }
738
+ };
739
+ }
740
+ };
741
+ }
742
+
743
+ // src/helpers/entity-adapter.ts
744
+ function defaultSelectId(entity) {
745
+ return entity.id;
746
+ }
747
+ function idKey(id) {
748
+ return String(id);
749
+ }
750
+ function ensureSort(state, sortComparer) {
751
+ if (!sortComparer) return;
752
+ state.ids.sort((a, b) => {
753
+ const ea = state.entities[idKey(a)];
754
+ const eb = state.entities[idKey(b)];
755
+ if (!ea || !eb) return 0;
756
+ return sortComparer(ea, eb);
757
+ });
758
+ }
759
+ function createEntityAdapter(options = {}) {
760
+ const selectId = options.selectId ?? defaultSelectId;
761
+ const sortComparer = options.sortComparer;
762
+ const getInitialState = (extra) => {
763
+ return {
764
+ ids: [],
765
+ entities: {},
766
+ ...extra ?? {}
767
+ };
768
+ };
769
+ const addOne = (entity, state) => {
770
+ const id = selectId(entity);
771
+ const key = idKey(id);
772
+ if (state.entities[key]) return;
773
+ state.ids.push(id);
774
+ state.entities[key] = entity;
775
+ ensureSort(state, sortComparer);
776
+ };
777
+ const addMany = (entities, state) => {
778
+ for (const entity of entities) addOne(entity, state);
779
+ };
780
+ const setAll = (entities, state) => {
781
+ state.ids = [];
782
+ state.entities = {};
783
+ for (const entity of entities) {
784
+ const id = selectId(entity);
785
+ state.ids.push(id);
786
+ state.entities[idKey(id)] = entity;
787
+ }
788
+ ensureSort(state, sortComparer);
789
+ };
790
+ const upsertOne = (entity, state) => {
791
+ const id = selectId(entity);
792
+ const key = idKey(id);
793
+ const exists = !!state.entities[key];
794
+ state.entities[key] = entity;
795
+ if (!exists) state.ids.push(id);
796
+ ensureSort(state, sortComparer);
797
+ };
798
+ const upsertMany = (entities, state) => {
799
+ for (const entity of entities) upsertOne(entity, state);
800
+ };
801
+ const updateOne = (update, state) => {
802
+ const key = idKey(update.id);
803
+ const current = state.entities[key];
804
+ if (!current) return;
805
+ state.entities[key] = { ...current, ...update.changes };
806
+ ensureSort(state, sortComparer);
807
+ };
808
+ const removeOne = (id, state) => {
809
+ const key = idKey(id);
810
+ if (!state.entities[key]) return;
811
+ delete state.entities[key];
812
+ state.ids = state.ids.filter((x) => !Object.is(x, id));
813
+ };
814
+ const removeMany = (ids, state) => {
815
+ const removeSet = new Set(ids.map(idKey));
816
+ for (const key of Object.keys(state.entities)) {
817
+ if (removeSet.has(key)) delete state.entities[key];
818
+ }
819
+ state.ids = state.ids.filter((id) => !removeSet.has(idKey(id)));
820
+ };
821
+ const removeAll = (state) => {
822
+ state.ids = [];
823
+ state.entities = {};
824
+ };
825
+ const getSelectors = (selectState) => {
826
+ const pick = (state) => selectState ? selectState(state) : state;
827
+ return {
828
+ selectIds: (state) => pick(state).ids,
829
+ selectEntities: (state) => pick(state).entities,
830
+ selectAll: (state) => {
831
+ const s = pick(state);
832
+ return s.ids.map((id) => s.entities[idKey(id)]).filter(Boolean);
833
+ },
834
+ selectTotal: (state) => pick(state).ids.length,
835
+ selectById: (state, id) => pick(state).entities[idKey(id)]
836
+ };
837
+ };
838
+ return {
839
+ selectId,
840
+ sortComparer,
841
+ getInitialState,
842
+ addOne,
843
+ addMany,
844
+ setAll,
845
+ upsertOne,
846
+ upsertMany,
847
+ updateOne,
848
+ removeOne,
849
+ removeMany,
850
+ removeAll,
851
+ getSelectors
852
+ };
853
+ }
854
+
855
+ // src/helpers/with-entities.ts
856
+ function cloneEntityState(state) {
857
+ return {
858
+ ids: [...state.ids],
859
+ entities: { ...state.entities }
860
+ };
861
+ }
862
+ function withEntities(config, options) {
863
+ const { key, adapter, initial } = options;
864
+ const initialSlice = adapter.getInitialState();
865
+ if (initial?.length) {
866
+ adapter.setAll(initial, initialSlice);
867
+ }
868
+ const baseSelectors = config.selectors ?? {};
869
+ const baseActions = config.actions ?? {};
870
+ const scopedSelectors = adapter.getSelectors((s) => s[key]);
871
+ const selectorNames = {
872
+ ids: options.selectors?.ids ?? `${key}Ids`,
873
+ entities: options.selectors?.entities ?? `${key}Entities`,
874
+ all: options.selectors?.all ?? `${key}All`,
875
+ total: options.selectors?.total ?? `${key}Total`,
876
+ byId: options.selectors?.byId ?? `${key}ById`
877
+ };
878
+ const actionNames = {
879
+ addOne: options.actions?.addOne ?? `${key}AddOne`,
880
+ addMany: options.actions?.addMany ?? `${key}AddMany`,
881
+ setAll: options.actions?.setAll ?? `${key}SetAll`,
882
+ upsertOne: options.actions?.upsertOne ?? `${key}UpsertOne`,
883
+ upsertMany: options.actions?.upsertMany ?? `${key}UpsertMany`,
884
+ updateOne: options.actions?.updateOne ?? `${key}UpdateOne`,
885
+ removeOne: options.actions?.removeOne ?? `${key}RemoveOne`,
886
+ removeMany: options.actions?.removeMany ?? `${key}RemoveMany`,
887
+ removeAll: options.actions?.removeAll ?? `${key}RemoveAll`
888
+ };
889
+ const nextSelectors = {
890
+ ...baseSelectors,
891
+ [selectorNames.ids]: (state) => scopedSelectors.selectIds(state),
892
+ [selectorNames.entities]: (state) => scopedSelectors.selectEntities(state),
893
+ [selectorNames.all]: (state) => scopedSelectors.selectAll(state),
894
+ [selectorNames.total]: (state) => scopedSelectors.selectTotal(state),
895
+ [selectorNames.byId]: (state) => (id) => scopedSelectors.selectById(state, id)
896
+ };
897
+ const nextActions = {
898
+ ...baseActions,
899
+ [actionNames.addOne]: (state, entity) => {
900
+ const prev = state[key];
901
+ const next = cloneEntityState(prev);
902
+ adapter.addOne(entity, next);
903
+ state[key] = next;
904
+ },
905
+ [actionNames.addMany]: (state, entities) => {
906
+ const prev = state[key];
907
+ const next = cloneEntityState(prev);
908
+ adapter.addMany(entities, next);
909
+ state[key] = next;
910
+ },
911
+ [actionNames.setAll]: (state, entities) => {
912
+ const next = adapter.getInitialState();
913
+ adapter.setAll(entities, next);
914
+ state[key] = next;
915
+ },
916
+ [actionNames.upsertOne]: (state, entity) => {
917
+ const prev = state[key];
918
+ const next = cloneEntityState(prev);
919
+ adapter.upsertOne(entity, next);
920
+ state[key] = next;
921
+ },
922
+ [actionNames.upsertMany]: (state, entities) => {
923
+ const prev = state[key];
924
+ const next = cloneEntityState(prev);
925
+ adapter.upsertMany(entities, next);
926
+ state[key] = next;
927
+ },
928
+ [actionNames.updateOne]: (state, update) => {
929
+ const prev = state[key];
930
+ const next = cloneEntityState(prev);
931
+ adapter.updateOne(update, next);
932
+ state[key] = next;
933
+ },
934
+ [actionNames.removeOne]: (state, id) => {
935
+ const prev = state[key];
936
+ const next = cloneEntityState(prev);
937
+ adapter.removeOne(id, next);
938
+ state[key] = next;
939
+ },
940
+ [actionNames.removeMany]: (state, ids) => {
941
+ const prev = state[key];
942
+ const next = cloneEntityState(prev);
943
+ adapter.removeMany(ids, next);
944
+ state[key] = next;
945
+ },
946
+ [actionNames.removeAll]: (state) => {
947
+ state[key] = adapter.getInitialState();
948
+ }
949
+ };
950
+ return {
951
+ ...config,
952
+ [key]: config[key] ?? initialSlice,
953
+ actions: nextActions,
954
+ selectors: nextSelectors
955
+ };
956
+ }
957
+
958
+ // src/helpers/stream-operators.ts
959
+ function pipeStream(source, ...ops) {
960
+ return ops.reduce((acc, op) => op(acc), source);
961
+ }
962
+ function isObservable(value) {
963
+ return !!value && typeof value === "object" && typeof value.subscribe === "function";
964
+ }
965
+ function toObservable(value) {
966
+ if (isObservable(value)) return value;
967
+ return {
968
+ subscribe(observer) {
969
+ let closed = false;
970
+ Promise.resolve(value).then((resolved) => {
971
+ if (closed) return;
972
+ observer.next?.(resolved);
973
+ observer.complete?.();
974
+ }).catch((error) => {
975
+ if (closed) return;
976
+ observer.error?.(error);
977
+ });
978
+ return { unsubscribe: () => {
979
+ closed = true;
980
+ } };
981
+ }
982
+ };
983
+ }
984
+ function mapStream(mapFn) {
985
+ return (source) => ({
986
+ subscribe(observer) {
987
+ return source.subscribe({
988
+ next: (value) => observer.next?.(mapFn(value)),
989
+ error: (error) => observer.error?.(error),
990
+ complete: () => observer.complete?.()
991
+ });
992
+ }
993
+ });
994
+ }
995
+ function filterStream(predicate) {
996
+ return (source) => ({
997
+ subscribe(observer) {
998
+ return source.subscribe({
999
+ next: (value) => {
1000
+ if (predicate(value)) observer.next?.(value);
1001
+ },
1002
+ error: (error) => observer.error?.(error),
1003
+ complete: () => observer.complete?.()
1004
+ });
1005
+ }
1006
+ });
1007
+ }
1008
+ function closeSubs(subs) {
1009
+ for (const sub of subs) {
1010
+ try {
1011
+ sub.unsubscribe();
1012
+ } catch {
1013
+ }
1014
+ }
1015
+ }
1016
+ function switchMapStream(mapper) {
1017
+ return (source) => ({
1018
+ subscribe(observer) {
1019
+ let closed = false;
1020
+ let sourceDone = false;
1021
+ let innerSub = null;
1022
+ let innerActive = false;
1023
+ let controller = null;
1024
+ const maybeComplete = () => {
1025
+ if (!closed && sourceDone && !innerActive) {
1026
+ closed = true;
1027
+ observer.complete?.();
1028
+ }
1029
+ };
1030
+ const sourceSub = source.subscribe({
1031
+ next: (value) => {
1032
+ if (closed) return;
1033
+ controller?.abort();
1034
+ innerSub?.unsubscribe();
1035
+ controller = new AbortController();
1036
+ innerActive = true;
1037
+ innerSub = toObservable(mapper(value, { signal: controller.signal })).subscribe({
1038
+ next: (v) => {
1039
+ if (!closed) observer.next?.(v);
1040
+ },
1041
+ error: (error) => {
1042
+ if (closed) return;
1043
+ closed = true;
1044
+ observer.error?.(error);
1045
+ sourceSub.unsubscribe();
1046
+ innerSub?.unsubscribe();
1047
+ },
1048
+ complete: () => {
1049
+ innerActive = false;
1050
+ maybeComplete();
1051
+ }
1052
+ });
1053
+ },
1054
+ error: (error) => {
1055
+ if (closed) return;
1056
+ closed = true;
1057
+ observer.error?.(error);
1058
+ controller?.abort();
1059
+ innerSub?.unsubscribe();
1060
+ },
1061
+ complete: () => {
1062
+ sourceDone = true;
1063
+ maybeComplete();
1064
+ }
1065
+ });
1066
+ return {
1067
+ unsubscribe() {
1068
+ if (closed) return;
1069
+ closed = true;
1070
+ controller?.abort();
1071
+ sourceSub.unsubscribe();
1072
+ innerSub?.unsubscribe();
1073
+ }
1074
+ };
1075
+ }
1076
+ });
1077
+ }
1078
+ function concatMapStream(mapper) {
1079
+ return (source) => ({
1080
+ subscribe(observer) {
1081
+ let closed = false;
1082
+ let sourceDone = false;
1083
+ const queue = [];
1084
+ let running = false;
1085
+ let currentSub = null;
1086
+ let currentController = null;
1087
+ const maybeComplete = () => {
1088
+ if (!closed && sourceDone && !running && queue.length === 0) {
1089
+ closed = true;
1090
+ observer.complete?.();
1091
+ }
1092
+ };
1093
+ const runNext = () => {
1094
+ if (closed || running || queue.length === 0) {
1095
+ maybeComplete();
1096
+ return;
1097
+ }
1098
+ running = true;
1099
+ const value = queue.shift();
1100
+ currentController = new AbortController();
1101
+ currentSub = toObservable(mapper(value, { signal: currentController.signal })).subscribe({
1102
+ next: (v) => {
1103
+ if (!closed) observer.next?.(v);
1104
+ },
1105
+ error: (error) => {
1106
+ if (closed) return;
1107
+ closed = true;
1108
+ observer.error?.(error);
1109
+ sourceSub.unsubscribe();
1110
+ currentController?.abort();
1111
+ currentSub?.unsubscribe();
1112
+ queue.length = 0;
1113
+ },
1114
+ complete: () => {
1115
+ running = false;
1116
+ runNext();
1117
+ }
1118
+ });
1119
+ };
1120
+ const sourceSub = source.subscribe({
1121
+ next: (value) => {
1122
+ if (closed) return;
1123
+ queue.push(value);
1124
+ runNext();
1125
+ },
1126
+ error: (error) => {
1127
+ if (closed) return;
1128
+ closed = true;
1129
+ observer.error?.(error);
1130
+ currentController?.abort();
1131
+ currentSub?.unsubscribe();
1132
+ queue.length = 0;
1133
+ },
1134
+ complete: () => {
1135
+ sourceDone = true;
1136
+ maybeComplete();
1137
+ }
1138
+ });
1139
+ return {
1140
+ unsubscribe() {
1141
+ if (closed) return;
1142
+ closed = true;
1143
+ sourceSub.unsubscribe();
1144
+ currentController?.abort();
1145
+ currentSub?.unsubscribe();
1146
+ queue.length = 0;
1147
+ }
1148
+ };
1149
+ }
1150
+ });
1151
+ }
1152
+ function exhaustMapStream(mapper) {
1153
+ return (source) => ({
1154
+ subscribe(observer) {
1155
+ let closed = false;
1156
+ let sourceDone = false;
1157
+ let running = false;
1158
+ let currentSub = null;
1159
+ let controller = null;
1160
+ const maybeComplete = () => {
1161
+ if (!closed && sourceDone && !running) {
1162
+ closed = true;
1163
+ observer.complete?.();
1164
+ }
1165
+ };
1166
+ const sourceSub = source.subscribe({
1167
+ next: (value) => {
1168
+ if (closed || running) return;
1169
+ running = true;
1170
+ controller = new AbortController();
1171
+ currentSub = toObservable(mapper(value, { signal: controller.signal })).subscribe({
1172
+ next: (v) => {
1173
+ if (!closed) observer.next?.(v);
1174
+ },
1175
+ error: (error) => {
1176
+ if (closed) return;
1177
+ closed = true;
1178
+ observer.error?.(error);
1179
+ sourceSub.unsubscribe();
1180
+ controller?.abort();
1181
+ currentSub?.unsubscribe();
1182
+ },
1183
+ complete: () => {
1184
+ running = false;
1185
+ maybeComplete();
1186
+ }
1187
+ });
1188
+ },
1189
+ error: (error) => {
1190
+ if (closed) return;
1191
+ closed = true;
1192
+ observer.error?.(error);
1193
+ controller?.abort();
1194
+ currentSub?.unsubscribe();
1195
+ },
1196
+ complete: () => {
1197
+ sourceDone = true;
1198
+ maybeComplete();
1199
+ }
1200
+ });
1201
+ return {
1202
+ unsubscribe() {
1203
+ if (closed) return;
1204
+ closed = true;
1205
+ sourceSub.unsubscribe();
1206
+ controller?.abort();
1207
+ currentSub?.unsubscribe();
1208
+ }
1209
+ };
1210
+ }
1211
+ });
1212
+ }
1213
+ function mergeMapStream(mapper, options) {
1214
+ const concurrency = Math.max(1, options?.concurrency ?? Number.POSITIVE_INFINITY);
1215
+ return (source) => ({
1216
+ subscribe(observer) {
1217
+ let closed = false;
1218
+ let sourceDone = false;
1219
+ const queue = [];
1220
+ const active = /* @__PURE__ */ new Set();
1221
+ const controllers = /* @__PURE__ */ new Set();
1222
+ const maybeComplete = () => {
1223
+ if (!closed && sourceDone && active.size === 0 && queue.length === 0) {
1224
+ closed = true;
1225
+ observer.complete?.();
1226
+ }
1227
+ };
1228
+ const spawn = (value) => {
1229
+ const controller = new AbortController();
1230
+ controllers.add(controller);
1231
+ const sub = toObservable(mapper(value, { signal: controller.signal })).subscribe({
1232
+ next: (v) => {
1233
+ if (!closed) observer.next?.(v);
1234
+ },
1235
+ error: (error) => {
1236
+ if (closed) return;
1237
+ closed = true;
1238
+ observer.error?.(error);
1239
+ sourceSub.unsubscribe();
1240
+ closeSubs(Array.from(active));
1241
+ active.clear();
1242
+ for (const c of controllers) c.abort();
1243
+ controllers.clear();
1244
+ queue.length = 0;
1245
+ },
1246
+ complete: () => {
1247
+ active.delete(sub);
1248
+ controllers.delete(controller);
1249
+ drain();
1250
+ maybeComplete();
1251
+ }
1252
+ });
1253
+ active.add(sub);
1254
+ };
1255
+ const drain = () => {
1256
+ while (!closed && active.size < concurrency && queue.length > 0) {
1257
+ spawn(queue.shift());
1258
+ }
1259
+ };
1260
+ const sourceSub = source.subscribe({
1261
+ next: (value) => {
1262
+ if (closed) return;
1263
+ queue.push(value);
1264
+ drain();
1265
+ },
1266
+ error: (error) => {
1267
+ if (closed) return;
1268
+ closed = true;
1269
+ observer.error?.(error);
1270
+ closeSubs(Array.from(active));
1271
+ active.clear();
1272
+ for (const c of controllers) c.abort();
1273
+ controllers.clear();
1274
+ queue.length = 0;
1275
+ },
1276
+ complete: () => {
1277
+ sourceDone = true;
1278
+ maybeComplete();
1279
+ }
1280
+ });
1281
+ return {
1282
+ unsubscribe() {
1283
+ if (closed) return;
1284
+ closed = true;
1285
+ sourceSub.unsubscribe();
1286
+ closeSubs(Array.from(active));
1287
+ active.clear();
1288
+ for (const c of controllers) c.abort();
1289
+ controllers.clear();
1290
+ queue.length = 0;
1291
+ }
1292
+ };
1293
+ }
1294
+ });
1295
+ }
1296
+ function distinctUntilChangedStream(keySelector, comparator = Object.is) {
1297
+ return (source) => ({
1298
+ subscribe(observer) {
1299
+ let initialized = false;
1300
+ let prevKey;
1301
+ return source.subscribe({
1302
+ next: (value) => {
1303
+ const nextKey = keySelector ? keySelector(value) : value;
1304
+ if (initialized && comparator(prevKey, nextKey)) return;
1305
+ initialized = true;
1306
+ prevKey = nextKey;
1307
+ observer.next?.(value);
1308
+ },
1309
+ error: (error) => observer.error?.(error),
1310
+ complete: () => observer.complete?.()
1311
+ });
1312
+ }
1313
+ });
1314
+ }
1315
+ function debounceStream(ms) {
1316
+ return (source) => ({
1317
+ subscribe(observer) {
1318
+ let timer = null;
1319
+ let sourceDone = false;
1320
+ let lastValue;
1321
+ let hasValue = false;
1322
+ let closed = false;
1323
+ const flush = () => {
1324
+ if (!hasValue || closed) return;
1325
+ observer.next?.(lastValue);
1326
+ hasValue = false;
1327
+ lastValue = void 0;
1328
+ };
1329
+ const maybeComplete = () => {
1330
+ if (sourceDone && !timer && !closed) {
1331
+ closed = true;
1332
+ observer.complete?.();
1333
+ }
1334
+ };
1335
+ const sub = source.subscribe({
1336
+ next: (value) => {
1337
+ if (closed) return;
1338
+ lastValue = value;
1339
+ hasValue = true;
1340
+ if (timer) clearTimeout(timer);
1341
+ timer = setTimeout(() => {
1342
+ timer = null;
1343
+ flush();
1344
+ maybeComplete();
1345
+ }, ms);
1346
+ },
1347
+ error: (error) => {
1348
+ if (closed) return;
1349
+ closed = true;
1350
+ if (timer) clearTimeout(timer);
1351
+ observer.error?.(error);
1352
+ },
1353
+ complete: () => {
1354
+ sourceDone = true;
1355
+ if (!timer) {
1356
+ maybeComplete();
1357
+ }
1358
+ }
1359
+ });
1360
+ return {
1361
+ unsubscribe() {
1362
+ if (closed) return;
1363
+ closed = true;
1364
+ if (timer) clearTimeout(timer);
1365
+ sub.unsubscribe();
1366
+ }
1367
+ };
1368
+ }
1369
+ });
1370
+ }
1371
+ function throttleStream(ms) {
1372
+ return (source) => ({
1373
+ subscribe(observer) {
1374
+ let throttled2 = false;
1375
+ let timer = null;
1376
+ let closed = false;
1377
+ const sub = source.subscribe({
1378
+ next: (value) => {
1379
+ if (closed || throttled2) return;
1380
+ observer.next?.(value);
1381
+ throttled2 = true;
1382
+ timer = setTimeout(() => {
1383
+ throttled2 = false;
1384
+ timer = null;
1385
+ }, ms);
1386
+ },
1387
+ error: (error) => {
1388
+ if (closed) return;
1389
+ closed = true;
1390
+ if (timer) clearTimeout(timer);
1391
+ observer.error?.(error);
1392
+ },
1393
+ complete: () => {
1394
+ if (closed) return;
1395
+ closed = true;
1396
+ if (timer) clearTimeout(timer);
1397
+ observer.complete?.();
1398
+ }
1399
+ });
1400
+ return {
1401
+ unsubscribe() {
1402
+ if (closed) return;
1403
+ closed = true;
1404
+ if (timer) clearTimeout(timer);
1405
+ sub.unsubscribe();
1406
+ }
1407
+ };
1408
+ }
1409
+ });
1410
+ }
1411
+ function catchErrorStream(handler) {
1412
+ return (source) => ({
1413
+ subscribe(observer) {
1414
+ let closed = false;
1415
+ let fallbackSub = null;
1416
+ const sourceSub = source.subscribe({
1417
+ next: (value) => {
1418
+ if (!closed) observer.next?.(value);
1419
+ },
1420
+ error: (error) => {
1421
+ if (closed) return;
1422
+ fallbackSub = toObservable(handler(error)).subscribe({
1423
+ next: (value) => {
1424
+ if (!closed) observer.next?.(value);
1425
+ },
1426
+ error: (innerErr) => {
1427
+ if (closed) return;
1428
+ closed = true;
1429
+ observer.error?.(innerErr);
1430
+ },
1431
+ complete: () => {
1432
+ if (closed) return;
1433
+ closed = true;
1434
+ observer.complete?.();
1435
+ }
1436
+ });
1437
+ },
1438
+ complete: () => {
1439
+ if (closed) return;
1440
+ closed = true;
1441
+ observer.complete?.();
1442
+ }
1443
+ });
1444
+ return {
1445
+ unsubscribe() {
1446
+ if (closed) return;
1447
+ closed = true;
1448
+ sourceSub.unsubscribe();
1449
+ fallbackSub?.unsubscribe();
1450
+ }
1451
+ };
1452
+ }
1453
+ });
1454
+ }
1455
+ function retryStream(options = {}) {
1456
+ const attempts = Math.max(1, options.attempts ?? 3);
1457
+ const delay = Math.max(0, options.delay ?? 0);
1458
+ const backoff = options.backoff ?? "fixed";
1459
+ return (source) => ({
1460
+ subscribe(observer) {
1461
+ let closed = false;
1462
+ let attempt = 0;
1463
+ let activeSub = null;
1464
+ let retryTimer = null;
1465
+ const subscribeOnce = () => {
1466
+ if (closed) return;
1467
+ attempt++;
1468
+ activeSub = source.subscribe({
1469
+ next: (value) => {
1470
+ if (!closed) observer.next?.(value);
1471
+ },
1472
+ complete: () => {
1473
+ if (closed) return;
1474
+ closed = true;
1475
+ observer.complete?.();
1476
+ },
1477
+ error: (error) => {
1478
+ if (closed) return;
1479
+ if (attempt >= attempts) {
1480
+ closed = true;
1481
+ observer.error?.(error);
1482
+ return;
1483
+ }
1484
+ const wait = backoff === "exponential" ? delay * Math.pow(2, attempt - 1) : delay;
1485
+ retryTimer = setTimeout(() => {
1486
+ retryTimer = null;
1487
+ subscribeOnce();
1488
+ }, wait);
1489
+ }
1490
+ });
1491
+ };
1492
+ subscribeOnce();
1493
+ return {
1494
+ unsubscribe() {
1495
+ if (closed) return;
1496
+ closed = true;
1497
+ if (retryTimer) clearTimeout(retryTimer);
1498
+ activeSub?.unsubscribe();
1499
+ }
1500
+ };
1501
+ }
1502
+ });
1503
+ }
1504
+
1505
+ // src/helpers/with-persist.ts
1506
+ function resolveStorage(custom) {
1507
+ if (custom) return custom;
1508
+ if (typeof window === "undefined") return null;
1509
+ try {
1510
+ return window.localStorage;
1511
+ } catch {
1512
+ return null;
1513
+ }
1514
+ }
1515
+ function pickState(state, keys) {
1516
+ if (!keys?.length) return state;
1517
+ const picked = {};
1518
+ for (const key of keys) {
1519
+ picked[key] = state[key];
1520
+ }
1521
+ return picked;
1522
+ }
1523
+ function withPersist(config, options) {
1524
+ const {
1525
+ key,
1526
+ version = 1,
1527
+ storage: customStorage,
1528
+ pick,
1529
+ migrate,
1530
+ onError
1531
+ } = options;
1532
+ const storage = resolveStorage(customStorage);
1533
+ const userHooks = config.hooks ?? {};
1534
+ const mergedHooks = {
1535
+ ...userHooks,
1536
+ onInit(store) {
1537
+ try {
1538
+ if (!storage) return userHooks.onInit?.(store);
1539
+ const raw = storage.getItem(key);
1540
+ if (!raw) return userHooks.onInit?.(store);
1541
+ const parsed = JSON.parse(raw);
1542
+ const data = parsed.v === version ? parsed.data : migrate ? migrate(parsed.data, parsed.v) : parsed.data;
1543
+ if (data && typeof data === "object") {
1544
+ store.__store__?.hydrate?.(data);
1545
+ }
1546
+ } catch (error) {
1547
+ onError?.(error);
1548
+ }
1549
+ return userHooks.onInit?.(store);
1550
+ },
1551
+ onStateChange(prev, next) {
1552
+ try {
1553
+ if (storage) {
1554
+ const payload = {
1555
+ v: version,
1556
+ data: pickState(next, pick)
1557
+ };
1558
+ storage.setItem(key, JSON.stringify(payload));
1559
+ }
1560
+ } catch (error) {
1561
+ onError?.(error);
1562
+ }
1563
+ userHooks.onStateChange?.(prev, next);
1564
+ },
1565
+ onDestroy(store) {
1566
+ return userHooks.onDestroy?.(store);
1567
+ },
1568
+ onAction(name, args) {
1569
+ return userHooks.onAction?.(name, args);
1570
+ },
1571
+ onActionDone(name, duration) {
1572
+ return userHooks.onActionDone?.(name, duration);
1573
+ },
1574
+ onError(error, actionName) {
1575
+ return userHooks.onError?.(error, actionName);
1576
+ }
1577
+ };
1578
+ return {
1579
+ ...config,
1580
+ hooks: mergedHooks
1581
+ };
1582
+ }
1583
+
369
1584
  // src/devtools.ts
370
1585
  function createDevTools(maxLogs = 50) {
371
1586
  let counter = 0;
@@ -455,17 +1670,40 @@ function connectDevTools(store, storeName) {
455
1670
  exports.StatoHttp = StatoHttp;
456
1671
  exports.StatoHttpError = StatoHttpError;
457
1672
  exports.abortable = abortable;
1673
+ exports.catchErrorStream = catchErrorStream;
1674
+ exports.combineLatest = combineLatest;
1675
+ exports.combineLatestStream = combineLatestStream;
1676
+ exports.concatMapStream = concatMapStream;
458
1677
  exports.configureHttp = configureHttp;
459
1678
  exports.connectDevTools = connectDevTools;
460
1679
  exports.createDevTools = createDevTools;
1680
+ exports.createEntityAdapter = createEntityAdapter;
461
1681
  exports.createHttp = createHttp;
462
1682
  exports.createStore = createStore;
1683
+ exports.debounceStream = debounceStream;
463
1684
  exports.debounced = debounced;
464
1685
  exports.devTools = devTools;
1686
+ exports.distinctUntilChanged = distinctUntilChanged;
1687
+ exports.distinctUntilChangedStream = distinctUntilChangedStream;
1688
+ exports.exclusive = exclusive;
1689
+ exports.exhaustMapStream = exhaustMapStream;
1690
+ exports.filterStream = filterStream;
1691
+ exports.forkJoin = forkJoin;
465
1692
  exports.fromStream = fromStream;
466
1693
  exports.http = http;
1694
+ exports.mapStream = mapStream;
1695
+ exports.mergeMapStream = mergeMapStream;
1696
+ exports.on = on;
467
1697
  exports.optimistic = optimistic;
1698
+ exports.pipeStream = pipeStream;
1699
+ exports.queued = queued;
1700
+ exports.race = race;
1701
+ exports.retryStream = retryStream;
468
1702
  exports.retryable = retryable;
1703
+ exports.switchMapStream = switchMapStream;
1704
+ exports.throttleStream = throttleStream;
469
1705
  exports.throttled = throttled;
1706
+ exports.withEntities = withEntities;
1707
+ exports.withPersist = withPersist;
470
1708
  //# sourceMappingURL=index.js.map
471
1709
  //# sourceMappingURL=index.js.map