@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,939 @@
1
+ /**
2
+ * @module @yoltra/core
3
+ */
4
+ /**
5
+ * A minimal "record of record" constraint for EventMaps.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * type EM = {
10
+ * ui: { toggle: boolean; setTheme: string };
11
+ * data: { loaded: { items: string[] } };
12
+ * };
13
+ * ```
14
+ *
15
+ * @public
16
+ */
17
+ export type EventMapBase = {
18
+ [C in string]: {
19
+ [T in string]: unknown;
20
+ };
21
+ };
22
+ /**
23
+ * Canonical routing concept: a readonly tuple `[channel, type]` that uniquely identifies an event.
24
+ *
25
+ * @typeParam EM - Event map.
26
+ *
27
+ * @remarks
28
+ * - Used consistently across ReducerSpec, EffectSpec, and React hooks.
29
+ * - Literal key lists narrow channel/type/payload in reducers and effects.
30
+ * - Non-literal usage degrades safely to unions.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * type EM = {
35
+ * ui: { increment: number; decrement: number };
36
+ * data: { loaded: string[] };
37
+ * };
38
+ *
39
+ * type K = EventKey<EM>;
40
+ * // K = ['ui', 'increment'] | ['ui', 'decrement'] | ['data', 'loaded']
41
+ *
42
+ * const key: EventKey<EM> = ['ui', 'increment'];
43
+ * ```
44
+ *
45
+ * @public
46
+ */
47
+ export type EventKey<EM extends EventMapBase> = {
48
+ [C in keyof EM & string]: [C, keyof EM[C] & string];
49
+ }[keyof EM & string];
50
+ /**
51
+ * A single event object: `{ channel, type, payload, id }`.
52
+ *
53
+ * @typeParam EM - Event map.
54
+ * @typeParam C - Channel key.
55
+ * @typeParam T - Type key within channel `C`.
56
+ * @typeParam P - Payload type (defaults to `EM[C][T]`).
57
+ *
58
+ * @remarks
59
+ * - The `id` field is automatically added by the store to enable deduplication.
60
+ * - Used for preventing duplicate event processing (e.g., React Strict Mode).
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * type EM = { ui: { toggle: boolean } };
65
+ * type Evt = Event<EM, 'ui', 'toggle'>;
66
+ * // { channel: 'ui'; type: 'toggle'; payload: boolean; id: string }
67
+ * ```
68
+ *
69
+ * @public
70
+ */
71
+ export interface Event<EM extends EventMapBase = EventMapBase, C extends keyof EM & string = keyof EM & string, T extends keyof EM[C] & string = keyof EM[C] & string, P = EM[C][T]> {
72
+ channel: C;
73
+ type: T;
74
+ payload: P;
75
+ /** Unique identifier for deduplication and devtools tracking (automatically added by store) */
76
+ id: string;
77
+ }
78
+ /**
79
+ * Generic "old → new" wrapper for fine-grained change notifications.
80
+ * Carries the dotted `path` that changed.
81
+ *
82
+ * @typeParam V - Value type at the changed path.
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * const change: Change<string> = {
87
+ * oldValue: 'foo',
88
+ * newValue: 'bar',
89
+ * path: 'user.name'
90
+ * };
91
+ * ```
92
+ *
93
+ * @public
94
+ */
95
+ export interface Change<V = any> {
96
+ oldValue: V;
97
+ newValue: V;
98
+ /** Dotted path for fine-grained listeners; e.g., "data.items.0.title" */
99
+ path?: string;
100
+ }
101
+ /**
102
+ * Emit function narrowed to the developer's EventMap.
103
+ * Returns a Promise that resolves when the event has been fully processed.
104
+ *
105
+ * @typeParam EM - Event map.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * type EM = { ui: { increment: number } };
110
+ * const emit: Emit<EM> = async (channel, type, payload) => { /* ... *\/ };
111
+ * await emit('ui', 'increment', 1);
112
+ * ```
113
+ *
114
+ * @public
115
+ */
116
+ export type Emit<EM extends EventMapBase> = <C extends keyof EM & string, T extends keyof EM[C] & string>(channel: C, type: T, payload: EM[C][T]) => Promise<void>;
117
+ /**
118
+ * Basic unsubscribe handle.
119
+ *
120
+ * @public
121
+ */
122
+ export type Unsubscribe = () => void;
123
+ /**
124
+ * Store spec - what you feed into the constructor / factory.
125
+ *
126
+ * @typeParam R - Reducer name union (string literal union).
127
+ * @typeParam S - State record keyed by `R`.
128
+ * @typeParam EM - Event map.
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * type S = { counter: { value: number } };
133
+ * type EM = { ui: { increment: number } };
134
+ *
135
+ * const spec: StoreSpec<'counter', S, EM> = {
136
+ * name: 'App',
137
+ * reducer: {
138
+ * counter: {
139
+ * state: { value: 0 },
140
+ * events: [['ui', 'increment']],
141
+ * reducer(s, evt) {
142
+ * if (evt.type === 'increment') return { value: s.value + evt.payload };
143
+ * return s;
144
+ * }
145
+ * }
146
+ * }
147
+ * };
148
+ * ```
149
+ *
150
+ * @public
151
+ */
152
+ /**
153
+ * Middleware input: accepts either a function (legacy) or a spec object (recommended).
154
+ *
155
+ * @typeParam S - Store state (readonly).
156
+ * @typeParam EM - Event map.
157
+ *
158
+ * @example Function form (legacy)
159
+ * ```ts
160
+ * const mw: MiddlewareInput<AppState, AppEM> = (state, event, emit) => {
161
+ * console.log(event.type);
162
+ * return true;
163
+ * };
164
+ * ```
165
+ *
166
+ * @example Spec form (recommended)
167
+ * ```ts
168
+ * const mw: MiddlewareInput<AppState, AppEM> = {
169
+ * when: { channel: 'admin' },
170
+ * middleware: (state, event, emit) => state.auth.isAdmin,
171
+ * meta: { type: 'middleware', name: 'authGuard' },
172
+ * };
173
+ * ```
174
+ *
175
+ * @public
176
+ */
177
+ export type MiddlewareInput<S = any, EM extends EventMapBase = EventMapBase> = MiddlewareFunction<S, EM> | MiddlewareSpec<S, EM>;
178
+ /**
179
+ * Store configuration object passed to the {@link Store} constructor or {@link createStore}.
180
+ *
181
+ * @typeParam R - Reducer name union (string literal union).
182
+ * @typeParam S - State record keyed by `R`.
183
+ * @typeParam EM - Event map.
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * type S = { counter: { value: number } };
188
+ * type EM = { ui: { increment: number } };
189
+ *
190
+ * const spec: StoreSpec<'counter', S, EM> = {
191
+ * name: 'App',
192
+ * reducer: {
193
+ * counter: {
194
+ * state: { value: 0 },
195
+ * when: { keys: eventKeys<EM>()([['ui', 'increment']]) },
196
+ * reducer(s, evt) {
197
+ * if (evt.type === 'increment') return { value: s.value + evt.payload };
198
+ * return s;
199
+ * }
200
+ * }
201
+ * }
202
+ * };
203
+ * ```
204
+ *
205
+ * @public
206
+ */
207
+ export type StoreSpec<R extends string, S extends Record<R, any>, EM extends EventMapBase> = {
208
+ /**
209
+ * Store name (used by DevTools to identify the instance).
210
+ */
211
+ name: string;
212
+ /**
213
+ * Map of slice name → reducer spec.
214
+ * Each entry declares initial state, the reducer function, and the event targeting.
215
+ */
216
+ reducer: Record<R, ReducerSpec<S[R], EM>>;
217
+ /**
218
+ * Middleware chain executed before reducers/effects.
219
+ * Accepts either functions (legacy) or MiddlewareSpec objects (recommended).
220
+ * If any middleware returns false (or resolves to false), the event will not propagate.
221
+ */
222
+ middleware?: MiddlewareInput<DeepReadonly<S>, EM>[];
223
+ /**
224
+ * Optional side-effect handlers registered at construction time.
225
+ * Runs after reducers for every propagated event.
226
+ */
227
+ effects?: Array<EffectSpec<DeepReadonly<S>, EM>>;
228
+ /**
229
+ * Time window in milliseconds for event deduplication.
230
+ * Events with identical fingerprints (channel + type + serialized payload)
231
+ * within this window are considered duplicates and skipped.
232
+ *
233
+ * This helps prevent double-firing in React Strict Mode.
234
+ *
235
+ * @default 50 in development, 100 in production
236
+ */
237
+ dedupWindowMs?: number;
238
+ /**
239
+ * DevTools configuration options.
240
+ *
241
+ * @remarks
242
+ * These options control runtime DevTools capabilities such as event replay.
243
+ */
244
+ devtools?: {
245
+ /**
246
+ * Enable event replay via `__replayEvents()`.
247
+ * When `false` (default), calling `__replayEvents()` throws.
248
+ *
249
+ * @default false
250
+ */
251
+ allowReplay?: boolean;
252
+ };
253
+ };
254
+ /**
255
+ * Public Store surface.
256
+ *
257
+ * @typeParam R - Reducer name union.
258
+ * @typeParam S - State record (already readonly at the call site).
259
+ * @typeParam EM - Event map.
260
+ *
261
+ * @remarks
262
+ * The concrete Store implements this as `StoreInstance<R, DeepReadonly<S>, EM>`.
263
+ *
264
+ * @public
265
+ */
266
+ export interface StoreInstance<R extends string = string, S extends Record<R, any> = Record<string, any>, EM extends EventMapBase = EventMapBase> {
267
+ /**
268
+ * Store name (used by DevTools to identify the instance).
269
+ */
270
+ name: string;
271
+ /**
272
+ * Read the full state (already readonly).
273
+ */
274
+ getState(): DeepReadonly<S>;
275
+ /**
276
+ * Emit a typed event `(channel, type, payload)`.
277
+ * Returns a promise that resolves when the event has been processed.
278
+ */
279
+ emit: Emit<EM>;
280
+ /**
281
+ * Coarse subscription: runs after any state change (once per committed event).
282
+ */
283
+ subscribe(listener: () => void): Unsubscribe;
284
+ /**
285
+ * Fine-grained subscription: listen to a specific `reducer.property` path.
286
+ * Accepts a dotted path string (e.g., "data.123.title").
287
+ * Fires when that path (or its ancestors) actually changes.
288
+ *
289
+ * @param spec - `{ reducer, property }` where `property` is a single dotted path string.
290
+ * @param handler - Handler receiving a {@link Change} with `{ oldValue, newValue, path }`.
291
+ */
292
+ connect(spec: {
293
+ reducer: R;
294
+ property: string;
295
+ }, handler: (change: Change) => void): Unsubscribe;
296
+ /**
297
+ * Convenience helper to register an **effect** filtered by a single `(channel, type)` pair.
298
+ *
299
+ * @typeParam C - Channel key within `EM`.
300
+ * @typeParam T - Event type key within channel `C`.
301
+ * @param channel - Channel to filter.
302
+ * @param type - Event type to filter.
303
+ * @param handler - Effect handler `(payload, getState, emit, event)`.
304
+ *
305
+ * @returns Unsubscribe/teardown function.
306
+ */
307
+ onEffect<C extends keyof EM & string, T extends keyof EM[C] & string>(channel: C, type: T, handler: (payload: EM[C][T], getState: () => DeepReadonly<S>, emit: Emit<EM>, event: Event<EM, C, T>) => void | Promise<void>): Unsubscribe;
308
+ /**
309
+ * Register a post-reducer effect (sees final state). Returns an unsubscribe.
310
+ */
311
+ registerEffect(spec: EffectSpec<DeepReadonly<S>, EM>): Unsubscribe;
312
+ /**
313
+ * Dynamically add middleware.
314
+ */
315
+ registerMiddleware(mw: MiddlewareFunction<DeepReadonly<S>, EM>): Unsubscribe;
316
+ /**
317
+ * Dynamically add/remove a namespaced reducer slice at runtime.
318
+ */
319
+ registerReducer(name: string, spec: ReducerSpec<any, EM>): Unsubscribe;
320
+ /**
321
+ * Cleanup resources (timers, etc.) when disposing the store.
322
+ * Call this if you're dynamically creating/destroying stores.
323
+ */
324
+ dispose(): void;
325
+ /**
326
+ * Subscribe to events by channel and type.
327
+ *
328
+ * Event subscriptions are intended for the View layer (e.g., React components)
329
+ * to react to events without affecting the event flow. They are fire-and-forget
330
+ * and cannot cancel event propagation.
331
+ *
332
+ * **Phases:**
333
+ * - `'committed'` (default): Events that passed middleware and reached reducers
334
+ * - `'uncommitted'`: Events rejected by middleware
335
+ * - `'all'`: Both committed and uncommitted events (handler receives phase parameter)
336
+ *
337
+ * @typeParam C - Channel key within `EM`.
338
+ * @typeParam T - Event type key within channel `C`.
339
+ * @param channel - Channel to subscribe to.
340
+ * @param type - Event type to subscribe to.
341
+ * @param handler - Handler function `(event, getState, emit, phase)`.
342
+ * @param phase - Event phase to subscribe to (default: `'committed'`).
343
+ * @returns Unsubscribe function.
344
+ *
345
+ * @example Committed events (default)
346
+ * ```ts
347
+ * const off = store.onEvent('ui', 'save', (event, getState, emit, phase) => {
348
+ * console.log('Save committed:', event.payload);
349
+ * });
350
+ * ```
351
+ *
352
+ * @example Uncommitted (rejected) events
353
+ * ```ts
354
+ * store.onEvent('ui', 'delete', (event, getState, emit, phase) => {
355
+ * console.log('Delete was rejected by middleware');
356
+ * }, 'uncommitted');
357
+ * ```
358
+ *
359
+ * @example All events
360
+ * ```ts
361
+ * store.onEvent('ui', 'action', (event, getState, emit, phase) => {
362
+ * console.log('Action:', phase); // 'committed' or 'uncommitted'
363
+ * }, 'all');
364
+ * ```
365
+ */
366
+ onEvent<C extends keyof EM & string, T extends keyof EM[C] & string>(channel: C, type: T, handler: NarrowedEventHandler<DeepReadonly<S>, EM, C, T>, phase?: EventPhase): Unsubscribe;
367
+ /**
368
+ * Replaces the entire middleware pipeline (HMR-friendly).
369
+ *
370
+ * @param next - New middleware array.
371
+ */
372
+ replaceMiddleware(next: MiddlewareFunction<DeepReadonly<S>, EM>[]): void;
373
+ /**
374
+ * Replaces all registered effects (HMR-friendly).
375
+ *
376
+ * @param next - New effects array (as EffectSpecs).
377
+ */
378
+ replaceEffects(next: Array<EffectSpec<DeepReadonly<S>, EM>>): void;
379
+ /**
380
+ * Replaces the entire reducer set (HMR-friendly).
381
+ *
382
+ * @param next - Map of slice specs keyed by slice name.
383
+ * @param opts - `{ preserveState?: boolean }` (default `true`).
384
+ */
385
+ replaceReducers(next: Record<R, ReducerSpec<S[R], EM>>, opts?: {
386
+ preserveState?: boolean;
387
+ }): void;
388
+ /**
389
+ * Convenience API to replace any subset of store parts (HMR patterns).
390
+ *
391
+ * @param partial - Partial replacement set.
392
+ */
393
+ hotReplace(partial: {
394
+ reducer?: Record<R, ReducerSpec<S[R], EM>>;
395
+ middleware?: MiddlewareFunction<DeepReadonly<S>, EM>[];
396
+ effects?: Array<EffectSpec<DeepReadonly<S>, EM>>;
397
+ preserveState?: boolean;
398
+ }): void;
399
+ /**
400
+ * Replays a sequence of events from a snapshot through reducers and event
401
+ * subscribers ONLY. Skips dedup, middleware, and effects.
402
+ *
403
+ * Gated by `createStore({ devtools: { allowReplay: true } })`.
404
+ * Throws if replay is not enabled.
405
+ *
406
+ * @param snapshot - The state snapshot to restore before replaying.
407
+ * @param events - Array of events to replay (in order).
408
+ *
409
+ * @internal
410
+ */
411
+ __replayEvents(snapshot: any, events: Array<{
412
+ channel: string;
413
+ type: string;
414
+ payload: any;
415
+ id: string;
416
+ }>): void;
417
+ /**
418
+ * Returns a structured introspection snapshot for DevTools UIs.
419
+ *
420
+ * @returns Reducers, effects, middleware, event subscriptions, and coarse subscriber count.
421
+ *
422
+ * @internal
423
+ */
424
+ __devtoolsIntrospect(): {
425
+ reducers: Array<{
426
+ name: string;
427
+ when?: unknown;
428
+ }>;
429
+ effects: Array<{
430
+ channel: string;
431
+ type: string;
432
+ name?: string;
433
+ description?: string;
434
+ }>;
435
+ middleware: Array<{
436
+ name?: string;
437
+ description?: string;
438
+ when?: unknown;
439
+ }>;
440
+ atomic: Array<{
441
+ reducer: string;
442
+ property: string;
443
+ }>;
444
+ event: Array<{
445
+ channel: string;
446
+ type: string;
447
+ phase: string;
448
+ }>;
449
+ coarse: number;
450
+ };
451
+ }
452
+ /**
453
+ * One reducer's definition blob (stateful event consumer).
454
+ *
455
+ * @typeParam S - State managed by this reducer.
456
+ * @typeParam EM - Event map.
457
+ *
458
+ * @remarks
459
+ * Use `when` for event targeting (preferred). The `events` property is
460
+ * kept for backward compatibility but `when` is recommended for new code.
461
+ *
462
+ * @example Using `when` (recommended)
463
+ * ```ts
464
+ * const counterSpec: ReducerSpec<{ value: number }, MyEM> = {
465
+ * state: { value: 0 },
466
+ * when: { keys: eventKeys<MyEM>()([['ui', 'increment'], ['ui', 'decrement']]) },
467
+ * reducer(s, evt) {
468
+ * if (evt.type === 'increment') return { value: s.value + evt.payload };
469
+ * if (evt.type === 'decrement') return { value: s.value - evt.payload };
470
+ * return s;
471
+ * },
472
+ * meta: { type: 'reducer', name: 'counter' },
473
+ * };
474
+ * ```
475
+ *
476
+ * @example Using `events` (legacy)
477
+ * ```ts
478
+ * const counterSpec: ReducerSpec<{ value: number }, MyEM> = {
479
+ * state: { value: 0 },
480
+ * events: [['ui', 'increment'], ['ui', 'decrement']],
481
+ * reducer(s, evt) { ... },
482
+ * };
483
+ * ```
484
+ *
485
+ * @public
486
+ */
487
+ export interface ReducerSpec<S = any, EM extends EventMapBase = EventMapBase> {
488
+ /**
489
+ * Initial state for this reducer.
490
+ */
491
+ state: S;
492
+ /**
493
+ * Event targeting using the unified `When` matcher.
494
+ * Preferred over `events` for new code.
495
+ */
496
+ when?: When<EM>;
497
+ /**
498
+ * List of EventKeys `[channel, type]` that this reducer responds to.
499
+ * @deprecated Use `when: { keys: [...] }` instead for better type inference.
500
+ */
501
+ events?: ReadonlyArray<EventKey<EM>>;
502
+ /**
503
+ * Pure reducer function: `(state, event) => nextState`.
504
+ */
505
+ reducer: ReducerFunction<S, EM>;
506
+ /**
507
+ * Optional metadata for debugging tools and DevTools integration.
508
+ */
509
+ meta?: EventConsumerMeta<"reducer">;
510
+ }
511
+ /**
512
+ * Pure reducer function (stateful event consumer).
513
+ *
514
+ * @typeParam S - State type.
515
+ * @typeParam EM - Event map.
516
+ *
517
+ * @public
518
+ */
519
+ export type ReducerFunction<S = any, EM extends EventMapBase = EventMapBase> = (state: S, event: EventUnion<EM>) => S;
520
+ /**
521
+ * Effect specification (stateless async event consumer).
522
+ *
523
+ * @typeParam S - Store state type (readonly).
524
+ * @typeParam EM - Event map.
525
+ *
526
+ * @remarks
527
+ * - Effects run after reducers see the event.
528
+ * - Effects are async-safe and do not own state.
529
+ * - Effects are keyed by event for O(1) lookup (no scanning).
530
+ * - Use `when` for event targeting (preferred over `events`).
531
+ *
532
+ * @example Using `when` (recommended)
533
+ * ```ts
534
+ * const logEffect: EffectSpec<AppState, MyEM> = {
535
+ * when: { keys: eventKeys<MyEM>()([['ui', 'increment']]) },
536
+ * effect: async (evt, getState, emit) => {
537
+ * console.log('increment', evt.payload, getState().counter.value);
538
+ * },
539
+ * meta: { type: 'effect', name: 'logEffect', description: 'Logs increment events' },
540
+ * };
541
+ * ```
542
+ *
543
+ * @example Match all events in a channel
544
+ * ```ts
545
+ * const notificationEffect: EffectSpec<AppState, MyEM> = {
546
+ * when: { channel: 'notifications' },
547
+ * effect: (evt, getState, emit) => {
548
+ * if (evt.type === 'show') showToast(evt.payload.message);
549
+ * },
550
+ * };
551
+ * ```
552
+ *
553
+ * @public
554
+ */
555
+ export interface EffectSpec<S = any, EM extends EventMapBase = EventMapBase> {
556
+ /**
557
+ * Event targeting using the unified `When` matcher.
558
+ * Preferred over `events` for new code.
559
+ */
560
+ when?: When<EM>;
561
+ /**
562
+ * List of EventKeys `[channel, type]` that this effect responds to.
563
+ * @deprecated Use `when: { keys: [...] }` instead for better type inference.
564
+ */
565
+ events?: ReadonlyArray<EventKey<EM>>;
566
+ /**
567
+ * Async effect handler: `(event, getState, emit) => void | Promise<void>`.
568
+ */
569
+ effect: EffectFunction<S, EM>;
570
+ /**
571
+ * Optional metadata for debugging tools and DevTools integration.
572
+ */
573
+ meta?: EventConsumerMeta<"effect">;
574
+ }
575
+ /**
576
+ * Every legal `{ channel, type, payload, id }` as a *distinct* object type.
577
+ *
578
+ * @typeParam EM - Event map.
579
+ *
580
+ * @public
581
+ */
582
+ export type EventUnion<EM extends EventMapBase> = {
583
+ [C in keyof EM & string]: {
584
+ [T in keyof EM[C] & string]: Event<EM, C, T>;
585
+ }[keyof EM[C] & string];
586
+ }[keyof EM & string];
587
+ /**
588
+ * Middleware function: may mutate, log, side-effect, or veto an event.
589
+ * Return true to continue; false to swallow / cancel propagation.
590
+ *
591
+ * @typeParam S - Store state (readonly).
592
+ * @typeParam EM - Event map.
593
+ *
594
+ * @public
595
+ */
596
+ export type MiddlewareFunction<S = any, EM extends EventMapBase = EventMapBase> = (state: S, event: EventUnion<EM>, emit: Emit<EM>) => boolean | Promise<boolean>;
597
+ /**
598
+ * Middleware specification with optional event targeting and metadata.
599
+ *
600
+ * @typeParam S - Store state (readonly).
601
+ * @typeParam EM - Event map.
602
+ *
603
+ * @remarks
604
+ * - If `when` is omitted, middleware receives ALL events.
605
+ * - Use `when` to filter which events the middleware processes.
606
+ * - Middleware runs BEFORE reducers and can cancel event propagation.
607
+ *
608
+ * @example Global logging middleware (all events)
609
+ * ```ts
610
+ * const loggingMiddleware: MiddlewareSpec<AppState, AppEM> = {
611
+ * middleware: (state, event, emit) => {
612
+ * console.log('Event:', event.channel, event.type);
613
+ * return true; // allow propagation
614
+ * },
615
+ * meta: { type: 'middleware', name: 'logger' },
616
+ * };
617
+ * ```
618
+ *
619
+ * @example Filtered middleware (specific events)
620
+ * ```ts
621
+ * const authMiddleware: MiddlewareSpec<AppState, AppEM> = {
622
+ * when: { channel: 'admin' },
623
+ * middleware: (state, event, emit) => {
624
+ * if (!state.auth.isAdmin) return false; // cancel
625
+ * return true;
626
+ * },
627
+ * meta: { type: 'middleware', name: 'authGuard', description: 'Guards admin events' },
628
+ * };
629
+ * ```
630
+ *
631
+ * @public
632
+ */
633
+ export interface MiddlewareSpec<S = any, EM extends EventMapBase = EventMapBase> {
634
+ /**
635
+ * Event targeting (optional). If omitted, middleware receives ALL events.
636
+ */
637
+ when?: When<EM>;
638
+ /**
639
+ * Middleware function: `(state, event, emit) => boolean | Promise<boolean>`.
640
+ * Return `false` to cancel event propagation.
641
+ */
642
+ middleware: MiddlewareFunction<S, EM>;
643
+ /**
644
+ * Optional metadata for debugging tools and DevTools integration.
645
+ */
646
+ meta?: EventConsumerMeta<"middleware">;
647
+ }
648
+ /**
649
+ * Effect handler: runs AFTER reducers, sees the final state.
650
+ *
651
+ * @typeParam S - Store state (readonly).
652
+ * @typeParam EM - Event map.
653
+ *
654
+ * @public
655
+ */
656
+ export type EffectFunction<S = any, EM extends EventMapBase = EventMapBase> = (event: EventUnion<EM>, getState: () => S, emit: Emit<EM>) => void | Promise<void>;
657
+ /**
658
+ * Helper: extract state shape from a reducers map.
659
+ *
660
+ * @internal
661
+ */
662
+ export type ReducersMapAny = Record<string, ReducerSpec<any, any>>;
663
+ /**
664
+ * Helper: derive state type from a reducers map.
665
+ *
666
+ * @internal
667
+ */
668
+ export type StateFromReducers<R> = {
669
+ [K in keyof R]: R[K] extends ReducerSpec<infer S, any> ? S : never;
670
+ };
671
+ /**
672
+ * Helper: derive event map from a reducers map (strict).
673
+ * Used by createStore inference overload.
674
+ *
675
+ * @internal
676
+ */
677
+ export type EMFromReducersStrict<RM extends ReducersMapAny> = RM[keyof RM] extends ReducerSpec<any, infer EM> ? RM[keyof RM] extends ReducerSpec<any, EM> ? EM : never : never;
678
+ /**
679
+ * Matcher for event targeting across reducers, effects, middleware, and subscriptions.
680
+ *
681
+ * Supports four targeting modes:
682
+ * - `{ any: true }` — match all events
683
+ * - `{ keys: [...] }` — match specific `[channel, type]` pairs (correlated)
684
+ * - `{ channel: 'x' }` — match all events in a channel
685
+ * - `{ channels: ['x', 'y'] }` — match all events in multiple channels
686
+ *
687
+ * @typeParam EM - Event map.
688
+ *
689
+ * @example Match all events
690
+ * ```ts
691
+ * const mw: MiddlewareSpec<S, EM> = {
692
+ * when: { any: true },
693
+ * middleware: (state, event, emit) => true,
694
+ * };
695
+ * ```
696
+ *
697
+ * @example Match specific event keys
698
+ * ```ts
699
+ * const reducer: ReducerSpec<S, EM> = {
700
+ * state: { value: 0 },
701
+ * when: { keys: eventKeys<EM>()([['ui', 'increment'], ['ui', 'decrement']]) },
702
+ * reducer: (s, e) => { ... },
703
+ * };
704
+ * ```
705
+ *
706
+ * @example Match entire channel
707
+ * ```ts
708
+ * const effect: EffectSpec<S, EM> = {
709
+ * when: { channel: 'notifications' },
710
+ * effect: (e, getState, emit) => { ... },
711
+ * };
712
+ * ```
713
+ *
714
+ * @public
715
+ */
716
+ export type When<EM extends EventMapBase> = {
717
+ any: true;
718
+ } | {
719
+ keys: ReadonlyArray<EventKey<EM>>;
720
+ } | {
721
+ channel: keyof EM & string;
722
+ } | {
723
+ channels: ReadonlyArray<keyof EM & string>;
724
+ };
725
+ /**
726
+ * Helper to create type-safe EventKey arrays without requiring `as const`.
727
+ * Preserves literal tuple types for proper type correlation in handlers.
728
+ *
729
+ * @typeParam EM - Event map.
730
+ *
731
+ * @example
732
+ * ```ts
733
+ * type AppEM = {
734
+ * ui: { increment: number; decrement: number };
735
+ * data: { loaded: string[] };
736
+ * };
737
+ *
738
+ * // Without helper (requires `as const`):
739
+ * const keys = [['ui', 'increment'], ['ui', 'decrement']] as const;
740
+ *
741
+ * // With helper (no `as const` needed):
742
+ * const keys = eventKeys<AppEM>()([
743
+ * ['ui', 'increment'],
744
+ * ['ui', 'decrement'],
745
+ * ]);
746
+ * // Type: readonly [['ui', 'increment'], ['ui', 'decrement']]
747
+ * ```
748
+ *
749
+ * @public
750
+ */
751
+ export declare const eventKeys: <EM extends EventMapBase>() => <const K extends ReadonlyArray<EventKey<EM>>>(keys: K) => K;
752
+ /**
753
+ * Extracts the event union from a `When` matcher.
754
+ * Used internally to narrow handler `event` parameter types based on the matcher.
755
+ *
756
+ * @typeParam EM - Event map.
757
+ * @typeParam W - When matcher type.
758
+ *
759
+ * @internal
760
+ */
761
+ export type EventFromWhen<EM extends EventMapBase, W extends When<EM>> = W extends {
762
+ any: true;
763
+ } ? EventUnion<EM> : W extends {
764
+ keys: ReadonlyArray<infer K>;
765
+ } ? K extends readonly [infer C, infer T] ? C extends keyof EM & string ? T extends keyof EM[C] & string ? Event<EM, C, T> : never : never : never : W extends {
766
+ channel: infer C;
767
+ } ? C extends keyof EM & string ? {
768
+ [T in keyof EM[C] & string]: Event<EM, C, T>;
769
+ }[keyof EM[C] & string] : never : W extends {
770
+ channels: ReadonlyArray<infer C>;
771
+ } ? C extends keyof EM & string ? {
772
+ [T in keyof EM[C] & string]: Event<EM, C, T>;
773
+ }[keyof EM[C] & string] : never : never;
774
+ /**
775
+ * Resolves the value type at a dotted path `P` inside object/array `T`.
776
+ * Supports numeric segments for array indexing (e.g., `"items.0.title"`).
777
+ *
778
+ * @typeParam T - Root type to index into.
779
+ * @typeParam P - Dotted path string.
780
+ *
781
+ * @example
782
+ * ```ts
783
+ * type S = { todos: Array<{ title: string; done: boolean }> };
784
+ * type T1 = PathValue<S['todos'], '0.title'>; // string
785
+ * type T2 = PathValue<S, 'todos.0'>; // { title: string; done: boolean }
786
+ * type T3 = PathValue<S, 'todos'>; // Array<{ title: string; done: boolean }>
787
+ * ```
788
+ *
789
+ * @public
790
+ */
791
+ export type PathValue<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? PathValue<T[K], Rest> : K extends `${number}` ? T extends readonly (infer E)[] ? PathValue<E, Rest> : never : never : P extends keyof T ? T[P] : P extends `${number}` ? T extends readonly (infer E)[] ? E : never : never;
792
+ /**
793
+ * Type discriminator for event consumers.
794
+ *
795
+ * @public
796
+ */
797
+ export type EventConsumerType = "reducer" | "middleware" | "effect";
798
+ /**
799
+ * Metadata for event consumers (reducers, effects, middleware).
800
+ * Useful for debugging tools, DevTools integration, and introspection.
801
+ *
802
+ * @typeParam T - Consumer type discriminator.
803
+ *
804
+ * @example
805
+ * ```ts
806
+ * const counterReducer: ReducerSpec<CounterState, AppEM> = {
807
+ * state: { value: 0 },
808
+ * when: { keys: eventKeys<AppEM>()([['ui', 'increment']]) },
809
+ * reducer: (s, e) => ({ value: s.value + e.payload }),
810
+ * meta: {
811
+ * type: 'reducer',
812
+ * name: 'counterReducer',
813
+ * description: 'Handles counter increment/decrement events',
814
+ * },
815
+ * };
816
+ * ```
817
+ *
818
+ * @public
819
+ */
820
+ export interface EventConsumerMeta<T extends EventConsumerType = EventConsumerType> {
821
+ /** Consumer type discriminator */
822
+ type: T;
823
+ /** Unique identifier for this consumer */
824
+ name: string;
825
+ /** Brief one-liner description of what this consumer does */
826
+ description?: string;
827
+ }
828
+ /**
829
+ * Alias for DeepReadonly.
830
+ *
831
+ * @public
832
+ */
833
+ export type DeepRO<T> = DeepReadonly<T>;
834
+ /**
835
+ * Primitive types (terminal leaves in deep traversal).
836
+ *
837
+ * @public
838
+ */
839
+ export type Primitive = string | number | boolean | bigint | symbol | null | undefined | Date | RegExp;
840
+ /**
841
+ * Compute dotted paths of T, including nested objects and arrays.
842
+ *
843
+ * @typeParam T - Type to compute paths for.
844
+ *
845
+ * @public
846
+ */
847
+ export type Path<T> = T extends Primitive ? never : T extends readonly (infer U)[] ? `${number}` | (Path<U> extends never ? never : `${number}.${Path<U>}`) : {
848
+ [K in keyof T & string]: T[K] extends Primitive ? K : K | (Path<T[K]> extends never ? never : `${K}.${Path<T[K]>}`);
849
+ }[keyof T & string];
850
+ /**
851
+ * Allow wildcard patterns like "*" and "**" anywhere in the string.
852
+ *
853
+ * @typeParam T - Base string type.
854
+ *
855
+ * @public
856
+ */
857
+ export type WithGlob<T extends string> = T | `${string}*${string}`;
858
+ /**
859
+ * Dotted keys of a slice: top-level keys or any nested path.
860
+ *
861
+ * @typeParam Slice - Slice state type.
862
+ *
863
+ * @public
864
+ */
865
+ export type Dotted<Slice> = (keyof Slice & string) | Path<Slice>;
866
+ /**
867
+ * Deep readonly type: recursively makes all properties readonly.
868
+ *
869
+ * @typeParam T - Type to make readonly.
870
+ *
871
+ * @public
872
+ */
873
+ export type DeepReadonly<T> = T extends (infer A)[] ? ReadonlyArray<DeepReadonly<A>> : T extends object ? {
874
+ readonly [K in keyof T]: DeepReadonly<T[K]>;
875
+ } : T;
876
+ /**
877
+ * Phase of event subscription notification.
878
+ *
879
+ * - `'committed'`: Events that passed middleware and reached reducers (default)
880
+ * - `'uncommitted'`: Events rejected by middleware
881
+ * - `'all'`: Both committed and uncommitted events
882
+ *
883
+ * @public
884
+ */
885
+ export type EventPhase = "committed" | "uncommitted" | "all";
886
+ /**
887
+ * Handler function for event subscriptions (receives full event union).
888
+ *
889
+ * Event subscriptions are intended for the View layer (e.g., React components)
890
+ * to react to events without affecting the event flow. They are fire-and-forget
891
+ * and cannot cancel event propagation.
892
+ *
893
+ * @typeParam S - Store state type (readonly).
894
+ * @typeParam EM - Event map.
895
+ *
896
+ * @param event - The event that was emitted
897
+ * @param getState - Function to get current state
898
+ * @param emit - Function to emit new events
899
+ * @param phase - The phase ('committed' or 'uncommitted') indicating how the event was processed
900
+ *
901
+ * @example
902
+ * ```ts
903
+ * const handler: EventSubscriptionHandler<AppState, AppEM> = (event, getState, emit, phase) => {
904
+ * if (phase === 'committed') {
905
+ * console.log('Event committed:', event.type);
906
+ * } else {
907
+ * console.log('Event rejected:', event.type);
908
+ * }
909
+ * };
910
+ * ```
911
+ *
912
+ * @public
913
+ */
914
+ export type EventSubscriptionHandler<S = any, EM extends EventMapBase = EventMapBase> = (event: EventUnion<EM>, getState: () => S, emit: Emit<EM>, phase: "committed" | "uncommitted") => void | Promise<void>;
915
+ /**
916
+ * Narrowed event subscription handler for specific `(channel, type)` pairs.
917
+ * Provides better type inference when subscribing to a single event type.
918
+ *
919
+ * @typeParam S - Store state type (readonly).
920
+ * @typeParam EM - Event map.
921
+ * @typeParam C - Channel key within `EM`.
922
+ * @typeParam T - Event type key within channel `C`.
923
+ *
924
+ * @example
925
+ * ```ts
926
+ * const handler: NarrowedEventHandler<AppState, AppEM, 'ui', 'increment'> = (
927
+ * event, // Event<AppEM, 'ui', 'increment'> - narrowed!
928
+ * getState,
929
+ * emit,
930
+ * phase,
931
+ * ) => {
932
+ * // event.payload is typed as number (from EM['ui']['increment'])
933
+ * console.log('Increment by:', event.payload);
934
+ * };
935
+ * ```
936
+ *
937
+ * @public
938
+ */
939
+ export type NarrowedEventHandler<S, EM extends EventMapBase, C extends keyof EM & string, T extends keyof EM[C] & string> = (event: Event<EM, C, T>, getState: () => S, emit: Emit<EM>, phase: "committed" | "uncommitted") => void | Promise<void>;