@yoltra/core 0.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.
@@ -0,0 +1,1557 @@
1
+ /*!
2
+ * @yoltra/core v0.1.0
3
+ * (c) 2026 Manu Ramirez <@pixerael>
4
+ * License: MIT
5
+ * Homepage: https://yoltra.dev
6
+ */
7
+ var M = Object.defineProperty;
8
+ var P = (a, e, t) => e in a ? M(a, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : a[e] = t;
9
+ var u = (a, e, t) => P(a, typeof e != "symbol" ? e + "" : e, t);
10
+ class z {
11
+ constructor() {
12
+ /**
13
+ * Internal registry: `channel → type → Set<handler>`.
14
+ * @internal
15
+ */
16
+ u(this, "handlers", /* @__PURE__ */ new Map());
17
+ }
18
+ /**
19
+ * Subscribes a handler to an exact `(channel, type)`.
20
+ *
21
+ * @typeParam C - Channel key (must be a string key of `EM`).
22
+ * @typeParam T - Type key within channel `C` (must be a string key of `EM[C]`).
23
+ * @param channel - Channel name to subscribe to.
24
+ * @param type - Event type within the channel.
25
+ * @param handler - Function invoked with the payload type `EM[C][T]`.
26
+ * @returns An **unsubscribe** function that removes this handler.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * const off = bus.on('data', 'loaded', ({ items }) => {
31
+ * console.log('Loaded', items.length, 'items');
32
+ * });
33
+ *
34
+ * // Later, stop listening:
35
+ * off();
36
+ * ```
37
+ *
38
+ * @public
39
+ */
40
+ on(e, t, s) {
41
+ let n = this.handlers.get(e);
42
+ n || (n = /* @__PURE__ */ new Map(), this.handlers.set(e, n));
43
+ let r = n.get(t);
44
+ return r || (r = /* @__PURE__ */ new Set(), n.set(t, r)), r.add(s), () => this.off(e, t, s);
45
+ }
46
+ /**
47
+ * Removes a specific handler previously added with {@link EventBus.on | `on`}.
48
+ *
49
+ * @typeParam C - Channel key (string key of `EM`).
50
+ * @typeParam T - Type key within channel `C` (string key of `EM[C]`).
51
+ * @param channel - Channel name of the subscription to remove.
52
+ * @param type - Event type of the subscription to remove.
53
+ * @param handler - The same handler reference that was passed to `on`.
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * const h = (n: number) => console.log('inc', n);
58
+ * bus.on('math', 'inc', h);
59
+ *
60
+ * // Explicitly remove this handler:
61
+ * bus.off('math', 'inc', h);
62
+ * ```
63
+ *
64
+ * @public
65
+ */
66
+ off(e, t, s) {
67
+ const n = this.handlers.get(e);
68
+ if (!n) return;
69
+ const r = n.get(t);
70
+ r && (r.delete(s), r.size === 0 && n.delete(t), n.size === 0 && this.handlers.delete(e));
71
+ }
72
+ /**
73
+ * Emits an event to all subscribers of the exact `(channel, type)`.
74
+ *
75
+ * Handlers are invoked **synchronously**. Any exception thrown by a handler is
76
+ * caught and logged, and other handlers still run.
77
+ *
78
+ * @typeParam C - Channel key (string key of `EM`).
79
+ * @typeParam T - Type key within channel `C` (string key of `EM[C]`).
80
+ * @param channel - Channel name to emit on.
81
+ * @param type - Event type to emit.
82
+ * @param payload - Payload matching `EM[C][T]`.
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * bus.emit('ui', 'toggle', false);
87
+ * ```
88
+ *
89
+ * @public
90
+ */
91
+ emit(e, t, s) {
92
+ const n = this.handlers.get(e);
93
+ if (!n) return;
94
+ const r = n.get(t);
95
+ if (!(!r || r.size === 0))
96
+ for (const o of [...r])
97
+ try {
98
+ o(s);
99
+ } catch (i) {
100
+ console.error("EventBus handler error:", i);
101
+ }
102
+ }
103
+ /**
104
+ * Clears **all** listeners across all channels/types.
105
+ *
106
+ * Useful for tests or during HMR teardown to avoid duplicate handlers.
107
+ *
108
+ * @example
109
+ * ```ts
110
+ * // In a test teardown:
111
+ * afterEach(() => bus.clear());
112
+ * ```
113
+ *
114
+ * @public
115
+ */
116
+ clear() {
117
+ this.handlers.clear();
118
+ }
119
+ }
120
+ class k {
121
+ constructor() {
122
+ /**
123
+ * Exact handlers: `channel → type → [handlers]`.
124
+ * @internal
125
+ */
126
+ u(this, "handlers", /* @__PURE__ */ new Map());
127
+ /**
128
+ * Pattern handlers with `*` and `**`: `channel → pattern(string) → [handlers]`.
129
+ * @internal
130
+ */
131
+ u(this, "patternHandlers", /* @__PURE__ */ new Map());
132
+ }
133
+ /**
134
+ * Subscribes a handler to either an **exact** type or a **pattern**.
135
+ *
136
+ * @param channel - Channel to subscribe on.
137
+ * @param type - Exact event type (e.g. `"a.b"`) or pattern (contains `*`/`**`).
138
+ * @param handler - Function invoked with the emitted payload.
139
+ * @returns An **unsubscribe** function that removes this handler.
140
+ *
141
+ * @remarks
142
+ * - Exact subscriptions are stored under a **normalized** key (leading `.` removed).
143
+ * - Pattern subscriptions are stored **as provided**; matching normalizes the subject.
144
+ *
145
+ * @example Exact subscription
146
+ * ```ts
147
+ * const off = bus.on('data', 'items.loaded', ({ count }) => {
148
+ * console.log('Loaded', count);
149
+ * });
150
+ * // Later
151
+ * off();
152
+ * ```
153
+ *
154
+ * @example Pattern subscription
155
+ * ```ts
156
+ * // Match any single sub-event: 'panel.open', 'panel.close', etc.
157
+ * const offStar = bus.on('ui', 'panel.*', () => {});
158
+ *
159
+ * // Match any depth: 'panel.open', 'panel.items.add', 'panel', etc.
160
+ * const offGlob = bus.on('ui', 'panel.**', () => {});
161
+ * ```
162
+ *
163
+ * @public
164
+ */
165
+ on(e, t, s) {
166
+ const n = String(t);
167
+ if (this.isPattern(n)) {
168
+ const r = n;
169
+ this.patternHandlers.has(e) || this.patternHandlers.set(e, /* @__PURE__ */ new Map());
170
+ const o = this.patternHandlers.get(e);
171
+ return o.has(r) || o.set(r, []), o.get(r).push(s), () => this.offPattern(e, r, s);
172
+ } else {
173
+ const r = this.normalizeTypeKey(n);
174
+ this.handlers.has(e) || this.handlers.set(e, /* @__PURE__ */ new Map());
175
+ const o = this.handlers.get(e);
176
+ return o.has(r) || o.set(r, []), o.get(r).push(s), () => this.offExactNormalized(e, r, s);
177
+ }
178
+ }
179
+ /**
180
+ * Unsubscribes an **exact** handler. The `type` key is normalized internally,
181
+ * so callers can pass `"foo"` or `".foo"` interchangeably.
182
+ *
183
+ * @param channel - Channel name.
184
+ * @param type - Exact event type key to remove (normalization applied).
185
+ * @param handler - The same handler reference previously passed to {@link LooseEventBus.on | `on`}.
186
+ *
187
+ * @example
188
+ * ```ts
189
+ * const h = () => {};
190
+ * bus.on('ui', 'panel.open', h);
191
+ * // Remove it (with or without leading dot)
192
+ * bus.off('ui', '.panel.open', h);
193
+ * ```
194
+ *
195
+ * @public
196
+ */
197
+ off(e, t, s) {
198
+ const n = this.normalizeTypeKey(String(t));
199
+ this.offExactNormalized(e, n, s);
200
+ }
201
+ /**
202
+ * Internal exact unsubscription using an already **normalized** type key.
203
+ *
204
+ * @param channel - Channel name.
205
+ * @param normalizedType - Event type key with leading dot removed.
206
+ * @param handler - Handler to remove.
207
+ * @internal
208
+ */
209
+ offExactNormalized(e, t, s) {
210
+ const n = this.handlers.get(e);
211
+ if (!n) return;
212
+ const r = n.get(t);
213
+ if (!r) return;
214
+ const o = r.indexOf(s);
215
+ o !== -1 && r.splice(o, 1), r.length === 0 && n.delete(t), n.size === 0 && this.handlers.delete(e);
216
+ }
217
+ /**
218
+ * Internal removal for a **pattern** subscription. No-ops if missing.
219
+ *
220
+ * @param channel - Channel name.
221
+ * @param pattern - Pattern string as originally subscribed.
222
+ * @param handler - Handler to remove.
223
+ * @internal
224
+ */
225
+ offPattern(e, t, s) {
226
+ const n = this.patternHandlers.get(e);
227
+ if (!n) return;
228
+ const r = n.get(t);
229
+ if (!r) return;
230
+ const o = r.indexOf(s);
231
+ o !== -1 && r.splice(o, 1), r.length === 0 && n.delete(t), n.size === 0 && this.patternHandlers.delete(e);
232
+ }
233
+ /**
234
+ * Emits an event to all exact subscribers first, then to **matching pattern** subscribers.
235
+ * Duplicate handler references are called **once** (de-duped).
236
+ *
237
+ * @param channel - Channel to emit on.
238
+ * @param type - Event type (subject). A leading dot is ignored for matching.
239
+ * @param payload - Payload delivered to handlers.
240
+ *
241
+ * @example
242
+ * ```ts
243
+ * // Suppose:
244
+ * // - on('ui', 'panel.open', h)
245
+ * // - on('ui', 'panel.*', h) // same handler ref!
246
+ * // - on('ui', 'panel.**', other)
247
+ * bus.emit('ui', 'panel.open', { id: 1 });
248
+ * // => 'h' runs once (de-duped), then 'other'
249
+ * ```
250
+ *
251
+ * @public
252
+ */
253
+ emit(e, t, s) {
254
+ const n = String(t), r = this.normalizeTypeKey(n), o = this.handlers.get(e)?.get(r) ?? [], i = this.patternHandlers.get(e), c = [];
255
+ if (i && i.size) {
256
+ const l = this.normalizeTypeKey(n);
257
+ for (const [p, d] of i.entries())
258
+ this.matchPattern(p, l) && c.push(d);
259
+ }
260
+ const f = /* @__PURE__ */ new Set(), h = (l) => {
261
+ for (const p of [...l])
262
+ if (!f.has(p)) {
263
+ f.add(p);
264
+ try {
265
+ p(s);
266
+ } catch (d) {
267
+ console.error(d);
268
+ continue;
269
+ }
270
+ }
271
+ };
272
+ h(o);
273
+ for (const l of c) h(l);
274
+ }
275
+ /**
276
+ * Determines if a string is a **pattern** (contains `*`).
277
+ * @param s - Event type or pattern string.
278
+ * @returns `true` if it contains at least one `*`, else `false`.
279
+ * @internal
280
+ */
281
+ isPattern(e) {
282
+ return e.includes("*");
283
+ }
284
+ /**
285
+ * Normalizes event type keys for exact matching by stripping a **single** leading dot.
286
+ *
287
+ * @param s - Event type key.
288
+ * @returns Normalized key without a leading dot.
289
+ * @example
290
+ * ```ts
291
+ * normalizeTypeKey('.a.b') // 'a.b'
292
+ * normalizeTypeKey('a.b') // 'a.b'
293
+ * ```
294
+ * @internal
295
+ */
296
+ normalizeTypeKey(e) {
297
+ return e.replace(/^\./, "");
298
+ }
299
+ /**
300
+ * Splits a path into dot-separated segments after normalization and removes empties.
301
+ * @param p - Event type or pattern string.
302
+ * @internal
303
+ */
304
+ splitPath(e) {
305
+ return this.normalizeTypeKey(e).split(".").filter(Boolean);
306
+ }
307
+ /**
308
+ * Pattern matcher over dot-separated segments.
309
+ *
310
+ * Rules:
311
+ * - **literal**: exact match.
312
+ * - `*` : matches exactly **one** segment.
313
+ * - `**` : matches **zero or more** remaining segments (including empty).
314
+ *
315
+ * @param pattern - Pattern (may include `*`/`**`).
316
+ * @param path - Subject event type key to test.
317
+ * @returns `true` if the pattern matches the path; otherwise `false`.
318
+ *
319
+ * @example
320
+ * ```ts
321
+ * matchPattern('a.*', 'a.b') // true
322
+ * matchPattern('a.*', 'a.b.c') // false
323
+ * matchPattern('a.**', 'a') // true
324
+ * matchPattern('a.**', 'a.b.c') // true
325
+ * matchPattern('**.end', 'x.y.end') // true
326
+ * ```
327
+ *
328
+ * @internal
329
+ */
330
+ matchPattern(e, t) {
331
+ const s = this.splitPath(e), n = this.splitPath(t);
332
+ let r = 0, o = 0;
333
+ for (; r < s.length && o < n.length; ) {
334
+ const i = s[r];
335
+ if (i === "**") {
336
+ if (r === s.length - 1 || s.slice(r).filter((f) => f !== "**").length === 0) return !0;
337
+ for (let f = o; f <= n.length; f++)
338
+ if (this.matchPattern(s.slice(r + 1).join("."), n.slice(f).join(".")))
339
+ return !0;
340
+ return !1;
341
+ }
342
+ if (i === "*" || i === n[o]) {
343
+ r++, o++;
344
+ continue;
345
+ }
346
+ return !1;
347
+ }
348
+ if (o === n.length) {
349
+ for (; r < s.length; r++)
350
+ if (s[r] !== "**") return !1;
351
+ return !0;
352
+ }
353
+ return !1;
354
+ }
355
+ /**
356
+ * Removes **all** listeners (exact and pattern). Useful for tests/HMR teardown.
357
+ *
358
+ * @example
359
+ * ```ts
360
+ * afterEach(() => bus.clear());
361
+ * ```
362
+ *
363
+ * @public
364
+ */
365
+ clear() {
366
+ this.handlers.clear(), this.patternHandlers.clear();
367
+ }
368
+ /**
369
+ * Returns a snapshot of all registered subscriptions for DevTools introspection.
370
+ *
371
+ * @returns An array of `{ channel, type, count }` entries for each distinct
372
+ * (channel, type/pattern) pair with at least one handler.
373
+ *
374
+ * @internal
375
+ */
376
+ __introspect() {
377
+ const e = [];
378
+ for (const [t, s] of this.handlers)
379
+ for (const [n, r] of s)
380
+ r.length > 0 && e.push({ channel: t, type: n, count: r.length });
381
+ for (const [t, s] of this.patternHandlers)
382
+ for (const [n, r] of s)
383
+ r.length > 0 && e.push({ channel: t, type: n, count: r.length });
384
+ return e;
385
+ }
386
+ }
387
+ class _ {
388
+ /**
389
+ * Creates a new {@link Reducer} from a pure reducer function.
390
+ *
391
+ * @param reduce - A function `(state, event) => nextState` that implements your update logic.
392
+ *
393
+ * @example
394
+ * ```ts
395
+ * const reducer = new Reducer<MyState, MyEM>((state, event) => {
396
+ * // implement your transitions here
397
+ * return state;
398
+ * });
399
+ * ```
400
+ *
401
+ * @public
402
+ */
403
+ constructor(e) {
404
+ /**
405
+ * The underlying pure reducer function.
406
+ * @internal
407
+ */
408
+ u(this, "_reduce");
409
+ this._reduce = e;
410
+ }
411
+ /**
412
+ * Applies the reducer to produce the next state.
413
+ *
414
+ * @param state - Current state.
415
+ * @param event - An event drawn from {@link EventUnion | `EventUnion<EM>`}.
416
+ * @returns The next state produced by the underlying reducer function.
417
+ *
418
+ * @example
419
+ * ```ts
420
+ * const next = reducer.reduce(curr, someEvent as EventUnion<MyEM>);
421
+ * ```
422
+ *
423
+ * @public
424
+ */
425
+ reduce(e, t) {
426
+ return this._reduce(e, t);
427
+ }
428
+ }
429
+ function b(a, e, t = "", s = /* @__PURE__ */ new WeakMap()) {
430
+ if (a === e) return [];
431
+ if (typeof a != "object" || typeof e != "object" || a === null || e === null)
432
+ return [t];
433
+ if (a instanceof Date && e instanceof Date)
434
+ return a.getTime() === e.getTime() ? [] : [t];
435
+ if (a instanceof RegExp && e instanceof RegExp)
436
+ return a.source === e.source && e.flags === a.flags ? [] : [t];
437
+ const n = a, r = e;
438
+ let o = s.get(n);
439
+ if (o) {
440
+ if (o.has(r)) return [];
441
+ o.add(r);
442
+ } else
443
+ o = /* @__PURE__ */ new WeakSet(), o.add(r), s.set(n, o);
444
+ const i = Array.isArray(a), c = Array.isArray(e);
445
+ if (i !== c) return [t];
446
+ const f = [];
447
+ if (i) {
448
+ const d = a, m = e;
449
+ if (d.length !== m.length) return [t];
450
+ for (let y = 0; y < d.length; y++) {
451
+ const g = t ? `${t}.${y}` : `${y}`, E = b(d[y], m[y], g, s);
452
+ f.push(...E);
453
+ }
454
+ return f.filter(Boolean);
455
+ }
456
+ const h = Object.keys(a), l = Object.keys(e), p = /* @__PURE__ */ new Set([...h, ...l]);
457
+ for (const d of p) {
458
+ const m = t ? `${t}.${d}` : d, y = Object.prototype.hasOwnProperty.call(a, d), g = Object.prototype.hasOwnProperty.call(e, d);
459
+ if (!y) {
460
+ f.push(m);
461
+ continue;
462
+ }
463
+ if (!g) {
464
+ f.push(m);
465
+ continue;
466
+ }
467
+ const E = b(a[d], e[d], m, s);
468
+ f.push(...E);
469
+ }
470
+ return f.filter(Boolean);
471
+ }
472
+ function w(a, e = /* @__PURE__ */ new WeakSet()) {
473
+ if (a === null || typeof a != "object" || e.has(a) || Object.isFrozen(a)) return a;
474
+ if (e.add(a), Array.isArray(a)) {
475
+ const t = a;
476
+ for (let s = 0; s < t.length; s++)
477
+ t[s] = w(t[s], e);
478
+ return Object.freeze(t);
479
+ }
480
+ for (const t of Object.getOwnPropertyNames(a)) {
481
+ const s = Object.getOwnPropertyDescriptor(a, t);
482
+ !s || !("value" in s) || (a[t] = w(a[t], e));
483
+ }
484
+ for (const t of Object.getOwnPropertySymbols(a)) {
485
+ const s = Object.getOwnPropertyDescriptor(a, t);
486
+ !s || !("value" in s) || (a[t] = w(a[t], e));
487
+ }
488
+ return Object.freeze(a);
489
+ }
490
+ class v {
491
+ /**
492
+ * Creates a store from a {@link StoreSpec}.
493
+ *
494
+ * @param spec - Store configuration (name, reducers, middleware, optional effects).
495
+ *
496
+ * @public
497
+ */
498
+ constructor(e) {
499
+ /**
500
+ * Store name (used by DevTools & diagnostics).
501
+ *
502
+ * @public
503
+ */
504
+ u(this, "name");
505
+ /**
506
+ * Registered middleware pipeline (run **before** reducers).
507
+ * Stores either raw functions (legacy) or MiddlewareSpec objects.
508
+ * Return `false` from the middleware function to stop propagation.
509
+ *
510
+ * @internal
511
+ */
512
+ u(this, "middleware");
513
+ /**
514
+ * Installed slice reducers keyed by slice name.
515
+ *
516
+ * @internal
517
+ */
518
+ u(this, "reducers");
519
+ /**
520
+ * Current immutable snapshot of the store state.
521
+ * This reference changes whenever any slice changes (shallow immutability).
522
+ *
523
+ * @internal
524
+ */
525
+ u(this, "state");
526
+ /**
527
+ * Bus for reducer wiring (emit by `(channel, type)`).
528
+ *
529
+ * @internal
530
+ */
531
+ u(this, "reducerBus");
532
+ /**
533
+ * Bus for **granular** connector events (emit by **dotted path** inside a slice).
534
+ *
535
+ * @internal
536
+ */
537
+ u(this, "connectorBus");
538
+ /**
539
+ * Coarse-grained listeners (called once per committed event, only if state changed).
540
+ *
541
+ * @internal
542
+ */
543
+ u(this, "listeners", /* @__PURE__ */ new Set());
544
+ /**
545
+ * Registered effect handlers keyed by `"channel::type"` for O(1) lookup.
546
+ * Used for effects with explicit `keys` or legacy `events` targeting.
547
+ *
548
+ * @internal
549
+ */
550
+ u(this, "effects", /* @__PURE__ */ new Map());
551
+ /**
552
+ * Pattern-based effects that need runtime matching.
553
+ * Used for effects with `when: { any }`, `{ channel }`, or `{ channels }`.
554
+ * Stores tuples of [effect function, when matcher].
555
+ *
556
+ * @internal
557
+ */
558
+ u(this, "patternEffects", /* @__PURE__ */ new Set());
559
+ /**
560
+ * Committed event subscribers keyed by `"channel::type"` for O(1) lookup.
561
+ * Notified after reducers, before effects, for events that passed middleware.
562
+ *
563
+ * @internal
564
+ */
565
+ u(this, "committedEventSubscribers", /* @__PURE__ */ new Map());
566
+ /**
567
+ * Uncommitted event subscribers keyed by `"channel::type"` for O(1) lookup.
568
+ * Notified when middleware rejects an event.
569
+ *
570
+ * @internal
571
+ */
572
+ u(this, "uncommittedEventSubscribers", /* @__PURE__ */ new Map());
573
+ /**
574
+ * All-events subscribers keyed by `"channel::type"` for O(1) lookup.
575
+ * Notified for both committed and uncommitted events with phase parameter.
576
+ *
577
+ * @internal
578
+ */
579
+ u(this, "allEventSubscribers", /* @__PURE__ */ new Map());
580
+ /**
581
+ * Track reducerBus unsubs per slice for HMR/register/unregister.
582
+ *
583
+ * @internal
584
+ */
585
+ u(this, "sliceUnsubs", /* @__PURE__ */ new Map());
586
+ /**
587
+ * Pattern-based reducers that need runtime matching.
588
+ * Used for reducers with `when: { any }`, `{ channel }`, or `{ channels }`.
589
+ * Maps slice name to the `when` matcher.
590
+ *
591
+ * @internal
592
+ */
593
+ u(this, "patternReducers", /* @__PURE__ */ new Map());
594
+ /**
595
+ * Whether `__replayEvents()` is allowed.
596
+ * Set from `spec.devtools.allowReplay`.
597
+ *
598
+ * @internal
599
+ */
600
+ u(this, "replayEnabled");
601
+ /**
602
+ * FIFO event queue for serialized emission.
603
+ *
604
+ * @internal
605
+ */
606
+ u(this, "eventQueue", []);
607
+ /**
608
+ * Re-entrancy guard while draining the queue.
609
+ *
610
+ * @internal
611
+ */
612
+ u(this, "isProcessingQueue", !1);
613
+ /**
614
+ * Tracks processed events by fingerprint with timestamps for TTL-based deduplication.
615
+ *
616
+ * **Deduplication Behavior:**
617
+ * - Events are fingerprinted using `channel::type::JSON(payload)`
618
+ * - If an identical fingerprint is seen within the dedup window, it's skipped
619
+ * - The window is 50ms in development, 100ms in production
620
+ *
621
+ * **Limitations:**
622
+ * - Non-serializable payloads (functions, symbols, circular refs) get unique
623
+ * fingerprints and won't be deduplicated
624
+ * - Legitimate rapid-fire identical events may be incorrectly deduplicated
625
+ * - The cache is bounded to 1000 entries with lazy pruning
626
+ *
627
+ * @internal
628
+ */
629
+ u(this, "processedEvents", /* @__PURE__ */ new Map());
630
+ /**
631
+ * Configuration for event deduplication.
632
+ * @internal
633
+ */
634
+ u(this, "dedupConfig");
635
+ /**
636
+ * Timer for periodic cleanup of processed events.
637
+ *
638
+ * @internal
639
+ */
640
+ u(this, "eventCleanupTimer", null);
641
+ this.name = e.name ?? "yoltra Store", this.reducerBus = new z(), this.connectorBus = new k(), this.middleware = [...e.middleware ?? []], this.reducers = {}, this.state = {}, this.replayEnabled = e.devtools?.allowReplay ?? !1;
642
+ const t = process.env.NODE_ENV === "production" ? 100 : 50;
643
+ if (this.dedupConfig = {
644
+ windowMs: e.dedupWindowMs ?? t,
645
+ maxCacheSize: 1e3
646
+ }, Object.entries(e.reducer).forEach(([s, n]) => {
647
+ this.mountSlice(s, n, { preserveState: !1 });
648
+ }), e.effects?.length)
649
+ for (const s of e.effects)
650
+ this.registerEffect(s);
651
+ this.eventCleanupTimer = setInterval(() => {
652
+ this.pruneProcessedEvents(Date.now());
653
+ }, 5e3), this.dispose = this.dispose.bind(this), this.notifyEffects = this.notifyEffects.bind(this), this.forwardEvent = this.forwardEvent.bind(this), this.__applyExternalState = this.__applyExternalState.bind(this), this.__replayEvents = this.__replayEvents.bind(this), this.__devtoolsIntrospect = this.__devtoolsIntrospect.bind(this), this.mountSlice = this.mountSlice.bind(this), this.unmountSlice = this.unmountSlice.bind(this), this.getAtPath = this.getAtPath.bind(this), this.emit = this.emit.bind(this), this.subscribe = this.subscribe.bind(this), this.connect = this.connect.bind(this), this.onEffect = this.onEffect.bind(this), this.onEvent = this.onEvent.bind(this), this.getState = this.getState.bind(this), this.registerEffect = this.registerEffect.bind(this), this.registerMiddleware = this.registerMiddleware.bind(this), this.registerReducer = this.registerReducer.bind(this), this.replaceMiddleware = this.replaceMiddleware.bind(this), this.replaceEffects = this.replaceEffects.bind(this), this.replaceReducers = this.replaceReducers.bind(this), this.hotReplace = this.hotReplace.bind(this);
654
+ }
655
+ /**
656
+ * Cleanup resources (timers, etc.) when disposing the store.
657
+ * Call this if you're dynamically creating/destroying stores.
658
+ *
659
+ * @example
660
+ * ```ts
661
+ * const store = createStore({ ... });
662
+ * // later
663
+ * store.dispose();
664
+ * ```
665
+ *
666
+ * @public
667
+ */
668
+ dispose() {
669
+ this.eventCleanupTimer && (clearInterval(this.eventCleanupTimer), this.eventCleanupTimer = null), this.processedEvents.clear(), this.effects.clear(), this.patternEffects.clear();
670
+ }
671
+ /**
672
+ * Generates a fingerprint for an event for deduplication purposes.
673
+ * Falls back gracefully for non-serializable payloads.
674
+ *
675
+ * @param channel - Event channel.
676
+ * @param type - Event type.
677
+ * @param payload - Event payload.
678
+ * @returns A string fingerprint for the event.
679
+ *
680
+ * @internal
681
+ */
682
+ fingerprint(e, t, s) {
683
+ const n = `${e}::${t}`;
684
+ try {
685
+ if (s == null)
686
+ return `${n}::null`;
687
+ if (typeof s != "object")
688
+ return `${n}::${String(s)}`;
689
+ const r = JSON.stringify(s);
690
+ return `${n}::${r}`;
691
+ } catch {
692
+ return `${n}::${Date.now()}::${Math.random()}`;
693
+ }
694
+ }
695
+ /**
696
+ * Checks if an event should be deduplicated.
697
+ * Returns true if this is a duplicate that should be skipped.
698
+ *
699
+ * @param fp - Event fingerprint.
700
+ * @returns `true` if duplicate (should skip), `false` otherwise.
701
+ *
702
+ * @internal
703
+ */
704
+ shouldDedupe(e) {
705
+ const t = Date.now(), s = this.processedEvents.get(e);
706
+ return s !== void 0 && t - s < this.dedupConfig.windowMs ? !0 : (this.processedEvents.set(e, t), this.processedEvents.size > this.dedupConfig.maxCacheSize && this.pruneProcessedEvents(t), !1);
707
+ }
708
+ /**
709
+ * Removes expired entries from the processed events cache.
710
+ *
711
+ * @param now - Current timestamp.
712
+ *
713
+ * @internal
714
+ */
715
+ pruneProcessedEvents(e) {
716
+ const t = e - this.dedupConfig.windowMs * 2;
717
+ for (const [s, n] of this.processedEvents)
718
+ n < t && this.processedEvents.delete(s);
719
+ }
720
+ /**
721
+ * Checks if an event matches a `When` matcher.
722
+ *
723
+ * @param when - The When matcher (or undefined for "all events").
724
+ * @param event - The event to check.
725
+ * @returns `true` if the event matches, `false` otherwise.
726
+ *
727
+ * @remarks
728
+ * - `undefined` or missing `when` matches ALL events.
729
+ * - `{ any: true }` matches ALL events.
730
+ * - `{ keys: [...] }` matches if event's `[channel, type]` is in the array.
731
+ * - `{ channel: 'x' }` matches if event's channel equals 'x'.
732
+ * - `{ channels: ['x', 'y'] }` matches if event's channel is in the array.
733
+ *
734
+ * @internal
735
+ */
736
+ matchesWhen(e, t) {
737
+ return !e || "any" in e && e.any === !0 ? !0 : "keys" in e ? e.keys.some(
738
+ ([s, n]) => t.channel === s && t.type === n
739
+ ) : "channel" in e ? t.channel === e.channel : "channels" in e ? e.channels.includes(t.channel) : !1;
740
+ }
741
+ /**
742
+ * Extracts the middleware function from a MiddlewareInput.
743
+ * Handles both raw functions (legacy) and MiddlewareSpec objects.
744
+ *
745
+ * @param input - MiddlewareInput (function or spec).
746
+ * @returns The middleware function.
747
+ *
748
+ * @internal
749
+ */
750
+ getMiddlewareFunction(e) {
751
+ return typeof e == "function" ? e : e.middleware;
752
+ }
753
+ /**
754
+ * Gets the `when` matcher from a MiddlewareInput.
755
+ *
756
+ * @param input - MiddlewareInput (function or spec).
757
+ * @returns The `when` matcher, or `undefined` for raw functions (match all).
758
+ *
759
+ * @internal
760
+ */
761
+ getMiddlewareWhen(e) {
762
+ if (typeof e != "function")
763
+ return e.when;
764
+ }
765
+ /**
766
+ * Invokes all registered **effects** for a given event.
767
+ * Handles both key-based effects (O(1) lookup) and pattern-based effects (runtime matching).
768
+ * Errors are caught and logged.
769
+ *
770
+ * @param event - The event that was reduced.
771
+ * @internal
772
+ */
773
+ async notifyEffects(e) {
774
+ const t = `${String(e.channel)}::${String(e.type)}`, s = this.effects.get(t);
775
+ if (s && s.size > 0)
776
+ for (const n of [...s])
777
+ try {
778
+ await n(e, this.getState, this.emit);
779
+ } catch (r) {
780
+ console.error("Effect error:", r);
781
+ }
782
+ for (const { effect: n, when: r } of this.patternEffects)
783
+ if (this.matchesWhen(r, e))
784
+ try {
785
+ await n(e, this.getState, this.emit);
786
+ } catch (o) {
787
+ console.error("Effect error:", o);
788
+ }
789
+ }
790
+ /**
791
+ * Notifies event subscribers for a specific phase.
792
+ *
793
+ * Calls both phase-specific subscribers and 'all' subscribers.
794
+ * Errors are caught and logged, allowing other subscribers to continue.
795
+ *
796
+ * @param event - The event to notify about.
797
+ * @param phase - The phase ('committed' or 'uncommitted').
798
+ * @internal
799
+ */
800
+ async notifyEventSubscribers(e, t) {
801
+ const s = `${String(e.channel)}::${String(e.type)}`, r = (t === "committed" ? this.committedEventSubscribers : this.uncommittedEventSubscribers).get(s);
802
+ if (r?.size)
803
+ for (const i of [...r])
804
+ try {
805
+ await i(e, this.getState, this.emit, t);
806
+ } catch (c) {
807
+ console.error("Event subscription error:", c);
808
+ }
809
+ const o = this.allEventSubscribers.get(s);
810
+ if (o?.size)
811
+ for (const i of [...o])
812
+ try {
813
+ await i(e, this.getState, this.emit, t);
814
+ } catch (c) {
815
+ console.error("Event subscription error:", c);
816
+ }
817
+ }
818
+ /**
819
+ * Applies a reduced event to a slice and emits **precise** connector events.
820
+ *
821
+ * For each changed **leaf path** (via {@link detectChangedProps}), emits that leaf and
822
+ * all of its **ancestors** once (e.g., `"data"`, `"data.123"`, `"data.123.title"`).
823
+ *
824
+ * **State Immutability**: When a slice changes, a new state object is created via
825
+ * shallow spread: `{ ...this.state, [sliceName]: newSlice }`. This ensures that
826
+ * `this.state` reference changes, enabling efficient change detection via `===`.
827
+ *
828
+ * @param rName - Slice name being updated.
829
+ * @param event - Reduced event with typed payload.
830
+ * @returns `true` if the slice actually changed, `false` otherwise.
831
+ *
832
+ * @internal
833
+ */
834
+ forwardEvent(e, t) {
835
+ const s = this.state[e], n = this.reducers[e].reduce(s, t);
836
+ if (s === n) return !1;
837
+ const r = b(s, n).filter(Boolean);
838
+ if (r.length === 0) return !1;
839
+ const o = w(structuredClone(n));
840
+ this.state = { ...this.state, [e]: o };
841
+ const i = /* @__PURE__ */ new Set();
842
+ for (const c of r)
843
+ for (const f of v.buildAncestorPaths(c)) i.add(f);
844
+ for (const c of i) {
845
+ const f = this.getAtPath(s, c), h = this.getAtPath(o, c);
846
+ this.connectorBus.emit(e, c, { oldValue: f, newValue: h, path: c });
847
+ }
848
+ return !0;
849
+ }
850
+ /**
851
+ * Returns a structured introspection snapshot for DevTools UIs.
852
+ *
853
+ * @remarks
854
+ * Reads the internal middleware, effects, reducers, and subscriber
855
+ * registries and returns a plain-object summary matching the
856
+ * `STORE_SUBSCRIPTIONS` protocol message shape.
857
+ *
858
+ * @public
859
+ */
860
+ __devtoolsIntrospect() {
861
+ const e = Object.keys(this.reducers).map((i) => {
862
+ const c = this.patternReducers.get(i);
863
+ return { name: i, when: c };
864
+ }), t = [];
865
+ for (const [i, c] of this.effects) {
866
+ if (c.size === 0) continue;
867
+ const [f, h] = i.split("::");
868
+ for (const l of c) {
869
+ const p = l.__quoMeta;
870
+ t.push({ channel: f, type: h, name: p?.name, description: p?.description });
871
+ }
872
+ }
873
+ for (const i of this.patternEffects) {
874
+ const c = i.effect.__quoMeta;
875
+ t.push({
876
+ channel: "*",
877
+ type: "*",
878
+ name: c?.name,
879
+ description: c?.description
880
+ });
881
+ }
882
+ const s = [];
883
+ for (const i of this.middleware)
884
+ typeof i == "function" ? s.push({ name: i.name || void 0 }) : s.push({
885
+ name: i.meta?.name,
886
+ description: i.meta?.description,
887
+ when: i.when
888
+ });
889
+ const n = [];
890
+ for (const i of this.connectorBus.__introspect())
891
+ for (let c = 0; c < i.count; c++)
892
+ n.push({ reducer: i.channel, property: i.type });
893
+ const r = [];
894
+ for (const [i, c] of this.committedEventSubscribers) {
895
+ if (c.size === 0) continue;
896
+ const [f, h] = i.split("::");
897
+ for (let l = 0; l < c.size; l++)
898
+ r.push({ channel: f, type: h, phase: "committed" });
899
+ }
900
+ for (const [i, c] of this.uncommittedEventSubscribers) {
901
+ if (c.size === 0) continue;
902
+ const [f, h] = i.split("::");
903
+ for (let l = 0; l < c.size; l++)
904
+ r.push({ channel: f, type: h, phase: "uncommitted" });
905
+ }
906
+ for (const [i, c] of this.allEventSubscribers) {
907
+ if (c.size === 0) continue;
908
+ const [f, h] = i.split("::");
909
+ for (let l = 0; l < c.size; l++)
910
+ r.push({ channel: f, type: h, phase: "all" });
911
+ }
912
+ const o = this.listeners.size;
913
+ return { reducers: e, effects: t, middleware: s, atomic: n, event: r, coarse: o };
914
+ }
915
+ /**
916
+ * Applies an externally provided **whole-state** (e.g., DevTools time travel) and emits
917
+ * fine-grained path changes for each slice.
918
+ *
919
+ * **State Immutability**: If any slices change, a new state object is created via
920
+ * shallow spread. This ensures consistent immutability with {@link forwardEvent}.
921
+ *
922
+ * @param nextPlain - Plain JS object to become the new state.
923
+ *
924
+ * @internal
925
+ */
926
+ __applyExternalState(e) {
927
+ const t = this.state, s = e;
928
+ let n = { ...this.state }, r = !1;
929
+ Object.keys(this.reducers).forEach((o) => {
930
+ const i = t?.[o], c = s?.[o];
931
+ if (i === c) return;
932
+ const f = w(structuredClone(c));
933
+ n[o] = f, r = !0;
934
+ const h = b(i, c).filter(Boolean);
935
+ if (h.length === 0) return;
936
+ const l = /* @__PURE__ */ new Set();
937
+ for (const p of h) for (const d of v.buildAncestorPaths(p)) l.add(d);
938
+ for (const p of l) {
939
+ const d = this.getAtPath(i, p), m = this.getAtPath(f, p);
940
+ this.connectorBus.emit(o, p, { oldValue: d, newValue: m, path: p });
941
+ }
942
+ }), r && (this.state = n), r && this.listeners.forEach((o) => o());
943
+ }
944
+ /**
945
+ * Replays a sequence of events from a snapshot through reducers and event
946
+ * subscribers ONLY. Skips dedup, middleware, and effects.
947
+ *
948
+ * This method is gated by the `devtools.allowReplay` runtime config.
949
+ * If replay is not enabled, this method throws.
950
+ *
951
+ * @param snapshot - The state snapshot to restore before replaying.
952
+ * @param events - Array of events to replay (in order).
953
+ *
954
+ * @internal
955
+ */
956
+ __replayEvents(e, t) {
957
+ if (!this.replayEnabled)
958
+ throw new Error(
959
+ "[yoltra] Event replay is disabled. Enable it with createStore({ devtools: { allowReplay: true } })"
960
+ );
961
+ this.__applyExternalState(e);
962
+ for (const s of t) {
963
+ const n = s, r = this.state;
964
+ this.reducerBus.emit(n.channel, n.type, n.payload);
965
+ for (const [c, f] of this.patternReducers)
966
+ this.matchesWhen(f, n) && this.forwardEvent(c, n);
967
+ const o = this.state, i = r !== o;
968
+ this.notifyEventSubscribers(n, "committed"), i && this.listeners.forEach((c) => c());
969
+ }
970
+ }
971
+ /**
972
+ * Emits a typed event `(channel, type, payload)`.
973
+ * Events are queued and processed **sequentially** (FIFO).
974
+ *
975
+ * **Pipeline per event:**
976
+ * 1. **Deduplication check** - Skip if event ID already processed (React Strict Mode safety)
977
+ * 2. **Middleware** - Pre-reducer hooks; may cancel by returning `false`
978
+ * 3. **Reducers** - Synchronous state updates via internal event bus
979
+ * 4. **Effects** - Async side-effects keyed by `(channel, type)` for O(1) lookup
980
+ * 5. **Coarse subscribers** - External store subscribers (only if state changed)
981
+ *
982
+ * **Change Detection**: Uses reference equality (`===`) on `this.state` to determine
983
+ * if any slice changed. Works because {@link forwardEvent} creates a new state reference
984
+ * via shallow spread when any slice changes.
985
+ *
986
+ * @typeParam C - Channel key in `EM`.
987
+ * @typeParam T - Type key within channel `C`.
988
+ * @param channel - Channel name.
989
+ * @param type - Event type name.
990
+ * @param payload - Payload typed as `EM[C][T]`.
991
+ * @returns A promise that resolves when the event has finished processing.
992
+ *
993
+ * @example Basic usage
994
+ * ```ts
995
+ * await store.emit('ui', 'increment', 1);
996
+ * ```
997
+ *
998
+ * @example With middleware cancellation
999
+ * ```ts
1000
+ * store.registerMiddleware((state, event) => {
1001
+ * if (event.type === 'dangerous') return false; // cancel
1002
+ * return true; // allow
1003
+ * });
1004
+ *
1005
+ * await store.emit('ui', 'dangerous', null); // cancelled, no state change
1006
+ * ```
1007
+ *
1008
+ * @public
1009
+ */
1010
+ async emit(e, t, s) {
1011
+ const n = this.fingerprint(e, t, s);
1012
+ if (this.shouldDedupe(n))
1013
+ return;
1014
+ const r = crypto.randomUUID();
1015
+ if (this.eventQueue.push({
1016
+ channel: e,
1017
+ type: t,
1018
+ payload: s,
1019
+ id: r
1020
+ }), !this.isProcessingQueue) {
1021
+ this.isProcessingQueue = !0;
1022
+ try {
1023
+ for (; this.eventQueue.length; ) {
1024
+ const { channel: o, type: i, payload: c, id: f } = this.eventQueue.shift(), h = { channel: o, type: i, payload: c, id: f };
1025
+ let l = !0;
1026
+ for (const y of this.middleware) {
1027
+ const g = this.getMiddlewareWhen(y);
1028
+ if (!this.matchesWhen(g, h))
1029
+ continue;
1030
+ const E = this.getMiddlewareFunction(y);
1031
+ try {
1032
+ if (!await E(this.state, h, this.emit)) {
1033
+ l = !1;
1034
+ break;
1035
+ }
1036
+ } catch (S) {
1037
+ console.error("Middleware error:", S), l = !1;
1038
+ break;
1039
+ }
1040
+ }
1041
+ if (!l) {
1042
+ await this.notifyEventSubscribers(h, "uncommitted");
1043
+ continue;
1044
+ }
1045
+ const p = this.state;
1046
+ this.reducerBus.emit(o, i, c);
1047
+ for (const [y, g] of this.patternReducers)
1048
+ this.matchesWhen(g, h) && this.forwardEvent(y, h);
1049
+ const d = this.state, m = p !== d;
1050
+ await this.notifyEventSubscribers(h, "committed"), await this.notifyEffects(h), m && this.listeners.forEach((y) => y());
1051
+ }
1052
+ } catch (o) {
1053
+ console.error("Emit queue error:", o);
1054
+ } finally {
1055
+ this.isProcessingQueue = !1;
1056
+ }
1057
+ }
1058
+ }
1059
+ /**
1060
+ * Connects a **fine-grained** listener to a dotted path under a slice.
1061
+ *
1062
+ * @param spec - `{ reducer, property }` where `property` is a dotted path (e.g., `"items.0.title"`).
1063
+ * Supports wildcards: `*` (one segment) and `**` (zero or more segments).
1064
+ * @param h - Handler receiving a {@link Change} with `{ oldValue, newValue, path }`.
1065
+ * @returns Unsubscribe function.
1066
+ *
1067
+ * @example Exact path
1068
+ * ```ts
1069
+ * const off = store.connect(
1070
+ * { reducer: 'todos', property: 'items.0.title' },
1071
+ * (chg) => console.log('title changed:', chg.newValue)
1072
+ * );
1073
+ * off();
1074
+ * ```
1075
+ *
1076
+ * @example Wildcard pattern
1077
+ * ```ts
1078
+ * // Listen to any item title change
1079
+ * const off = store.connect(
1080
+ * { reducer: 'todos', property: 'items.*.title' },
1081
+ * (chg) => console.log('some title changed')
1082
+ * );
1083
+ * ```
1084
+ *
1085
+ * @public
1086
+ */
1087
+ connect(e, t) {
1088
+ return this.connectorBus.on(e.reducer, e.property, t);
1089
+ }
1090
+ /**
1091
+ * Subscribe to events by channel and type.
1092
+ *
1093
+ * Event subscriptions are intended for the View layer (e.g., React components)
1094
+ * to react to events without affecting the event flow. They are fire-and-forget
1095
+ * and cannot cancel event propagation.
1096
+ *
1097
+ * **Phases:**
1098
+ * - `'committed'` (default): Events that passed middleware and reached reducers.
1099
+ * Notified after reducers, before effects.
1100
+ * - `'uncommitted'`: Events rejected by middleware. Notified immediately after rejection.
1101
+ * - `'all'`: Both committed and uncommitted events. Handler receives the phase parameter
1102
+ * to distinguish between the two.
1103
+ *
1104
+ * @typeParam C - Channel key within `EM`.
1105
+ * @typeParam T - Event type key within channel `C`.
1106
+ * @param channel - Channel to subscribe to.
1107
+ * @param type - Event type to subscribe to.
1108
+ * @param handler - Handler function `(event, getState, emit, phase)`.
1109
+ * @param phase - Event phase to subscribe to (default: `'committed'`).
1110
+ * @returns Unsubscribe function.
1111
+ *
1112
+ * @example Committed events (default)
1113
+ * ```ts
1114
+ * const off = store.onEvent('ui', 'save', (event, getState, emit, phase) => {
1115
+ * console.log('Save committed:', event.payload);
1116
+ * });
1117
+ * off();
1118
+ * ```
1119
+ *
1120
+ * @example Uncommitted (rejected) events
1121
+ * ```ts
1122
+ * store.onEvent('ui', 'delete', (event, getState, emit, phase) => {
1123
+ * console.log('Delete was rejected by middleware');
1124
+ * }, 'uncommitted');
1125
+ * ```
1126
+ *
1127
+ * @example All events
1128
+ * ```ts
1129
+ * store.onEvent('ui', 'action', (event, getState, emit, phase) => {
1130
+ * console.log('Action:', phase); // 'committed' or 'uncommitted'
1131
+ * }, 'all');
1132
+ * ```
1133
+ *
1134
+ * @public
1135
+ */
1136
+ onEvent(e, t, s, n = "committed") {
1137
+ const r = `${e}::${String(t)}`, o = n === "committed" ? this.committedEventSubscribers : n === "uncommitted" ? this.uncommittedEventSubscribers : this.allEventSubscribers;
1138
+ return o.has(r) || o.set(r, /* @__PURE__ */ new Set()), o.get(r).add(s), () => {
1139
+ const i = o.get(r);
1140
+ i && (i.delete(s), i.size === 0 && o.delete(r));
1141
+ };
1142
+ }
1143
+ /**
1144
+ * Subscribes to **coarse-grained** commits (called once per successful event, only if state changed).
1145
+ *
1146
+ * **Use Case**: React's `useSyncExternalStore` or similar external store integrations.
1147
+ *
1148
+ * @param fn - Listener invoked after reducers/effects have run and state has changed.
1149
+ * @returns Unsubscribe function.
1150
+ *
1151
+ * @example
1152
+ * ```ts
1153
+ * const off = store.subscribe(() => console.log('state committed'));
1154
+ * // Later:
1155
+ * off();
1156
+ * ```
1157
+ *
1158
+ * @public
1159
+ */
1160
+ subscribe(e) {
1161
+ return this.listeners.add(e), () => this.listeners.delete(e);
1162
+ }
1163
+ /**
1164
+ * Returns the current immutable state snapshot.
1165
+ *
1166
+ * @returns Deep-readonly state object.
1167
+ *
1168
+ * @example
1169
+ * ```ts
1170
+ * const state = store.getState();
1171
+ * console.log(state.counter.value);
1172
+ * ```
1173
+ *
1174
+ * @public
1175
+ */
1176
+ getState() {
1177
+ return this.state;
1178
+ }
1179
+ /**
1180
+ * Registers a middleware (runs **before** reducers).
1181
+ *
1182
+ * @param mw - Middleware `(state, event, emit) => boolean|Promise<boolean>`.
1183
+ * Return `false` to cancel event propagation.
1184
+ * @returns Unsubscribe function that removes this middleware.
1185
+ *
1186
+ * @example Logging middleware
1187
+ * ```ts
1188
+ * const off = store.registerMiddleware(async (state, event) => {
1189
+ * console.log('Event:', event.channel, event.type, event.payload);
1190
+ * return true; // allow
1191
+ * });
1192
+ * off();
1193
+ * ```
1194
+ *
1195
+ * @example Cancellation middleware
1196
+ * ```ts
1197
+ * store.registerMiddleware((state, event) => {
1198
+ * if (event.type === 'forbidden') return false; // cancel
1199
+ * return true;
1200
+ * });
1201
+ * ```
1202
+ *
1203
+ * @public
1204
+ */
1205
+ registerMiddleware(e) {
1206
+ return this.middleware.push(e), () => {
1207
+ const t = this.middleware.indexOf(e);
1208
+ t !== -1 && this.middleware.splice(t, 1);
1209
+ };
1210
+ }
1211
+ /**
1212
+ * Dynamically **adds** a named slice reducer at runtime.
1213
+ *
1214
+ * @param name - New slice name (must not already exist).
1215
+ * @param spec - Reducer spec (state, events, reducer).
1216
+ * @returns Disposer function that **removes** the slice (and its state).
1217
+ *
1218
+ * @example
1219
+ * ```ts
1220
+ * const dispose = store.registerReducer('filters', {
1221
+ * state: { q: '' },
1222
+ * events: [['ui', 'setQuery']],
1223
+ * reducer(s, evt) {
1224
+ * return evt.type === 'setQuery' ? { q: evt.payload } : s;
1225
+ * }
1226
+ * });
1227
+ * // Later:
1228
+ * dispose();
1229
+ * ```
1230
+ *
1231
+ * @public
1232
+ */
1233
+ registerReducer(e, t) {
1234
+ if (e in this.reducers) throw new Error(`Reducer ${e} already exists`);
1235
+ return this.mountSlice(e, t, {
1236
+ preserveState: !1
1237
+ }), this.listeners.forEach((s) => s()), () => {
1238
+ this.unmountSlice(e, { deleteState: !0 }), this.listeners.forEach((s) => s());
1239
+ };
1240
+ }
1241
+ /**
1242
+ * Registers an **effect** (stateless async event consumer) that runs after reducers.
1243
+ *
1244
+ * Effects are **keyed** by `(channel, type)` for O(1) lookup (no scanning all effects).
1245
+ *
1246
+ * @param spec - Effect specification with `events` (EventKeys) and `effect` (handler).
1247
+ * @returns Unsubscribe function.
1248
+ *
1249
+ * @example Logging effect
1250
+ * ```ts
1251
+ * const off = store.registerEffect({
1252
+ * events: [['ui', 'increment']],
1253
+ * effect: async (evt, getState, emit) => {
1254
+ * console.log('increment', evt.payload, getState().counter.value);
1255
+ * }
1256
+ * });
1257
+ * off();
1258
+ * ```
1259
+ *
1260
+ * @example Multi-event effect
1261
+ * ```ts
1262
+ * store.registerEffect({
1263
+ * events: [['ui', 'increment'], ['ui', 'decrement']],
1264
+ * effect: async (evt, getState, emit) => {
1265
+ * // Runs for both increment and decrement
1266
+ * await saveToServer(getState());
1267
+ * }
1268
+ * });
1269
+ * ```
1270
+ *
1271
+ * @public
1272
+ */
1273
+ registerEffect(e) {
1274
+ const { effect: t, meta: s, when: n } = e, r = [];
1275
+ if (s && (t.__quoMeta = s), n && ("any" in n && n.any === !0 || "channel" in n || "channels" in n)) {
1276
+ const c = { effect: t, when: n };
1277
+ return this.patternEffects.add(c), () => {
1278
+ this.patternEffects.delete(c);
1279
+ };
1280
+ }
1281
+ const i = this.normalizeEventKeys(e);
1282
+ if (i.length === 0 && !n && !e.events) {
1283
+ const c = { effect: t, when: { any: !0 } };
1284
+ return this.patternEffects.add(c), () => {
1285
+ this.patternEffects.delete(c);
1286
+ };
1287
+ }
1288
+ for (const [c, f] of i) {
1289
+ const h = `${String(c)}::${String(f)}`;
1290
+ this.effects.has(h) || this.effects.set(h, /* @__PURE__ */ new Set()), this.effects.get(h).add(t), r.push(() => {
1291
+ const l = this.effects.get(h);
1292
+ l && (l.delete(t), l.size === 0 && this.effects.delete(h));
1293
+ });
1294
+ }
1295
+ return () => {
1296
+ for (const c of r) c();
1297
+ };
1298
+ }
1299
+ /**
1300
+ * Convenience helper to register an **effect** filtered by a single `(channel, type)` pair.
1301
+ *
1302
+ * @typeParam C - Channel key within `EM`.
1303
+ * @typeParam T - Event type key within channel `C`.
1304
+ * @param channel - Channel to filter.
1305
+ * @param type - Event type to filter.
1306
+ * @param handler - Effect handler `(payload, getState, emit, event)`.
1307
+ * @returns Unsubscribe/teardown function.
1308
+ *
1309
+ * @example
1310
+ * ```ts
1311
+ * const off = store.onEffect('ui', 'increment', async (n, get, emit) => {
1312
+ * if (n > 10) await emit('ui', 'increment', -10);
1313
+ * });
1314
+ * // later
1315
+ * off();
1316
+ * ```
1317
+ *
1318
+ * @public
1319
+ */
1320
+ onEffect(e, t, s) {
1321
+ const n = async (r, o, i) => {
1322
+ if (r.channel !== e || r.type !== t) return;
1323
+ const c = r;
1324
+ return s(c.payload, o, i, c);
1325
+ };
1326
+ return this.registerEffect({
1327
+ events: [[e, t]],
1328
+ effect: n
1329
+ });
1330
+ }
1331
+ /**
1332
+ * Replaces the **entire** middleware pipeline (HMR-friendly).
1333
+ *
1334
+ * @param next - New middleware array.
1335
+ *
1336
+ * @example Hot module replacement
1337
+ * ```ts
1338
+ * if (import.meta.hot) {
1339
+ * import.meta.hot.accept('./middleware', (newModule) => {
1340
+ * store.replaceMiddleware(newModule.middleware);
1341
+ * });
1342
+ * }
1343
+ * ```
1344
+ *
1345
+ * @public
1346
+ */
1347
+ replaceMiddleware(e) {
1348
+ this.middleware.length = 0;
1349
+ for (const t of e) this.middleware.push(t);
1350
+ }
1351
+ /**
1352
+ * Replaces all registered **effects** (HMR-friendly).
1353
+ *
1354
+ * @param next - New effects array (as EffectSpecs).
1355
+ *
1356
+ * @example Hot module replacement
1357
+ * ```ts
1358
+ * if (import.meta.hot) {
1359
+ * import.meta.hot.accept('./effects', (newModule) => {
1360
+ * store.replaceEffects(newModule.effects);
1361
+ * });
1362
+ * }
1363
+ * ```
1364
+ *
1365
+ * @public
1366
+ */
1367
+ replaceEffects(e) {
1368
+ this.effects.clear(), this.patternEffects.clear();
1369
+ for (const t of e)
1370
+ this.registerEffect(t);
1371
+ }
1372
+ /**
1373
+ * Replaces the entire **reducer set** (HMR-friendly).
1374
+ *
1375
+ * @param next - Map of slice specs keyed by slice name.
1376
+ * @param opts - `{ preserveState?: boolean }` (default `true`).
1377
+ *
1378
+ * @example Hot module replacement
1379
+ * ```ts
1380
+ * if (import.meta.hot) {
1381
+ * import.meta.hot.accept('./reducers', (newModule) => {
1382
+ * store.replaceReducers(newModule.reducers, { preserveState: true });
1383
+ * });
1384
+ * }
1385
+ * ```
1386
+ *
1387
+ * @public
1388
+ */
1389
+ replaceReducers(e, t = {}) {
1390
+ const s = t.preserveState !== !1, n = new Set(Object.keys(this.reducers)), r = Object.entries(e), o = new Set(r.map(([i]) => i));
1391
+ for (const i of n)
1392
+ o.has(i) || this.unmountSlice(i, { deleteState: !0 });
1393
+ for (const [i, c] of r)
1394
+ n.has(i) ? (this.unmountSlice(i, { deleteState: !1 }), this.mountSlice(i, c, { preserveState: s })) : this.mountSlice(i, c, { preserveState: !1 });
1395
+ }
1396
+ /**
1397
+ * Convenience API to replace **any subset** of store parts (HMR patterns).
1398
+ *
1399
+ * @param partial - Partial replacement set.
1400
+ *
1401
+ * @example Replace everything
1402
+ * ```ts
1403
+ * store.hotReplace({
1404
+ * reducer: newReducers,
1405
+ * middleware: newMiddleware,
1406
+ * effects: newEffects,
1407
+ * preserveState: true
1408
+ * });
1409
+ * ```
1410
+ *
1411
+ * @public
1412
+ */
1413
+ hotReplace(e) {
1414
+ e.middleware && this.replaceMiddleware(e.middleware), e.effects && this.replaceEffects(e.effects), e.reducer && this.replaceReducers(e.reducer, { preserveState: e.preserveState });
1415
+ }
1416
+ /**
1417
+ * Mounts a slice: installs reducer, initializes state (unless preserved),
1418
+ * and wires `(channel, type)` listeners on the reducer bus.
1419
+ *
1420
+ * @param name - Slice name.
1421
+ * @param rSpec - Reducer spec (state, events, reducer).
1422
+ * @param opts - `{ preserveState: boolean }` whether to keep existing state.
1423
+ *
1424
+ * @internal
1425
+ */
1426
+ mountSlice(e, t, s) {
1427
+ const n = e, { events: r, reducer: o, state: i, when: c } = t;
1428
+ if (this.reducers[e] = new _(o), (!s.preserveState || this.state[n] === void 0) && (this.state[n] = w(structuredClone(i))), c && ("any" in c && c.any === !0 || "channel" in c || "channels" in c)) {
1429
+ this.patternReducers.set(e, c), this.sliceUnsubs.set(n, []);
1430
+ return;
1431
+ }
1432
+ const h = this.normalizeEventKeys(t);
1433
+ if (h.length === 0 && !c && !r) {
1434
+ this.patternReducers.set(e, { any: !0 }), this.sliceUnsubs.set(n, []);
1435
+ return;
1436
+ }
1437
+ const l = [];
1438
+ for (const [p, d] of h) {
1439
+ const m = this.reducerBus.on(p, d, (y) => {
1440
+ const g = { channel: p, type: d, payload: y, id: crypto.randomUUID() };
1441
+ this.forwardEvent(e, g);
1442
+ });
1443
+ l.push(m);
1444
+ }
1445
+ this.sliceUnsubs.set(n, l);
1446
+ }
1447
+ /**
1448
+ * Unmounts a slice: disposes reducer-bus listeners, removes reducer,
1449
+ * and optionally deletes the slice state.
1450
+ *
1451
+ * @param name - Slice name.
1452
+ * @param opts - `{ deleteState: boolean }`.
1453
+ *
1454
+ * @internal
1455
+ */
1456
+ unmountSlice(e, t) {
1457
+ const s = e;
1458
+ this.patternReducers.delete(e);
1459
+ const n = this.sliceUnsubs.get(s);
1460
+ if (n) {
1461
+ for (const r of n)
1462
+ try {
1463
+ r();
1464
+ } catch (o) {
1465
+ console.error(`[Store error]: ${o}`);
1466
+ }
1467
+ this.sliceUnsubs.delete(s);
1468
+ }
1469
+ delete this.reducers[e], t.deleteState && delete this.state[s];
1470
+ }
1471
+ /**
1472
+ * Normalizes event targeting from `when` or legacy `events` to an array of EventKeys.
1473
+ *
1474
+ * @param spec - Object with optional `when` and/or `events` properties.
1475
+ * @returns Array of `[channel, type]` pairs.
1476
+ *
1477
+ * @internal
1478
+ */
1479
+ normalizeEventKeys(e) {
1480
+ if (e.when) {
1481
+ const t = e.when;
1482
+ if ("any" in t && t.any === !0)
1483
+ return [];
1484
+ if ("keys" in t)
1485
+ return t.keys;
1486
+ if ("channel" in t)
1487
+ return [];
1488
+ if ("channels" in t)
1489
+ return [];
1490
+ }
1491
+ return e.events ? e.events : [];
1492
+ }
1493
+ /**
1494
+ * Reads a dotted path from an object (supports numeric array indices via string keys).
1495
+ *
1496
+ * @param obj - Root object (slice or value).
1497
+ * @param path - Dotted path; leading dot is ignored.
1498
+ * @returns The value at the path, or `undefined`.
1499
+ *
1500
+ * @internal
1501
+ */
1502
+ getAtPath(e, t) {
1503
+ if (!t) return e;
1504
+ const n = (t[0] === "." ? t.slice(1) : t).split(".");
1505
+ let r = e;
1506
+ for (const o of n) {
1507
+ if (r == null) return;
1508
+ r = r[o];
1509
+ }
1510
+ return r;
1511
+ }
1512
+ /**
1513
+ * Builds ancestor paths for a dotted path.
1514
+ *
1515
+ * For `"a.b.c"`, returns `["a", "a.b", "a.b.c"]`. Leading dots are trimmed.
1516
+ *
1517
+ * @param path - Dotted path string.
1518
+ * @returns Array of ancestor paths.
1519
+ *
1520
+ * @example
1521
+ * ```ts
1522
+ * Store.buildAncestorPaths('x.y.z'); // ['x','x.y','x.y.z']
1523
+ * ```
1524
+ *
1525
+ * @public
1526
+ */
1527
+ static buildAncestorPaths(e) {
1528
+ if (!e) return [];
1529
+ const s = (e[0] === "." ? e.slice(1) : e).split("."), n = [];
1530
+ for (let r = 0; r < s.length; r++)
1531
+ n.push(s.slice(0, r + 1).join("."));
1532
+ return n;
1533
+ }
1534
+ }
1535
+ function x(a) {
1536
+ return new v({
1537
+ name: a.name,
1538
+ reducer: a.reducer ?? {},
1539
+ middleware: a.middleware ?? [],
1540
+ effects: a.effects ?? [],
1541
+ dedupWindowMs: a.dedupWindowMs,
1542
+ devtools: a.devtools
1543
+ });
1544
+ }
1545
+ const B = (a) => (e, t) => t.map((s) => [e, s]), $ = () => (a) => a;
1546
+ export {
1547
+ z as EventBus,
1548
+ k as LooseEventBus,
1549
+ _ as Reducer,
1550
+ v as Store,
1551
+ x as createStore,
1552
+ b as detectChangedProps,
1553
+ $ as eventKeys,
1554
+ w as freezeState,
1555
+ B as typedEvents
1556
+ };
1557
+ //# sourceMappingURL=yoltra.esm.js.map