@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,834 @@
|
|
|
1
|
+
import { Event, EventMapBase, EventKey, Change, DeepReadonly, EffectSpec, MiddlewareFunction, ReducersMapAny, ReducerSpec, StateFromReducers, StoreInstance, StoreSpec, Unsubscribe, EMFromReducersStrict, Emit, EventPhase, NarrowedEventHandler, When } from '../types';
|
|
2
|
+
export declare class Store<EM extends EventMapBase, R extends string, S extends Record<R, any>> implements StoreInstance<R, S, EM> {
|
|
3
|
+
/**
|
|
4
|
+
* Store name (used by DevTools & diagnostics).
|
|
5
|
+
*
|
|
6
|
+
* @public
|
|
7
|
+
*/
|
|
8
|
+
name: string;
|
|
9
|
+
/**
|
|
10
|
+
* Registered middleware pipeline (run **before** reducers).
|
|
11
|
+
* Stores either raw functions (legacy) or MiddlewareSpec objects.
|
|
12
|
+
* Return `false` from the middleware function to stop propagation.
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
private readonly middleware;
|
|
17
|
+
/**
|
|
18
|
+
* Installed slice reducers keyed by slice name.
|
|
19
|
+
*
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
private readonly reducers;
|
|
23
|
+
/**
|
|
24
|
+
* Current immutable snapshot of the store state.
|
|
25
|
+
* This reference changes whenever any slice changes (shallow immutability).
|
|
26
|
+
*
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
private state;
|
|
30
|
+
/**
|
|
31
|
+
* Bus for reducer wiring (emit by `(channel, type)`).
|
|
32
|
+
*
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
private readonly reducerBus;
|
|
36
|
+
/**
|
|
37
|
+
* Bus for **granular** connector events (emit by **dotted path** inside a slice).
|
|
38
|
+
*
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
private readonly connectorBus;
|
|
42
|
+
/**
|
|
43
|
+
* Coarse-grained listeners (called once per committed event, only if state changed).
|
|
44
|
+
*
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
private readonly listeners;
|
|
48
|
+
/**
|
|
49
|
+
* Registered effect handlers keyed by `"channel::type"` for O(1) lookup.
|
|
50
|
+
* Used for effects with explicit `keys` or legacy `events` targeting.
|
|
51
|
+
*
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
private readonly effects;
|
|
55
|
+
/**
|
|
56
|
+
* Pattern-based effects that need runtime matching.
|
|
57
|
+
* Used for effects with `when: { any }`, `{ channel }`, or `{ channels }`.
|
|
58
|
+
* Stores tuples of [effect function, when matcher].
|
|
59
|
+
*
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
62
|
+
private readonly patternEffects;
|
|
63
|
+
/**
|
|
64
|
+
* Committed event subscribers keyed by `"channel::type"` for O(1) lookup.
|
|
65
|
+
* Notified after reducers, before effects, for events that passed middleware.
|
|
66
|
+
*
|
|
67
|
+
* @internal
|
|
68
|
+
*/
|
|
69
|
+
private readonly committedEventSubscribers;
|
|
70
|
+
/**
|
|
71
|
+
* Uncommitted event subscribers keyed by `"channel::type"` for O(1) lookup.
|
|
72
|
+
* Notified when middleware rejects an event.
|
|
73
|
+
*
|
|
74
|
+
* @internal
|
|
75
|
+
*/
|
|
76
|
+
private readonly uncommittedEventSubscribers;
|
|
77
|
+
/**
|
|
78
|
+
* All-events subscribers keyed by `"channel::type"` for O(1) lookup.
|
|
79
|
+
* Notified for both committed and uncommitted events with phase parameter.
|
|
80
|
+
*
|
|
81
|
+
* @internal
|
|
82
|
+
*/
|
|
83
|
+
private readonly allEventSubscribers;
|
|
84
|
+
/**
|
|
85
|
+
* Track reducerBus unsubs per slice for HMR/register/unregister.
|
|
86
|
+
*
|
|
87
|
+
* @internal
|
|
88
|
+
*/
|
|
89
|
+
private readonly sliceUnsubs;
|
|
90
|
+
/**
|
|
91
|
+
* Pattern-based reducers that need runtime matching.
|
|
92
|
+
* Used for reducers with `when: { any }`, `{ channel }`, or `{ channels }`.
|
|
93
|
+
* Maps slice name to the `when` matcher.
|
|
94
|
+
*
|
|
95
|
+
* @internal
|
|
96
|
+
*/
|
|
97
|
+
private readonly patternReducers;
|
|
98
|
+
/**
|
|
99
|
+
* Whether `__replayEvents()` is allowed.
|
|
100
|
+
* Set from `spec.devtools.allowReplay`.
|
|
101
|
+
*
|
|
102
|
+
* @internal
|
|
103
|
+
*/
|
|
104
|
+
private readonly replayEnabled;
|
|
105
|
+
/**
|
|
106
|
+
* FIFO event queue for serialized emission.
|
|
107
|
+
*
|
|
108
|
+
* @internal
|
|
109
|
+
*/
|
|
110
|
+
private readonly eventQueue;
|
|
111
|
+
/**
|
|
112
|
+
* Re-entrancy guard while draining the queue.
|
|
113
|
+
*
|
|
114
|
+
* @internal
|
|
115
|
+
*/
|
|
116
|
+
private isProcessingQueue;
|
|
117
|
+
/**
|
|
118
|
+
* Tracks processed events by fingerprint with timestamps for TTL-based deduplication.
|
|
119
|
+
*
|
|
120
|
+
* **Deduplication Behavior:**
|
|
121
|
+
* - Events are fingerprinted using `channel::type::JSON(payload)`
|
|
122
|
+
* - If an identical fingerprint is seen within the dedup window, it's skipped
|
|
123
|
+
* - The window is 50ms in development, 100ms in production
|
|
124
|
+
*
|
|
125
|
+
* **Limitations:**
|
|
126
|
+
* - Non-serializable payloads (functions, symbols, circular refs) get unique
|
|
127
|
+
* fingerprints and won't be deduplicated
|
|
128
|
+
* - Legitimate rapid-fire identical events may be incorrectly deduplicated
|
|
129
|
+
* - The cache is bounded to 1000 entries with lazy pruning
|
|
130
|
+
*
|
|
131
|
+
* @internal
|
|
132
|
+
*/
|
|
133
|
+
private readonly processedEvents;
|
|
134
|
+
/**
|
|
135
|
+
* Configuration for event deduplication.
|
|
136
|
+
* @internal
|
|
137
|
+
*/
|
|
138
|
+
private readonly dedupConfig;
|
|
139
|
+
/**
|
|
140
|
+
* Timer for periodic cleanup of processed events.
|
|
141
|
+
*
|
|
142
|
+
* @internal
|
|
143
|
+
*/
|
|
144
|
+
private eventCleanupTimer;
|
|
145
|
+
/**
|
|
146
|
+
* Creates a store from a {@link StoreSpec}.
|
|
147
|
+
*
|
|
148
|
+
* @param spec - Store configuration (name, reducers, middleware, optional effects).
|
|
149
|
+
*
|
|
150
|
+
* @public
|
|
151
|
+
*/
|
|
152
|
+
constructor(spec: StoreSpec<R, S, EM>);
|
|
153
|
+
/**
|
|
154
|
+
* Cleanup resources (timers, etc.) when disposing the store.
|
|
155
|
+
* Call this if you're dynamically creating/destroying stores.
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```ts
|
|
159
|
+
* const store = createStore({ ... });
|
|
160
|
+
* // later
|
|
161
|
+
* store.dispose();
|
|
162
|
+
* ```
|
|
163
|
+
*
|
|
164
|
+
* @public
|
|
165
|
+
*/
|
|
166
|
+
dispose(): void;
|
|
167
|
+
/**
|
|
168
|
+
* Generates a fingerprint for an event for deduplication purposes.
|
|
169
|
+
* Falls back gracefully for non-serializable payloads.
|
|
170
|
+
*
|
|
171
|
+
* @param channel - Event channel.
|
|
172
|
+
* @param type - Event type.
|
|
173
|
+
* @param payload - Event payload.
|
|
174
|
+
* @returns A string fingerprint for the event.
|
|
175
|
+
*
|
|
176
|
+
* @internal
|
|
177
|
+
*/
|
|
178
|
+
private fingerprint;
|
|
179
|
+
/**
|
|
180
|
+
* Checks if an event should be deduplicated.
|
|
181
|
+
* Returns true if this is a duplicate that should be skipped.
|
|
182
|
+
*
|
|
183
|
+
* @param fp - Event fingerprint.
|
|
184
|
+
* @returns `true` if duplicate (should skip), `false` otherwise.
|
|
185
|
+
*
|
|
186
|
+
* @internal
|
|
187
|
+
*/
|
|
188
|
+
private shouldDedupe;
|
|
189
|
+
/**
|
|
190
|
+
* Removes expired entries from the processed events cache.
|
|
191
|
+
*
|
|
192
|
+
* @param now - Current timestamp.
|
|
193
|
+
*
|
|
194
|
+
* @internal
|
|
195
|
+
*/
|
|
196
|
+
private pruneProcessedEvents;
|
|
197
|
+
/**
|
|
198
|
+
* Checks if an event matches a `When` matcher.
|
|
199
|
+
*
|
|
200
|
+
* @param when - The When matcher (or undefined for "all events").
|
|
201
|
+
* @param event - The event to check.
|
|
202
|
+
* @returns `true` if the event matches, `false` otherwise.
|
|
203
|
+
*
|
|
204
|
+
* @remarks
|
|
205
|
+
* - `undefined` or missing `when` matches ALL events.
|
|
206
|
+
* - `{ any: true }` matches ALL events.
|
|
207
|
+
* - `{ keys: [...] }` matches if event's `[channel, type]` is in the array.
|
|
208
|
+
* - `{ channel: 'x' }` matches if event's channel equals 'x'.
|
|
209
|
+
* - `{ channels: ['x', 'y'] }` matches if event's channel is in the array.
|
|
210
|
+
*
|
|
211
|
+
* @internal
|
|
212
|
+
*/
|
|
213
|
+
private matchesWhen;
|
|
214
|
+
/**
|
|
215
|
+
* Extracts the middleware function from a MiddlewareInput.
|
|
216
|
+
* Handles both raw functions (legacy) and MiddlewareSpec objects.
|
|
217
|
+
*
|
|
218
|
+
* @param input - MiddlewareInput (function or spec).
|
|
219
|
+
* @returns The middleware function.
|
|
220
|
+
*
|
|
221
|
+
* @internal
|
|
222
|
+
*/
|
|
223
|
+
private getMiddlewareFunction;
|
|
224
|
+
/**
|
|
225
|
+
* Gets the `when` matcher from a MiddlewareInput.
|
|
226
|
+
*
|
|
227
|
+
* @param input - MiddlewareInput (function or spec).
|
|
228
|
+
* @returns The `when` matcher, or `undefined` for raw functions (match all).
|
|
229
|
+
*
|
|
230
|
+
* @internal
|
|
231
|
+
*/
|
|
232
|
+
private getMiddlewareWhen;
|
|
233
|
+
/**
|
|
234
|
+
* Invokes all registered **effects** for a given event.
|
|
235
|
+
* Handles both key-based effects (O(1) lookup) and pattern-based effects (runtime matching).
|
|
236
|
+
* Errors are caught and logged.
|
|
237
|
+
*
|
|
238
|
+
* @param event - The event that was reduced.
|
|
239
|
+
* @internal
|
|
240
|
+
*/
|
|
241
|
+
private notifyEffects;
|
|
242
|
+
/**
|
|
243
|
+
* Notifies event subscribers for a specific phase.
|
|
244
|
+
*
|
|
245
|
+
* Calls both phase-specific subscribers and 'all' subscribers.
|
|
246
|
+
* Errors are caught and logged, allowing other subscribers to continue.
|
|
247
|
+
*
|
|
248
|
+
* @param event - The event to notify about.
|
|
249
|
+
* @param phase - The phase ('committed' or 'uncommitted').
|
|
250
|
+
* @internal
|
|
251
|
+
*/
|
|
252
|
+
private notifyEventSubscribers;
|
|
253
|
+
/**
|
|
254
|
+
* Applies a reduced event to a slice and emits **precise** connector events.
|
|
255
|
+
*
|
|
256
|
+
* For each changed **leaf path** (via {@link detectChangedProps}), emits that leaf and
|
|
257
|
+
* all of its **ancestors** once (e.g., `"data"`, `"data.123"`, `"data.123.title"`).
|
|
258
|
+
*
|
|
259
|
+
* **State Immutability**: When a slice changes, a new state object is created via
|
|
260
|
+
* shallow spread: `{ ...this.state, [sliceName]: newSlice }`. This ensures that
|
|
261
|
+
* `this.state` reference changes, enabling efficient change detection via `===`.
|
|
262
|
+
*
|
|
263
|
+
* @param rName - Slice name being updated.
|
|
264
|
+
* @param event - Reduced event with typed payload.
|
|
265
|
+
* @returns `true` if the slice actually changed, `false` otherwise.
|
|
266
|
+
*
|
|
267
|
+
* @internal
|
|
268
|
+
*/
|
|
269
|
+
private forwardEvent;
|
|
270
|
+
/**
|
|
271
|
+
* Returns a structured introspection snapshot for DevTools UIs.
|
|
272
|
+
*
|
|
273
|
+
* @remarks
|
|
274
|
+
* Reads the internal middleware, effects, reducers, and subscriber
|
|
275
|
+
* registries and returns a plain-object summary matching the
|
|
276
|
+
* `STORE_SUBSCRIPTIONS` protocol message shape.
|
|
277
|
+
*
|
|
278
|
+
* @public
|
|
279
|
+
*/
|
|
280
|
+
__devtoolsIntrospect(): {
|
|
281
|
+
reducers: {
|
|
282
|
+
name: string;
|
|
283
|
+
when: When<EM> | undefined;
|
|
284
|
+
}[];
|
|
285
|
+
effects: {
|
|
286
|
+
channel: string;
|
|
287
|
+
type: string;
|
|
288
|
+
name?: string;
|
|
289
|
+
description?: string;
|
|
290
|
+
}[];
|
|
291
|
+
middleware: {
|
|
292
|
+
name?: string;
|
|
293
|
+
description?: string;
|
|
294
|
+
when?: unknown;
|
|
295
|
+
}[];
|
|
296
|
+
atomic: {
|
|
297
|
+
reducer: string;
|
|
298
|
+
property: string;
|
|
299
|
+
}[];
|
|
300
|
+
event: {
|
|
301
|
+
channel: string;
|
|
302
|
+
type: string;
|
|
303
|
+
phase: string;
|
|
304
|
+
}[];
|
|
305
|
+
coarse: number;
|
|
306
|
+
};
|
|
307
|
+
/**
|
|
308
|
+
* Applies an externally provided **whole-state** (e.g., DevTools time travel) and emits
|
|
309
|
+
* fine-grained path changes for each slice.
|
|
310
|
+
*
|
|
311
|
+
* **State Immutability**: If any slices change, a new state object is created via
|
|
312
|
+
* shallow spread. This ensures consistent immutability with {@link forwardEvent}.
|
|
313
|
+
*
|
|
314
|
+
* @param nextPlain - Plain JS object to become the new state.
|
|
315
|
+
*
|
|
316
|
+
* @internal
|
|
317
|
+
*/
|
|
318
|
+
private __applyExternalState;
|
|
319
|
+
/**
|
|
320
|
+
* Replays a sequence of events from a snapshot through reducers and event
|
|
321
|
+
* subscribers ONLY. Skips dedup, middleware, and effects.
|
|
322
|
+
*
|
|
323
|
+
* This method is gated by the `devtools.allowReplay` runtime config.
|
|
324
|
+
* If replay is not enabled, this method throws.
|
|
325
|
+
*
|
|
326
|
+
* @param snapshot - The state snapshot to restore before replaying.
|
|
327
|
+
* @param events - Array of events to replay (in order).
|
|
328
|
+
*
|
|
329
|
+
* @internal
|
|
330
|
+
*/
|
|
331
|
+
__replayEvents(snapshot: any, events: Array<{
|
|
332
|
+
channel: string;
|
|
333
|
+
type: string;
|
|
334
|
+
payload: any;
|
|
335
|
+
id: string;
|
|
336
|
+
}>): void;
|
|
337
|
+
/**
|
|
338
|
+
* Emits a typed event `(channel, type, payload)`.
|
|
339
|
+
* Events are queued and processed **sequentially** (FIFO).
|
|
340
|
+
*
|
|
341
|
+
* **Pipeline per event:**
|
|
342
|
+
* 1. **Deduplication check** - Skip if event ID already processed (React Strict Mode safety)
|
|
343
|
+
* 2. **Middleware** - Pre-reducer hooks; may cancel by returning `false`
|
|
344
|
+
* 3. **Reducers** - Synchronous state updates via internal event bus
|
|
345
|
+
* 4. **Effects** - Async side-effects keyed by `(channel, type)` for O(1) lookup
|
|
346
|
+
* 5. **Coarse subscribers** - External store subscribers (only if state changed)
|
|
347
|
+
*
|
|
348
|
+
* **Change Detection**: Uses reference equality (`===`) on `this.state` to determine
|
|
349
|
+
* if any slice changed. Works because {@link forwardEvent} creates a new state reference
|
|
350
|
+
* via shallow spread when any slice changes.
|
|
351
|
+
*
|
|
352
|
+
* @typeParam C - Channel key in `EM`.
|
|
353
|
+
* @typeParam T - Type key within channel `C`.
|
|
354
|
+
* @param channel - Channel name.
|
|
355
|
+
* @param type - Event type name.
|
|
356
|
+
* @param payload - Payload typed as `EM[C][T]`.
|
|
357
|
+
* @returns A promise that resolves when the event has finished processing.
|
|
358
|
+
*
|
|
359
|
+
* @example Basic usage
|
|
360
|
+
* ```ts
|
|
361
|
+
* await store.emit('ui', 'increment', 1);
|
|
362
|
+
* ```
|
|
363
|
+
*
|
|
364
|
+
* @example With middleware cancellation
|
|
365
|
+
* ```ts
|
|
366
|
+
* store.registerMiddleware((state, event) => {
|
|
367
|
+
* if (event.type === 'dangerous') return false; // cancel
|
|
368
|
+
* return true; // allow
|
|
369
|
+
* });
|
|
370
|
+
*
|
|
371
|
+
* await store.emit('ui', 'dangerous', null); // cancelled, no state change
|
|
372
|
+
* ```
|
|
373
|
+
*
|
|
374
|
+
* @public
|
|
375
|
+
*/
|
|
376
|
+
emit<C extends keyof EM & string, T extends keyof EM[C] & string>(channel: C, type: T, payload: EM[C][T]): Promise<void>;
|
|
377
|
+
/**
|
|
378
|
+
* Connects a **fine-grained** listener to a dotted path under a slice.
|
|
379
|
+
*
|
|
380
|
+
* @param spec - `{ reducer, property }` where `property` is a dotted path (e.g., `"items.0.title"`).
|
|
381
|
+
* Supports wildcards: `*` (one segment) and `**` (zero or more segments).
|
|
382
|
+
* @param h - Handler receiving a {@link Change} with `{ oldValue, newValue, path }`.
|
|
383
|
+
* @returns Unsubscribe function.
|
|
384
|
+
*
|
|
385
|
+
* @example Exact path
|
|
386
|
+
* ```ts
|
|
387
|
+
* const off = store.connect(
|
|
388
|
+
* { reducer: 'todos', property: 'items.0.title' },
|
|
389
|
+
* (chg) => console.log('title changed:', chg.newValue)
|
|
390
|
+
* );
|
|
391
|
+
* off();
|
|
392
|
+
* ```
|
|
393
|
+
*
|
|
394
|
+
* @example Wildcard pattern
|
|
395
|
+
* ```ts
|
|
396
|
+
* // Listen to any item title change
|
|
397
|
+
* const off = store.connect(
|
|
398
|
+
* { reducer: 'todos', property: 'items.*.title' },
|
|
399
|
+
* (chg) => console.log('some title changed')
|
|
400
|
+
* );
|
|
401
|
+
* ```
|
|
402
|
+
*
|
|
403
|
+
* @public
|
|
404
|
+
*/
|
|
405
|
+
connect(spec: {
|
|
406
|
+
reducer: R;
|
|
407
|
+
property: string;
|
|
408
|
+
}, h: (chg: Change) => void): () => void;
|
|
409
|
+
/**
|
|
410
|
+
* Subscribe to events by channel and type.
|
|
411
|
+
*
|
|
412
|
+
* Event subscriptions are intended for the View layer (e.g., React components)
|
|
413
|
+
* to react to events without affecting the event flow. They are fire-and-forget
|
|
414
|
+
* and cannot cancel event propagation.
|
|
415
|
+
*
|
|
416
|
+
* **Phases:**
|
|
417
|
+
* - `'committed'` (default): Events that passed middleware and reached reducers.
|
|
418
|
+
* Notified after reducers, before effects.
|
|
419
|
+
* - `'uncommitted'`: Events rejected by middleware. Notified immediately after rejection.
|
|
420
|
+
* - `'all'`: Both committed and uncommitted events. Handler receives the phase parameter
|
|
421
|
+
* to distinguish between the two.
|
|
422
|
+
*
|
|
423
|
+
* @typeParam C - Channel key within `EM`.
|
|
424
|
+
* @typeParam T - Event type key within channel `C`.
|
|
425
|
+
* @param channel - Channel to subscribe to.
|
|
426
|
+
* @param type - Event type to subscribe to.
|
|
427
|
+
* @param handler - Handler function `(event, getState, emit, phase)`.
|
|
428
|
+
* @param phase - Event phase to subscribe to (default: `'committed'`).
|
|
429
|
+
* @returns Unsubscribe function.
|
|
430
|
+
*
|
|
431
|
+
* @example Committed events (default)
|
|
432
|
+
* ```ts
|
|
433
|
+
* const off = store.onEvent('ui', 'save', (event, getState, emit, phase) => {
|
|
434
|
+
* console.log('Save committed:', event.payload);
|
|
435
|
+
* });
|
|
436
|
+
* off();
|
|
437
|
+
* ```
|
|
438
|
+
*
|
|
439
|
+
* @example Uncommitted (rejected) events
|
|
440
|
+
* ```ts
|
|
441
|
+
* store.onEvent('ui', 'delete', (event, getState, emit, phase) => {
|
|
442
|
+
* console.log('Delete was rejected by middleware');
|
|
443
|
+
* }, 'uncommitted');
|
|
444
|
+
* ```
|
|
445
|
+
*
|
|
446
|
+
* @example All events
|
|
447
|
+
* ```ts
|
|
448
|
+
* store.onEvent('ui', 'action', (event, getState, emit, phase) => {
|
|
449
|
+
* console.log('Action:', phase); // 'committed' or 'uncommitted'
|
|
450
|
+
* }, 'all');
|
|
451
|
+
* ```
|
|
452
|
+
*
|
|
453
|
+
* @public
|
|
454
|
+
*/
|
|
455
|
+
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;
|
|
456
|
+
/**
|
|
457
|
+
* Subscribes to **coarse-grained** commits (called once per successful event, only if state changed).
|
|
458
|
+
*
|
|
459
|
+
* **Use Case**: React's `useSyncExternalStore` or similar external store integrations.
|
|
460
|
+
*
|
|
461
|
+
* @param fn - Listener invoked after reducers/effects have run and state has changed.
|
|
462
|
+
* @returns Unsubscribe function.
|
|
463
|
+
*
|
|
464
|
+
* @example
|
|
465
|
+
* ```ts
|
|
466
|
+
* const off = store.subscribe(() => console.log('state committed'));
|
|
467
|
+
* // Later:
|
|
468
|
+
* off();
|
|
469
|
+
* ```
|
|
470
|
+
*
|
|
471
|
+
* @public
|
|
472
|
+
*/
|
|
473
|
+
subscribe(fn: () => void): () => void;
|
|
474
|
+
/**
|
|
475
|
+
* Returns the current immutable state snapshot.
|
|
476
|
+
*
|
|
477
|
+
* @returns Deep-readonly state object.
|
|
478
|
+
*
|
|
479
|
+
* @example
|
|
480
|
+
* ```ts
|
|
481
|
+
* const state = store.getState();
|
|
482
|
+
* console.log(state.counter.value);
|
|
483
|
+
* ```
|
|
484
|
+
*
|
|
485
|
+
* @public
|
|
486
|
+
*/
|
|
487
|
+
getState(): DeepReadonly<S>;
|
|
488
|
+
/**
|
|
489
|
+
* Registers a middleware (runs **before** reducers).
|
|
490
|
+
*
|
|
491
|
+
* @param mw - Middleware `(state, event, emit) => boolean|Promise<boolean>`.
|
|
492
|
+
* Return `false` to cancel event propagation.
|
|
493
|
+
* @returns Unsubscribe function that removes this middleware.
|
|
494
|
+
*
|
|
495
|
+
* @example Logging middleware
|
|
496
|
+
* ```ts
|
|
497
|
+
* const off = store.registerMiddleware(async (state, event) => {
|
|
498
|
+
* console.log('Event:', event.channel, event.type, event.payload);
|
|
499
|
+
* return true; // allow
|
|
500
|
+
* });
|
|
501
|
+
* off();
|
|
502
|
+
* ```
|
|
503
|
+
*
|
|
504
|
+
* @example Cancellation middleware
|
|
505
|
+
* ```ts
|
|
506
|
+
* store.registerMiddleware((state, event) => {
|
|
507
|
+
* if (event.type === 'forbidden') return false; // cancel
|
|
508
|
+
* return true;
|
|
509
|
+
* });
|
|
510
|
+
* ```
|
|
511
|
+
*
|
|
512
|
+
* @public
|
|
513
|
+
*/
|
|
514
|
+
registerMiddleware(mw: MiddlewareFunction<DeepReadonly<S>, EM>): Unsubscribe;
|
|
515
|
+
/**
|
|
516
|
+
* Dynamically **adds** a named slice reducer at runtime.
|
|
517
|
+
*
|
|
518
|
+
* @param name - New slice name (must not already exist).
|
|
519
|
+
* @param spec - Reducer spec (state, events, reducer).
|
|
520
|
+
* @returns Disposer function that **removes** the slice (and its state).
|
|
521
|
+
*
|
|
522
|
+
* @example
|
|
523
|
+
* ```ts
|
|
524
|
+
* const dispose = store.registerReducer('filters', {
|
|
525
|
+
* state: { q: '' },
|
|
526
|
+
* events: [['ui', 'setQuery']],
|
|
527
|
+
* reducer(s, evt) {
|
|
528
|
+
* return evt.type === 'setQuery' ? { q: evt.payload } : s;
|
|
529
|
+
* }
|
|
530
|
+
* });
|
|
531
|
+
* // Later:
|
|
532
|
+
* dispose();
|
|
533
|
+
* ```
|
|
534
|
+
*
|
|
535
|
+
* @public
|
|
536
|
+
*/
|
|
537
|
+
registerReducer(name: string, spec: ReducerSpec<any, EM>): () => void;
|
|
538
|
+
/**
|
|
539
|
+
* Registers an **effect** (stateless async event consumer) that runs after reducers.
|
|
540
|
+
*
|
|
541
|
+
* Effects are **keyed** by `(channel, type)` for O(1) lookup (no scanning all effects).
|
|
542
|
+
*
|
|
543
|
+
* @param spec - Effect specification with `events` (EventKeys) and `effect` (handler).
|
|
544
|
+
* @returns Unsubscribe function.
|
|
545
|
+
*
|
|
546
|
+
* @example Logging effect
|
|
547
|
+
* ```ts
|
|
548
|
+
* const off = store.registerEffect({
|
|
549
|
+
* events: [['ui', 'increment']],
|
|
550
|
+
* effect: async (evt, getState, emit) => {
|
|
551
|
+
* console.log('increment', evt.payload, getState().counter.value);
|
|
552
|
+
* }
|
|
553
|
+
* });
|
|
554
|
+
* off();
|
|
555
|
+
* ```
|
|
556
|
+
*
|
|
557
|
+
* @example Multi-event effect
|
|
558
|
+
* ```ts
|
|
559
|
+
* store.registerEffect({
|
|
560
|
+
* events: [['ui', 'increment'], ['ui', 'decrement']],
|
|
561
|
+
* effect: async (evt, getState, emit) => {
|
|
562
|
+
* // Runs for both increment and decrement
|
|
563
|
+
* await saveToServer(getState());
|
|
564
|
+
* }
|
|
565
|
+
* });
|
|
566
|
+
* ```
|
|
567
|
+
*
|
|
568
|
+
* @public
|
|
569
|
+
*/
|
|
570
|
+
registerEffect(spec: EffectSpec<DeepReadonly<S>, EM>): () => void;
|
|
571
|
+
/**
|
|
572
|
+
* Convenience helper to register an **effect** filtered by a single `(channel, type)` pair.
|
|
573
|
+
*
|
|
574
|
+
* @typeParam C - Channel key within `EM`.
|
|
575
|
+
* @typeParam T - Event type key within channel `C`.
|
|
576
|
+
* @param channel - Channel to filter.
|
|
577
|
+
* @param type - Event type to filter.
|
|
578
|
+
* @param handler - Effect handler `(payload, getState, emit, event)`.
|
|
579
|
+
* @returns Unsubscribe/teardown function.
|
|
580
|
+
*
|
|
581
|
+
* @example
|
|
582
|
+
* ```ts
|
|
583
|
+
* const off = store.onEffect('ui', 'increment', async (n, get, emit) => {
|
|
584
|
+
* if (n > 10) await emit('ui', 'increment', -10);
|
|
585
|
+
* });
|
|
586
|
+
* // later
|
|
587
|
+
* off();
|
|
588
|
+
* ```
|
|
589
|
+
*
|
|
590
|
+
* @public
|
|
591
|
+
*/
|
|
592
|
+
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>): () => void;
|
|
593
|
+
/**
|
|
594
|
+
* Replaces the **entire** middleware pipeline (HMR-friendly).
|
|
595
|
+
*
|
|
596
|
+
* @param next - New middleware array.
|
|
597
|
+
*
|
|
598
|
+
* @example Hot module replacement
|
|
599
|
+
* ```ts
|
|
600
|
+
* if (import.meta.hot) {
|
|
601
|
+
* import.meta.hot.accept('./middleware', (newModule) => {
|
|
602
|
+
* store.replaceMiddleware(newModule.middleware);
|
|
603
|
+
* });
|
|
604
|
+
* }
|
|
605
|
+
* ```
|
|
606
|
+
*
|
|
607
|
+
* @public
|
|
608
|
+
*/
|
|
609
|
+
replaceMiddleware(next: MiddlewareFunction<DeepReadonly<S>, EM>[]): void;
|
|
610
|
+
/**
|
|
611
|
+
* Replaces all registered **effects** (HMR-friendly).
|
|
612
|
+
*
|
|
613
|
+
* @param next - New effects array (as EffectSpecs).
|
|
614
|
+
*
|
|
615
|
+
* @example Hot module replacement
|
|
616
|
+
* ```ts
|
|
617
|
+
* if (import.meta.hot) {
|
|
618
|
+
* import.meta.hot.accept('./effects', (newModule) => {
|
|
619
|
+
* store.replaceEffects(newModule.effects);
|
|
620
|
+
* });
|
|
621
|
+
* }
|
|
622
|
+
* ```
|
|
623
|
+
*
|
|
624
|
+
* @public
|
|
625
|
+
*/
|
|
626
|
+
replaceEffects(next: Array<EffectSpec<DeepReadonly<S>, EM>>): void;
|
|
627
|
+
/**
|
|
628
|
+
* Replaces the entire **reducer set** (HMR-friendly).
|
|
629
|
+
*
|
|
630
|
+
* @param next - Map of slice specs keyed by slice name.
|
|
631
|
+
* @param opts - `{ preserveState?: boolean }` (default `true`).
|
|
632
|
+
*
|
|
633
|
+
* @example Hot module replacement
|
|
634
|
+
* ```ts
|
|
635
|
+
* if (import.meta.hot) {
|
|
636
|
+
* import.meta.hot.accept('./reducers', (newModule) => {
|
|
637
|
+
* store.replaceReducers(newModule.reducers, { preserveState: true });
|
|
638
|
+
* });
|
|
639
|
+
* }
|
|
640
|
+
* ```
|
|
641
|
+
*
|
|
642
|
+
* @public
|
|
643
|
+
*/
|
|
644
|
+
replaceReducers(next: Record<R, ReducerSpec<S[R], EM>>, opts?: {
|
|
645
|
+
preserveState?: boolean;
|
|
646
|
+
}): void;
|
|
647
|
+
/**
|
|
648
|
+
* Convenience API to replace **any subset** of store parts (HMR patterns).
|
|
649
|
+
*
|
|
650
|
+
* @param partial - Partial replacement set.
|
|
651
|
+
*
|
|
652
|
+
* @example Replace everything
|
|
653
|
+
* ```ts
|
|
654
|
+
* store.hotReplace({
|
|
655
|
+
* reducer: newReducers,
|
|
656
|
+
* middleware: newMiddleware,
|
|
657
|
+
* effects: newEffects,
|
|
658
|
+
* preserveState: true
|
|
659
|
+
* });
|
|
660
|
+
* ```
|
|
661
|
+
*
|
|
662
|
+
* @public
|
|
663
|
+
*/
|
|
664
|
+
hotReplace(partial: {
|
|
665
|
+
reducer?: Record<R, ReducerSpec<S[R], EM>>;
|
|
666
|
+
middleware?: MiddlewareFunction<DeepReadonly<S>, EM>[];
|
|
667
|
+
effects?: Array<EffectSpec<DeepReadonly<S>, EM>>;
|
|
668
|
+
preserveState?: boolean;
|
|
669
|
+
}): void;
|
|
670
|
+
/**
|
|
671
|
+
* Mounts a slice: installs reducer, initializes state (unless preserved),
|
|
672
|
+
* and wires `(channel, type)` listeners on the reducer bus.
|
|
673
|
+
*
|
|
674
|
+
* @param name - Slice name.
|
|
675
|
+
* @param rSpec - Reducer spec (state, events, reducer).
|
|
676
|
+
* @param opts - `{ preserveState: boolean }` whether to keep existing state.
|
|
677
|
+
*
|
|
678
|
+
* @internal
|
|
679
|
+
*/
|
|
680
|
+
private mountSlice;
|
|
681
|
+
/**
|
|
682
|
+
* Unmounts a slice: disposes reducer-bus listeners, removes reducer,
|
|
683
|
+
* and optionally deletes the slice state.
|
|
684
|
+
*
|
|
685
|
+
* @param name - Slice name.
|
|
686
|
+
* @param opts - `{ deleteState: boolean }`.
|
|
687
|
+
*
|
|
688
|
+
* @internal
|
|
689
|
+
*/
|
|
690
|
+
private unmountSlice;
|
|
691
|
+
/**
|
|
692
|
+
* Normalizes event targeting from `when` or legacy `events` to an array of EventKeys.
|
|
693
|
+
*
|
|
694
|
+
* @param spec - Object with optional `when` and/or `events` properties.
|
|
695
|
+
* @returns Array of `[channel, type]` pairs.
|
|
696
|
+
*
|
|
697
|
+
* @internal
|
|
698
|
+
*/
|
|
699
|
+
private normalizeEventKeys;
|
|
700
|
+
/**
|
|
701
|
+
* Reads a dotted path from an object (supports numeric array indices via string keys).
|
|
702
|
+
*
|
|
703
|
+
* @param obj - Root object (slice or value).
|
|
704
|
+
* @param path - Dotted path; leading dot is ignored.
|
|
705
|
+
* @returns The value at the path, or `undefined`.
|
|
706
|
+
*
|
|
707
|
+
* @internal
|
|
708
|
+
*/
|
|
709
|
+
private getAtPath;
|
|
710
|
+
/**
|
|
711
|
+
* Builds ancestor paths for a dotted path.
|
|
712
|
+
*
|
|
713
|
+
* For `"a.b.c"`, returns `["a", "a.b", "a.b.c"]`. Leading dots are trimmed.
|
|
714
|
+
*
|
|
715
|
+
* @param path - Dotted path string.
|
|
716
|
+
* @returns Array of ancestor paths.
|
|
717
|
+
*
|
|
718
|
+
* @example
|
|
719
|
+
* ```ts
|
|
720
|
+
* Store.buildAncestorPaths('x.y.z'); // ['x','x.y','x.y.z']
|
|
721
|
+
* ```
|
|
722
|
+
*
|
|
723
|
+
* @public
|
|
724
|
+
*/
|
|
725
|
+
static buildAncestorPaths(path: string): string[];
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Creates a store with explicit State and EventMap types.
|
|
729
|
+
*
|
|
730
|
+
* Use this overload for:
|
|
731
|
+
* - **Event-only stores** (no reducers, just middleware/effects)
|
|
732
|
+
* - When TypeScript inference from reducers isn't sufficient
|
|
733
|
+
* - When you want to define the EventMap independently of reducers
|
|
734
|
+
*
|
|
735
|
+
* @typeParam S - State record type (can be empty `{}` for event-only stores).
|
|
736
|
+
* @typeParam EM - Event map type defining all `channel → type → payload` combinations.
|
|
737
|
+
* @param cfg - Configuration with `name`, optional `reducer`, optional `middleware`, optional `effects`.
|
|
738
|
+
* @returns A typed {@link StoreInstance}.
|
|
739
|
+
*
|
|
740
|
+
* @example Event-only store
|
|
741
|
+
* ```ts
|
|
742
|
+
* type AppEM = {
|
|
743
|
+
* notifications: { show: { message: string }; hide: void };
|
|
744
|
+
* };
|
|
745
|
+
*
|
|
746
|
+
* const store = createStore<{}, AppEM>({
|
|
747
|
+
* name: 'NotificationBus',
|
|
748
|
+
* effects: [{
|
|
749
|
+
* when: { channel: 'notifications' },
|
|
750
|
+
* effect: (evt) => {
|
|
751
|
+
* if (evt.type === 'show') showToast(evt.payload.message);
|
|
752
|
+
* },
|
|
753
|
+
* }],
|
|
754
|
+
* });
|
|
755
|
+
* ```
|
|
756
|
+
*
|
|
757
|
+
* @example Explicit generics with reducers
|
|
758
|
+
* ```ts
|
|
759
|
+
* const store = createStore<AppState, AppEM>({
|
|
760
|
+
* name: 'App',
|
|
761
|
+
* reducer: { counter: counterSpec },
|
|
762
|
+
* middleware: [loggingMiddleware],
|
|
763
|
+
* });
|
|
764
|
+
* ```
|
|
765
|
+
*
|
|
766
|
+
* @public
|
|
767
|
+
*/
|
|
768
|
+
export declare function createStore<S extends Record<string, any>, EM extends EventMapBase>(cfg: {
|
|
769
|
+
name: string;
|
|
770
|
+
reducer?: {
|
|
771
|
+
[K in keyof S]?: ReducerSpec<S[K], EM>;
|
|
772
|
+
};
|
|
773
|
+
middleware?: MiddlewareFunction<DeepReadonly<S>, EM>[];
|
|
774
|
+
effects?: Array<EffectSpec<DeepReadonly<S>, EM>>;
|
|
775
|
+
dedupWindowMs?: number;
|
|
776
|
+
devtools?: {
|
|
777
|
+
allowReplay?: boolean;
|
|
778
|
+
};
|
|
779
|
+
}): StoreInstance<keyof S & string, S, EM>;
|
|
780
|
+
/**
|
|
781
|
+
* Creates a store with types inferred from the reducers map.
|
|
782
|
+
*
|
|
783
|
+
* This is the primary overload for most use cases where reducers define
|
|
784
|
+
* both the state shape and the event map.
|
|
785
|
+
*
|
|
786
|
+
* @typeParam RM - Reducers map object with each slice's `ReducerSpec`.
|
|
787
|
+
* @param cfg - Configuration with `name`, `reducer`, optional `middleware`, optional `effects`.
|
|
788
|
+
* @returns A typed {@link StoreInstance}.
|
|
789
|
+
*
|
|
790
|
+
* @example
|
|
791
|
+
* ```ts
|
|
792
|
+
* const store = createStore({
|
|
793
|
+
* name: 'App',
|
|
794
|
+
* reducer: {
|
|
795
|
+
* counter: {
|
|
796
|
+
* state: { value: 0 },
|
|
797
|
+
* when: { keys: eventKeys<MyEM>()([['ui', 'increment']]) },
|
|
798
|
+
* reducer: (s, evt) => evt.type === 'increment' ? { value: s.value + evt.payload } : s
|
|
799
|
+
* }
|
|
800
|
+
* },
|
|
801
|
+
* middleware: [],
|
|
802
|
+
* effects: []
|
|
803
|
+
* });
|
|
804
|
+
* ```
|
|
805
|
+
*
|
|
806
|
+
* @public
|
|
807
|
+
*/
|
|
808
|
+
export declare function createStore<RM extends ReducersMapAny>(cfg: {
|
|
809
|
+
name: string;
|
|
810
|
+
reducer: RM;
|
|
811
|
+
middleware?: MiddlewareFunction<DeepReadonly<StateFromReducers<RM>>, EMFromReducersStrict<RM>>[];
|
|
812
|
+
effects?: Array<EffectSpec<DeepReadonly<StateFromReducers<RM>>, EMFromReducersStrict<RM>>>;
|
|
813
|
+
dedupWindowMs?: number;
|
|
814
|
+
devtools?: {
|
|
815
|
+
allowReplay?: boolean;
|
|
816
|
+
};
|
|
817
|
+
}): StoreInstance<keyof RM & string, StateFromReducers<RM>, EMFromReducersStrict<RM>>;
|
|
818
|
+
/**
|
|
819
|
+
* Utility to define **typed** `(channel, events[])` definitions for reducer specs.
|
|
820
|
+
*
|
|
821
|
+
* @typeParam EM - Event map for the store.
|
|
822
|
+
* @param _ - Internal marker parameter (usually `events` array placeholder). Not used at runtime.
|
|
823
|
+
* @returns A helper that, given a `channel` and a readonly `events` array, returns typed event keys.
|
|
824
|
+
*
|
|
825
|
+
* @example
|
|
826
|
+
* ```ts
|
|
827
|
+
* // In a ReducerSpec:
|
|
828
|
+
* const events = typedEvents<EM>([])('ui', ['increment', 'decrement'] as const);
|
|
829
|
+
* // events: ReadonlyArray<EventKey<EM>>
|
|
830
|
+
* ```
|
|
831
|
+
*
|
|
832
|
+
* @public
|
|
833
|
+
*/
|
|
834
|
+
export declare const typedEvents: <EM extends EventMapBase>(_: string[][]) => <C extends keyof EM & string, Evt extends readonly (keyof EM[C] & string)[]>(channel: C, events: Evt) => ReadonlyArray<EventKey<EM>>;
|