@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.
- package/LICENSE +21 -0
- package/README.es.md +473 -0
- package/README.md +468 -0
- package/dist/types/eventBus/EventBus.d.ts +127 -0
- package/dist/types/eventBus/LooseEventBus.d.ts +219 -0
- package/dist/types/eventBus/index.d.ts +2 -0
- package/dist/types/index.d.ts +15 -0
- package/dist/types/reducer/Reducer.d.ts +81 -0
- package/dist/types/store/Store.d.ts +834 -0
- package/dist/types/types.d.ts +939 -0
- package/dist/types/utils/detectChangedProps.d.ts +67 -0
- package/dist/types/utils/immutability.d.ts +47 -0
- package/dist/types/utils/index.d.ts +2 -0
- package/dist/yoltra.cjs.js +8 -0
- package/dist/yoltra.esm.js +1557 -0
- package/dist/yoltra.umd.js +8 -0
- package/package.json +83 -0
|
@@ -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>;
|