@yoltra/core 0.5.0 → 0.7.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/README.es.md +462 -76
- package/README.md +317 -63
- package/dist/types/index.d.ts +6 -1
- package/dist/types/persistence/persist.d.ts +3 -6
- package/dist/types/reducer/Reducer.d.ts +3 -2
- package/dist/types/store/Store.d.ts +239 -56
- package/dist/types/store/call.d.ts +149 -0
- package/dist/types/store/callQueue.d.ts +79 -0
- package/dist/types/store/matching.d.ts +49 -0
- package/dist/types/store/paths.d.ts +39 -0
- package/dist/types/store/performCall.d.ts +15 -0
- package/dist/types/store/rejection.d.ts +58 -0
- package/dist/types/types.d.ts +246 -13
- package/dist/yoltra.cjs +3 -8
- package/dist/yoltra.cjs.map +1 -1
- package/dist/yoltra.mjs +1509 -1038
- package/dist/yoltra.mjs.map +1 -1
- package/dist/yoltra.umd.js +3 -8
- package/dist/yoltra.umd.js.map +1 -1
- package/package.json +12 -11
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { Event, EventMapBase, EventKey, EventUnion, Change, DeepReadonly, EffectSpec, EventMeta, MiddlewareInput, ReducersMapAny, ReducerSpec, StateFromReducers, StoreInstance, StoreSpec, Unsubscribe, EMFromReducersStrict, Emit, EmitOptions, InstrumentationObserver, EventPhase, NarrowedEventHandler, When } from '../types.js';
|
|
1
|
+
import { Event, EventMapBase, EventKey, EventUnion, Change, DeepReadonly, EffectSpec, EventMeta, MiddlewareInput, ReducersMapAny, ReducerSpec, StateFromReducers, StoreInstance, StoreSpec, Unsubscribe, EMFromReducersStrict, Emit, EmitOptions, EmitResult, ConnectOptions, InstrumentationObserver, CascadeInfo, EventPhase, NarrowedEventHandler, When } from '../types.js';
|
|
2
|
+
import { CallHandle, CallOptions } from './call.js';
|
|
3
|
+
import { Rejection } from './rejection.js';
|
|
2
4
|
export declare class Store<EM extends EventMapBase, R extends string, S extends Record<R, any>> implements StoreInstance<R, S, EM> {
|
|
3
5
|
/**
|
|
4
6
|
* Store name (used by DevTools & diagnostics).
|
|
@@ -80,6 +82,18 @@ export declare class Store<EM extends EventMapBase, R extends string, S extends
|
|
|
80
82
|
*
|
|
81
83
|
* @internal
|
|
82
84
|
*/
|
|
85
|
+
/**
|
|
86
|
+
* Subscribers to events that actually changed state, notified after the commit.
|
|
87
|
+
*
|
|
88
|
+
* @remarks
|
|
89
|
+
* Separate from `committedEventSubscribers` rather than a filter over it, because the two
|
|
90
|
+
* answer different questions and one of them is load bearing: `committed` means "not vetoed"
|
|
91
|
+
* and fires for every event a store accepts, including every event in a store with no
|
|
92
|
+
* reducers. Narrowing it would have silently stopped toasts and analytics firing.
|
|
93
|
+
*
|
|
94
|
+
* @internal
|
|
95
|
+
*/
|
|
96
|
+
private readonly writtenEventSubscribers;
|
|
83
97
|
private readonly allEventSubscribers;
|
|
84
98
|
/**
|
|
85
99
|
* Track reducerBus unsubs per slice for HMR/register/unregister.
|
|
@@ -142,6 +156,36 @@ export declare class Store<EM extends EventMapBase, R extends string, S extends
|
|
|
142
156
|
* @internal
|
|
143
157
|
*/
|
|
144
158
|
private isReducing;
|
|
159
|
+
/**
|
|
160
|
+
* The event currently being reduced, or `null` outside the drain.
|
|
161
|
+
*
|
|
162
|
+
* @remarks
|
|
163
|
+
* This is what makes causality exact rather than best-effort. The drain is synchronous — no
|
|
164
|
+
* `await` can interleave — so any `emit` that arrives while it is set is, without ambiguity, a
|
|
165
|
+
* consequence of this event. That catches the case a scoped `emit` closure cannot: a
|
|
166
|
+
* middleware or subscriber that captured the store and calls `store.emit` directly instead of
|
|
167
|
+
* using the injected one. Attribution should not depend on which reference a consumer reached
|
|
168
|
+
* for.
|
|
169
|
+
*
|
|
170
|
+
* @internal
|
|
171
|
+
*/
|
|
172
|
+
private currentEvent;
|
|
173
|
+
/**
|
|
174
|
+
* Events processed by the drain currently in progress. Compared against
|
|
175
|
+
* `maxTransitionsPerDrain`, which is off unless configured.
|
|
176
|
+
*
|
|
177
|
+
* @internal
|
|
178
|
+
*/
|
|
179
|
+
private transitionsThisDrain;
|
|
180
|
+
/**
|
|
181
|
+
* Ceilings that stop a cascade from becoming a hung process. See {@link StoreSpec.maxReduceDepth}.
|
|
182
|
+
*
|
|
183
|
+
* @internal
|
|
184
|
+
*/
|
|
185
|
+
private readonly maxReduceDepth;
|
|
186
|
+
private readonly maxTransitionsPerDrain;
|
|
187
|
+
private readonly onCascade?;
|
|
188
|
+
private readonly onRejected?;
|
|
145
189
|
/**
|
|
146
190
|
* Registered instrumentation observers (DevTools seam). See {@link instrument}.
|
|
147
191
|
*
|
|
@@ -151,11 +195,24 @@ export declare class Store<EM extends EventMapBase, R extends string, S extends
|
|
|
151
195
|
/**
|
|
152
196
|
* Scratch array collecting slice-prefixed changed leaf paths during an
|
|
153
197
|
* instrumented reduce. Set by {@link drainReduce} while observers are active;
|
|
154
|
-
* appended to by {@link
|
|
198
|
+
* appended to by {@link commitStaged}. `null` when not instrumenting.
|
|
155
199
|
*
|
|
156
200
|
* @internal
|
|
157
201
|
*/
|
|
158
202
|
private changedPathSink;
|
|
203
|
+
/**
|
|
204
|
+
* Where keyed reducers put their pending writes during a reduce, and the refusal one of them
|
|
205
|
+
* returned.
|
|
206
|
+
*
|
|
207
|
+
* @remarks
|
|
208
|
+
* Keyed reducers are invoked through `reducerBus`, which delivers to handlers and has no way
|
|
209
|
+
* to hand a value back — the same reason `changedPathSink` exists. `null` outside a reduce.
|
|
210
|
+
*
|
|
211
|
+
* @internal
|
|
212
|
+
*/
|
|
213
|
+
private stagingSink;
|
|
214
|
+
private stagedRejection;
|
|
215
|
+
private stagedRejectedBy;
|
|
159
216
|
/**
|
|
160
217
|
* Count of effect tasks currently in flight; surfaced as queue depth by
|
|
161
218
|
* {@link __devtoolsIntrospect}.
|
|
@@ -269,41 +326,17 @@ export declare class Store<EM extends EventMapBase, R extends string, S extends
|
|
|
269
326
|
*/
|
|
270
327
|
private pruneProcessedEvents;
|
|
271
328
|
/**
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
* @param when - The When matcher (or undefined for "all events").
|
|
275
|
-
* @param event - The event to check.
|
|
276
|
-
* @returns `true` if the event matches, `false` otherwise.
|
|
329
|
+
* Reports a breached ceiling and refuses the emit.
|
|
277
330
|
*
|
|
278
331
|
* @remarks
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
* - `{ channels: ['x', 'y'] }` matches if event's channel is in the array.
|
|
332
|
+
* Console *and* hook, matching how reducer and effect errors are reported: a cascade is a
|
|
333
|
+
* wiring bug, and the console line is what a developer who has not registered a hook will
|
|
334
|
+
* actually see. Without one, refusing the emit would look exactly like the event never having
|
|
335
|
+
* been emitted at all — which is the invisibility this whole guard exists to end.
|
|
284
336
|
*
|
|
285
337
|
* @internal
|
|
286
338
|
*/
|
|
287
|
-
private
|
|
288
|
-
/**
|
|
289
|
-
* Extracts the middleware function from a MiddlewareInput.
|
|
290
|
-
* Handles both raw functions (legacy) and MiddlewareSpec objects.
|
|
291
|
-
*
|
|
292
|
-
* @param input - MiddlewareInput (function or spec).
|
|
293
|
-
* @returns The middleware function.
|
|
294
|
-
*
|
|
295
|
-
* @internal
|
|
296
|
-
*/
|
|
297
|
-
private getMiddlewareFunction;
|
|
298
|
-
/**
|
|
299
|
-
* Gets the `when` matcher from a MiddlewareInput.
|
|
300
|
-
*
|
|
301
|
-
* @param input - MiddlewareInput (function or spec).
|
|
302
|
-
* @returns The `when` matcher, or `undefined` for raw functions (match all).
|
|
303
|
-
*
|
|
304
|
-
* @internal
|
|
305
|
-
*/
|
|
306
|
-
private getMiddlewareWhen;
|
|
339
|
+
private reportCascade;
|
|
307
340
|
/**
|
|
308
341
|
* Invokes all registered **effects** for a given event.
|
|
309
342
|
* Handles both key-based effects (O(1) lookup) and pattern-based effects (runtime matching).
|
|
@@ -313,6 +346,17 @@ export declare class Store<EM extends EventMapBase, R extends string, S extends
|
|
|
313
346
|
* @internal
|
|
314
347
|
*/
|
|
315
348
|
private notifyEffects;
|
|
349
|
+
/**
|
|
350
|
+
* An `emit` that attributes whatever it sends to `cause`.
|
|
351
|
+
*
|
|
352
|
+
* @remarks
|
|
353
|
+
* Built per event rather than per effect: every effect reacting to one event shares a cause,
|
|
354
|
+
* and one closure is cheaper than one per handler on a path that runs for every committed
|
|
355
|
+
* event.
|
|
356
|
+
*
|
|
357
|
+
* @internal
|
|
358
|
+
*/
|
|
359
|
+
private scopedEmit;
|
|
316
360
|
/**
|
|
317
361
|
* Notifies event subscribers for a specific phase.
|
|
318
362
|
*
|
|
@@ -367,17 +411,60 @@ export declare class Store<EM extends EventMapBase, R extends string, S extends
|
|
|
367
411
|
* pattern reducer's throw aborted the commit and notified nobody, not even the uncommitted
|
|
368
412
|
* subscribers a veto would have reached.
|
|
369
413
|
*
|
|
370
|
-
* The semantics are
|
|
414
|
+
* The semantics are the same either way: **the failing slice is isolated.** Its state is
|
|
371
415
|
* unchanged, every other slice still reduces, and the event still commits if anything else
|
|
372
|
-
* changed.
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
416
|
+
* changed.
|
|
417
|
+
*
|
|
418
|
+
* That is deliberately *not* what a {@link Rejected} refusal does, which discards the whole
|
|
419
|
+
* event. A crash and a refusal are different acts: a reducer that throws has a bug and should
|
|
420
|
+
* not be able to veto its neighbours' work, while a reducer that refuses has made a decision
|
|
421
|
+
* and must be able to.
|
|
422
|
+
*
|
|
423
|
+
* This once argued that rolling back was untenable, because subscribers were notified as each
|
|
424
|
+
* slice committed and a later revert would have told them about a value that no longer
|
|
425
|
+
* existed. Staging removed that obstacle — nothing is notified until every slice is written —
|
|
426
|
+
* which is what made refusal possible at all.
|
|
427
|
+
*
|
|
428
|
+
* @internal
|
|
429
|
+
*/
|
|
430
|
+
private stageSliceGuarded;
|
|
431
|
+
/**
|
|
432
|
+
* Runs one slice's reducer and records what it *would* write. Writes nothing.
|
|
433
|
+
*
|
|
434
|
+
* @returns The reducer's {@link Rejection} if it refused, otherwise `null`.
|
|
435
|
+
*
|
|
436
|
+
* @remarks
|
|
437
|
+
* The staging half of the write path. Nothing here touches `this.state` or notifies anybody,
|
|
438
|
+
* which is what lets the event be refused after every reducer has had its say — a decision
|
|
439
|
+
* that has to see the whole diff cannot be made one slice at a time.
|
|
440
|
+
*
|
|
441
|
+
* Freezing happens here rather than at commit because it is where the new value is built, and
|
|
442
|
+
* the freeze is a no-op on anything already frozen; a staged slice that never commits is
|
|
443
|
+
* discarded frozen, which costs nothing and keeps the committed path free of a second walk.
|
|
376
444
|
*
|
|
377
445
|
* @internal
|
|
378
446
|
*/
|
|
379
|
-
private
|
|
380
|
-
|
|
447
|
+
private stageSlice;
|
|
448
|
+
/**
|
|
449
|
+
* Writes every staged slice, then tells the world — in that order.
|
|
450
|
+
*
|
|
451
|
+
* @remarks
|
|
452
|
+
* The commit half. Assigning all slices under a single new root before any notification goes
|
|
453
|
+
* out is what closes the window this used to leave open: notifications fired per slice as each
|
|
454
|
+
* committed, so a subscriber to slice A that read `getState()` could observe slice B of the
|
|
455
|
+
* *same event* not yet applied. In React that window is real, because the atomic hooks use a
|
|
456
|
+
* change as a bare signal and then re-read the whole store.
|
|
457
|
+
*
|
|
458
|
+
* It is also what makes refusal possible at all. The previous code documented rollback as
|
|
459
|
+
* untenable precisely because "an event that reverted afterwards would have already told
|
|
460
|
+
* components about a value that no longer exists" — true when notification and commit were the
|
|
461
|
+
* same step, and no longer true now that they are not.
|
|
462
|
+
*
|
|
463
|
+
* @returns `true` if anything was written.
|
|
464
|
+
*
|
|
465
|
+
* @internal
|
|
466
|
+
*/
|
|
467
|
+
private commitStaged;
|
|
381
468
|
/**
|
|
382
469
|
* Returns a structured introspection snapshot for DevTools UIs.
|
|
383
470
|
*
|
|
@@ -422,7 +509,7 @@ export declare class Store<EM extends EventMapBase, R extends string, S extends
|
|
|
422
509
|
* fine-grained path changes for each slice.
|
|
423
510
|
*
|
|
424
511
|
* **State Immutability**: If any slices change, a new state object is created via
|
|
425
|
-
* shallow spread. This ensures consistent immutability with {@link
|
|
512
|
+
* shallow spread. This ensures consistent immutability with {@link commitStaged}.
|
|
426
513
|
*
|
|
427
514
|
* **Missing slices**: the snapshot should contain every slice. A slice absent
|
|
428
515
|
* from `nextPlain` is **retained at its current value** (not blanked to
|
|
@@ -461,13 +548,13 @@ export declare class Store<EM extends EventMapBase, R extends string, S extends
|
|
|
461
548
|
* phase* (step 5) runs afterwards, asynchronously.
|
|
462
549
|
* 1. **Deduplication** (opt-in) - Skip when content-dedup is enabled (`dedupWindowMs > 0`) or a matching `dedupKey` recurs; off by default
|
|
463
550
|
* 2. **Middleware** (sync) - Pre-reducer hooks; may cancel by returning `false`
|
|
464
|
-
* 3. **Reducers** (sync) -
|
|
465
|
-
* 4. **
|
|
551
|
+
* 3. **Reducers** (sync) - every matching slice is *staged*; nothing is written yet, so a refusal from the last reducer still stops the first one's write
|
|
552
|
+
* 4. **Commit + subscribers** (sync) - all staged slices are assigned under one new root, then event subscribers (`committed`, then `written` when state actually changed), then coarse listeners
|
|
466
553
|
* 5. **Effects** (async) - side-effects keyed by `(channel, type)`; the returned promise resolves once they complete
|
|
467
554
|
*
|
|
468
555
|
* **Change Detection**: Uses reference equality (`===`) on `this.state` to determine
|
|
469
|
-
* if any slice changed. Works because
|
|
470
|
-
*
|
|
556
|
+
* if any slice changed. Works because the commit builds a new state reference via
|
|
557
|
+
* shallow spread when any slice changes.
|
|
471
558
|
*
|
|
472
559
|
* @typeParam C - Channel key in `EM`.
|
|
473
560
|
* @typeParam T - Type key within channel `C`.
|
|
@@ -495,7 +582,20 @@ export declare class Store<EM extends EventMapBase, R extends string, S extends
|
|
|
495
582
|
*
|
|
496
583
|
* @public
|
|
497
584
|
*/
|
|
498
|
-
emit<C extends keyof EM & string, T extends keyof EM[C] & string>(channel: C, type: T, payload: EM[C][T], opts?: EmitOptions): Promise<
|
|
585
|
+
emit<C extends keyof EM & string, T extends keyof EM[C] & string>(channel: C, type: T, payload: EM[C][T], opts?: EmitOptions): Promise<EmitResult>;
|
|
586
|
+
/**
|
|
587
|
+
* The real emit, with an explicitly supplied cause.
|
|
588
|
+
*
|
|
589
|
+
* @remarks
|
|
590
|
+
* Exists so the parent can be passed without a pseudo-private field on the public
|
|
591
|
+
* {@link EmitOptions}. Two callers supply one: the public {@link emit} passes `null` and lets
|
|
592
|
+
* `currentEvent` speak for the synchronous case, and the scoped `emit` handed to effects passes
|
|
593
|
+
* the event that triggered them — effects resume after the drain has ended, so nothing else
|
|
594
|
+
* could still know what caused them.
|
|
595
|
+
*
|
|
596
|
+
* @internal
|
|
597
|
+
*/
|
|
598
|
+
private emitCaused;
|
|
499
599
|
/**
|
|
500
600
|
* Drains the reduce queue **synchronously**. For each event it runs middleware,
|
|
501
601
|
* reducers, event subscribers, and coarse listeners in the same tick, so
|
|
@@ -536,7 +636,7 @@ export declare class Store<EM extends EventMapBase, R extends string, S extends
|
|
|
536
636
|
/**
|
|
537
637
|
* Builds an {@link InstrumentedEvent} from the reduce result and notifies
|
|
538
638
|
* observers. `changedPaths` are the exact slice-prefixed leaf paths recorded
|
|
539
|
-
* by {@link
|
|
639
|
+
* by {@link commitStaged} during this reduce, so DevTools patches need no
|
|
540
640
|
* re-diff.
|
|
541
641
|
*
|
|
542
642
|
* @internal
|
|
@@ -573,7 +673,7 @@ export declare class Store<EM extends EventMapBase, R extends string, S extends
|
|
|
573
673
|
connect(spec: {
|
|
574
674
|
reducer: R;
|
|
575
675
|
property: string;
|
|
576
|
-
}, h: (chg: Change) => void): () => void;
|
|
676
|
+
}, h: (chg: Change) => void, options?: ConnectOptions): () => void;
|
|
577
677
|
/**
|
|
578
678
|
* Subscribe to events by channel and type.
|
|
579
679
|
*
|
|
@@ -743,6 +843,86 @@ export declare class Store<EM extends EventMapBase, R extends string, S extends
|
|
|
743
843
|
*
|
|
744
844
|
* @public
|
|
745
845
|
*/
|
|
846
|
+
/**
|
|
847
|
+
* Sends a request and waits for the reply, correlating the two automatically.
|
|
848
|
+
*
|
|
849
|
+
* @typeParam C - Request channel.
|
|
850
|
+
* @typeParam T - Request type within `C`.
|
|
851
|
+
* @param channel - Channel to send on.
|
|
852
|
+
* @param type - Event type to send.
|
|
853
|
+
* @param payload - The **request** payload. This is what you are sending; what comes back is
|
|
854
|
+
* described by {@link CallOptions.reply}, not by this.
|
|
855
|
+
* @param opts - Which replies end the call, and how long to wait. See {@link CallOptions}.
|
|
856
|
+
* @returns A {@link CallHandle}: `await` it for the terminal reply, or `for await` it for
|
|
857
|
+
* progress events as they arrive.
|
|
858
|
+
*
|
|
859
|
+
* @remarks
|
|
860
|
+
* Every consumer of an event bus eventually writes request/reply by hand — mint an id,
|
|
861
|
+
* subscribe, match, time out, unsubscribe — and every one of them writes the same eighty lines
|
|
862
|
+
* with the same two bugs: the subscription outlives the call, and a responder that forgets to
|
|
863
|
+
* echo the id produces a timeout with nothing to point at. This is that, once.
|
|
864
|
+
*
|
|
865
|
+
* **Correlation is causal.** The store stamps `parentId` on anything emitted while an event is
|
|
866
|
+
* being handled, so a responder that replies through the `emit` it was handed is already
|
|
867
|
+
* correlated. There is no id to mint, echo, or forget:
|
|
868
|
+
*
|
|
869
|
+
* ```ts
|
|
870
|
+
* store.registerEffect({
|
|
871
|
+
* when: { keys: [["rpc", "ask"]] },
|
|
872
|
+
* effect: async (event, _get, emit) => {
|
|
873
|
+
* await emit("rpc", "answer", await lookup(event.payload.q));
|
|
874
|
+
* },
|
|
875
|
+
* });
|
|
876
|
+
* ```
|
|
877
|
+
*
|
|
878
|
+
* **The reply carries its own discriminant.** A call resolves to the *event*, not the payload,
|
|
879
|
+
* because a caller often cannot know which kind of reply it will get:
|
|
880
|
+
*
|
|
881
|
+
* ```ts
|
|
882
|
+
* const res = await store.call("rpc", "ask", { q }, { reply: ["rpc", ["answer", "error"]] });
|
|
883
|
+
* switch (res.type) {
|
|
884
|
+
* case "answer": return res.payload;
|
|
885
|
+
* case "error": throw new Error(res.payload.reason);
|
|
886
|
+
* }
|
|
887
|
+
* ```
|
|
888
|
+
*
|
|
889
|
+
* **Progress streams, with backpressure.** Any correlated event that is not terminal is
|
|
890
|
+
* progress, and iterating the call consumes it. The producer genuinely waits: `emit` resolves
|
|
891
|
+
* only once its effects have run, and the collector is an effect that does not return until the
|
|
892
|
+
* consumer has taken the item. A responder writing `await emit("rpc", "progress", chunk)` is
|
|
893
|
+
* therefore paced by the reader, with nothing buffering without bound.
|
|
894
|
+
*
|
|
895
|
+
* ```ts
|
|
896
|
+
* const call = store.call("job", "start", { id }, {
|
|
897
|
+
* reply: ["job", "done"],
|
|
898
|
+
* highWaterMark: 4,
|
|
899
|
+
* });
|
|
900
|
+
* for await (const step of call) await render(step.payload); // producer waits on this
|
|
901
|
+
* const { payload } = await call;
|
|
902
|
+
* ```
|
|
903
|
+
*
|
|
904
|
+
* Backpressure engages **once you begin iterating**. A call that is only awaited never pulls,
|
|
905
|
+
* so blocking its producer would deadlock the call itself — progress nobody reads would stop
|
|
906
|
+
* the terminal event from ever being sent. Un-iterated progress therefore buffers to
|
|
907
|
+
* `highWaterMark` and is then counted on {@link CallHandle.dropped} rather than blocking.
|
|
908
|
+
*
|
|
909
|
+
* **This is a local primitive.**
|
|
910
|
+
*
|
|
911
|
+
* @example Timeout is idle, not total
|
|
912
|
+
* ```ts
|
|
913
|
+
* // Survives a job that streams for minutes; fails a responder that goes quiet for 5s.
|
|
914
|
+
* await store.call("job", "start", { id }, { reply: ["job", "done"], timeoutMs: 5_000 });
|
|
915
|
+
* ```
|
|
916
|
+
*
|
|
917
|
+
* @example Cancelling
|
|
918
|
+
* ```ts
|
|
919
|
+
* const call = store.call("rpc", "ask", { q }, { reply: ["rpc", "answer"] });
|
|
920
|
+
* useEffect(() => () => call.cancel("unmounted"), [call]);
|
|
921
|
+
* ```
|
|
922
|
+
*
|
|
923
|
+
* @public
|
|
924
|
+
*/
|
|
925
|
+
call<C extends keyof EM & string, T extends keyof EM[C] & string>(channel: C, type: T, payload: EM[C][T], opts: CallOptions<EM>): CallHandle<EventUnion<EM>, EventUnion<EM>>;
|
|
746
926
|
registerEffect(spec: EffectSpec<DeepReadonly<S>, EM>): () => void;
|
|
747
927
|
/**
|
|
748
928
|
* Convenience helper to register an **effect** filtered by a single `(channel, type)` pair.
|
|
@@ -864,15 +1044,6 @@ export declare class Store<EM extends EventMapBase, R extends string, S extends
|
|
|
864
1044
|
* @internal
|
|
865
1045
|
*/
|
|
866
1046
|
private unmountSlice;
|
|
867
|
-
/**
|
|
868
|
-
* Normalizes event targeting from `when` to an array of EventKeys.
|
|
869
|
-
*
|
|
870
|
-
* @param spec - Object with an optional `when` matcher.
|
|
871
|
-
* @returns Array of `[channel, type]` pairs.
|
|
872
|
-
*
|
|
873
|
-
* @internal
|
|
874
|
-
*/
|
|
875
|
-
private normalizeEventKeys;
|
|
876
1047
|
/**
|
|
877
1048
|
* Reads a dotted path from an object (supports numeric array indices via string keys).
|
|
878
1049
|
*
|
|
@@ -880,6 +1051,10 @@ export declare class Store<EM extends EventMapBase, R extends string, S extends
|
|
|
880
1051
|
* @param path - Dotted path; leading dot is ignored.
|
|
881
1052
|
* @returns The value at the path, or `undefined`.
|
|
882
1053
|
*
|
|
1054
|
+
* @remarks
|
|
1055
|
+
* A member rather than a bare import: a test replaces this on the instance to count how many
|
|
1056
|
+
* walks describing a change costs, which only works while the callers go through `this`.
|
|
1057
|
+
*
|
|
883
1058
|
* @internal
|
|
884
1059
|
*/
|
|
885
1060
|
private getAtPath;
|
|
@@ -955,6 +1130,10 @@ export declare function createStore<S extends Record<string, any>, EM extends Ev
|
|
|
955
1130
|
};
|
|
956
1131
|
onEffectError?: (error: unknown, event: EventUnion<EM>) => void;
|
|
957
1132
|
onReducerError?: (error: unknown, event: EventUnion<EM>, slice: string) => void;
|
|
1133
|
+
maxReduceDepth?: number;
|
|
1134
|
+
maxTransitionsPerDrain?: number;
|
|
1135
|
+
onCascade?: (info: CascadeInfo<EM>) => void;
|
|
1136
|
+
onRejected?: (rejection: Rejection, event: EventUnion<EM>, slice: string) => void;
|
|
958
1137
|
}): StoreInstance<keyof S & string, S, EM>;
|
|
959
1138
|
/**
|
|
960
1139
|
* Creates a store with types inferred from the reducers map.
|
|
@@ -996,6 +1175,10 @@ export declare function createStore<RM extends ReducersMapAny>(cfg: {
|
|
|
996
1175
|
};
|
|
997
1176
|
onEffectError?: (error: unknown, event: EventUnion<EMFromReducersStrict<RM>>) => void;
|
|
998
1177
|
onReducerError?: (error: unknown, event: EventUnion<EMFromReducersStrict<RM>>, slice: string) => void;
|
|
1178
|
+
maxReduceDepth?: number;
|
|
1179
|
+
maxTransitionsPerDrain?: number;
|
|
1180
|
+
onCascade?: (info: CascadeInfo<EMFromReducersStrict<RM>>) => void;
|
|
1181
|
+
onRejected?: (rejection: Rejection, event: EventUnion<EMFromReducersStrict<RM>>, slice: string) => void;
|
|
999
1182
|
}): StoreInstance<keyof RM & string, StateFromReducers<RM>, EMFromReducersStrict<RM>>;
|
|
1000
1183
|
/**
|
|
1001
1184
|
* Utility to define **typed** `(channel, events[])` definitions for reducer specs.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { EventMapBase, EventUnion } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Which reply events end a {@link StoreInstance.call | call}, and therefore what it resolves to.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Given as `[channel]` or `[channel, type]` or `[channel, [type, type]]`. The named types are
|
|
7
|
+
* **terminal**: the first one to arrive settles the call. Every other correlated event on that
|
|
8
|
+
* channel is progress.
|
|
9
|
+
*
|
|
10
|
+
* Naming a channel alone makes every event on it terminal, which suits a responder with a single
|
|
11
|
+
* kind of answer. Naming types is what lets a responder stream: `["rpc", ["answer", "error"]]`
|
|
12
|
+
* ends on either, and anything else — `progress`, `partial`, `log` — flows to the consumer.
|
|
13
|
+
*
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
16
|
+
export type ReplySpec<EM extends EventMapBase> = readonly [channel: keyof EM & string] | readonly [channel: keyof EM & string, type: string] | readonly [channel: keyof EM & string, types: readonly string[]];
|
|
17
|
+
/**
|
|
18
|
+
* Options for {@link StoreInstance.call}.
|
|
19
|
+
*
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
export interface CallOptions<EM extends EventMapBase> {
|
|
23
|
+
/** Which reply events end the call. See {@link ReplySpec}. */
|
|
24
|
+
readonly reply: ReplySpec<EM>;
|
|
25
|
+
/**
|
|
26
|
+
* How long the call may sit **idle** before it gives up, in milliseconds.
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* Idle, not total: every correlated event resets it, progress included. A job that streams for
|
|
30
|
+
* two minutes must not fail a thirty-second call, and a total deadline would make the timeout a
|
|
31
|
+
* function of how much work the responder had to do rather than whether it is still alive.
|
|
32
|
+
*
|
|
33
|
+
* For a genuine deadline — "this must be finished by then, however lively" — use
|
|
34
|
+
* {@link CallOptions.signal} with an `AbortSignal.timeout()`.
|
|
35
|
+
*
|
|
36
|
+
* @default 30000
|
|
37
|
+
*/
|
|
38
|
+
readonly timeoutMs?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Aborts the call. The returned promise rejects and the iterator ends.
|
|
41
|
+
*
|
|
42
|
+
* @remarks
|
|
43
|
+
* Unlike `timeoutMs` this is absolute, so it is the right tool for a request deadline, a
|
|
44
|
+
* user-cancelled action, or a component unmounting.
|
|
45
|
+
*/
|
|
46
|
+
readonly signal?: AbortSignal;
|
|
47
|
+
/**
|
|
48
|
+
* How many progress events may buffer before the producer is made to wait.
|
|
49
|
+
*
|
|
50
|
+
* @remarks
|
|
51
|
+
* Only meaningful once the caller is iterating. See {@link StoreInstance.call} for what
|
|
52
|
+
* backpressure means here and when it engages.
|
|
53
|
+
*
|
|
54
|
+
* @default 16
|
|
55
|
+
*/
|
|
56
|
+
readonly highWaterMark?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Correlate on this id instead of on causality.
|
|
59
|
+
*
|
|
60
|
+
* @remarks
|
|
61
|
+
* Causal matching — a reply is correlated because the store stamped it as *caused by* the
|
|
62
|
+
* request — is free and cannot be forged, but only holds in one process. A reply arriving from
|
|
63
|
+
* another node, a worker, or any transport carries no causal link, so for those the responder
|
|
64
|
+
* echoes an id and both sides agree on it here.
|
|
65
|
+
*
|
|
66
|
+
* When set, the id is sent as `meta.correlationId` and a reply matches if it echoes the same
|
|
67
|
+
* value **or** is causally descended. Causality still wins where it applies, so a local
|
|
68
|
+
* responder needs no changes to be compatible with a remote one.
|
|
69
|
+
*/
|
|
70
|
+
readonly correlationId?: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The result of {@link StoreInstance.call}: awaitable for the terminal reply, async-iterable for
|
|
74
|
+
* progress.
|
|
75
|
+
*
|
|
76
|
+
* @typeParam TReply - The terminal reply event.
|
|
77
|
+
* @typeParam TProgress - Non-terminal correlated events.
|
|
78
|
+
*
|
|
79
|
+
* @remarks
|
|
80
|
+
* One object serving both shapes, rather than two functions, because the caller's intent is not
|
|
81
|
+
* known at the call site — the same request may be awaited in one place and streamed in another,
|
|
82
|
+
* and the responder should not have to care which.
|
|
83
|
+
*
|
|
84
|
+
* ```ts
|
|
85
|
+
* // Await the answer, ignore the running commentary.
|
|
86
|
+
* const done = await store.call("rpc", "ask", { q }, { reply: ["rpc", "answer"] });
|
|
87
|
+
*
|
|
88
|
+
* // Or consume the commentary, then take the answer.
|
|
89
|
+
* const call = store.call("rpc", "ask", { q }, { reply: ["rpc", "answer"] });
|
|
90
|
+
* for await (const step of call) render(step.payload);
|
|
91
|
+
* const answer = await call;
|
|
92
|
+
* ```
|
|
93
|
+
*
|
|
94
|
+
* Awaiting the same call twice is safe and yields the same reply; the terminal event is retained.
|
|
95
|
+
*
|
|
96
|
+
* @public
|
|
97
|
+
*/
|
|
98
|
+
export interface CallHandle<TReply, TProgress> extends Promise<TReply>, AsyncIterable<TProgress> {
|
|
99
|
+
/**
|
|
100
|
+
* Progress events discarded because nothing was iterating.
|
|
101
|
+
*
|
|
102
|
+
* @remarks
|
|
103
|
+
* Zero unless the call was awaited without being iterated *and* the responder streamed more
|
|
104
|
+
* than `highWaterMark` events. Non-zero is not an error — it is the honest count of what a
|
|
105
|
+
* caller chose not to read, and is worth logging rather than guessing at.
|
|
106
|
+
*/
|
|
107
|
+
readonly dropped: number;
|
|
108
|
+
/** Stops listening and settles the call. Safe to call more than once. */
|
|
109
|
+
cancel(reason?: string): void;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Raised when a call goes {@link CallOptions.timeoutMs} without a correlated event.
|
|
113
|
+
*
|
|
114
|
+
* @public
|
|
115
|
+
*/
|
|
116
|
+
export declare class CallTimeoutError extends Error {
|
|
117
|
+
readonly channel: string;
|
|
118
|
+
readonly type: string;
|
|
119
|
+
readonly idleMs: number;
|
|
120
|
+
constructor(channel: string, type: string, idleMs: number);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Raised when a call is cancelled, or its {@link CallOptions.signal} aborts.
|
|
124
|
+
*
|
|
125
|
+
* @public
|
|
126
|
+
*/
|
|
127
|
+
export declare class CallAbortedError extends Error {
|
|
128
|
+
constructor(reason: string);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Normalises a {@link ReplySpec} into a channel and a terminal-type test.
|
|
132
|
+
*
|
|
133
|
+
* @internal
|
|
134
|
+
*/
|
|
135
|
+
export declare function parseReply<EM extends EventMapBase>(reply: ReplySpec<EM>): {
|
|
136
|
+
channel: string;
|
|
137
|
+
isTerminal: (type: string) => boolean;
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Whether `event` is a reply to the request identified by `requestId` / `correlationId`.
|
|
141
|
+
*
|
|
142
|
+
* @remarks
|
|
143
|
+
* Causality first: the store stamps `parentId` on anything emitted while handling an event, so a
|
|
144
|
+
* responder that answers through the `emit` it was given is correlated without doing anything.
|
|
145
|
+
* The explicit id is the fallback for replies that crossed a boundary causality cannot.
|
|
146
|
+
*
|
|
147
|
+
* @internal
|
|
148
|
+
*/
|
|
149
|
+
export declare function isReplyTo<EM extends EventMapBase>(event: EventUnion<EM>, requestId: string, correlationId: string | undefined): boolean;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @yoltra/core
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* A bounded hand-off queue between one producer and one consumer, where **the producer waits**.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* This is what makes {@link StoreInstance.call}'s backpressure real rather than decorative. A
|
|
9
|
+
* plain buffer accepts everything and grows; this one hands the producer a promise that does not
|
|
10
|
+
* resolve until the consumer has taken an item. Because the store awaits effects, and `emit`
|
|
11
|
+
* resolves only once its effects have finished, a producer writing
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* await emit("rpc", "progress", chunk);
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* genuinely blocks until the consumer catches up — end to end, through machinery that already
|
|
18
|
+
* existed, with nothing polling and nothing dropped.
|
|
19
|
+
*
|
|
20
|
+
* **Backpressure only engages once the consumer has begun iterating.** Before that, items buffer
|
|
21
|
+
* up to `highWaterMark` and further ones are counted and discarded. That asymmetry is deliberate:
|
|
22
|
+
* a caller that only awaits the terminal reply never pulls, so blocking the producer would
|
|
23
|
+
* deadlock the very call it is feeding — the producer would be waiting to deliver progress
|
|
24
|
+
* nobody will read, and would therefore never emit the terminal event that ends the wait.
|
|
25
|
+
*
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export declare class CallQueue<T> {
|
|
29
|
+
private readonly highWaterMark;
|
|
30
|
+
private readonly buffer;
|
|
31
|
+
/** Consumers parked in `take`, oldest first. */
|
|
32
|
+
private readonly takers;
|
|
33
|
+
/** Producers parked in `put`, each with the item they are waiting to hand over. */
|
|
34
|
+
private readonly putters;
|
|
35
|
+
private consuming;
|
|
36
|
+
/** No more items will be accepted, but what is already here is still owed to the consumer. */
|
|
37
|
+
private ended;
|
|
38
|
+
/** Abandoned: nothing further is owed to anybody. */
|
|
39
|
+
private closed;
|
|
40
|
+
/** Items discarded because nobody was iterating and the buffer was full. */
|
|
41
|
+
private dropped;
|
|
42
|
+
constructor(highWaterMark: number);
|
|
43
|
+
/** How many items were discarded for want of a consumer. */
|
|
44
|
+
get droppedCount(): number;
|
|
45
|
+
/**
|
|
46
|
+
* Marks that a consumer has started pulling. From here on, a full buffer parks the producer
|
|
47
|
+
* rather than dropping.
|
|
48
|
+
*/
|
|
49
|
+
beginConsuming(): void;
|
|
50
|
+
/**
|
|
51
|
+
* Offers an item. The returned promise settles when the item has been taken — or immediately,
|
|
52
|
+
* if it fit in the buffer or was dropped.
|
|
53
|
+
*/
|
|
54
|
+
put(item: T): Promise<void>;
|
|
55
|
+
/** Takes the next item, waiting if none is available. Resolves `done` once closed and drained. */
|
|
56
|
+
take(): Promise<IteratorResult<T>>;
|
|
57
|
+
/**
|
|
58
|
+
* Stops accepting items, but keeps owing the consumer everything already queued.
|
|
59
|
+
*
|
|
60
|
+
* @remarks
|
|
61
|
+
* What the terminal reply does. Closing outright at that moment would throw away progress the
|
|
62
|
+
* responder had already handed over and the consumer had not yet read — which is exactly what
|
|
63
|
+
* happened before this existed: a six-step job delivered five steps, because the sixth was in
|
|
64
|
+
* the buffer when `done` arrived and the buffer was cleared. The terminal event says "no more
|
|
65
|
+
* is coming", not "forget what you were given".
|
|
66
|
+
*/
|
|
67
|
+
end(): void;
|
|
68
|
+
/**
|
|
69
|
+
* Closes the queue: waiting consumers are told `done`, and **every parked producer is
|
|
70
|
+
* released**.
|
|
71
|
+
*
|
|
72
|
+
* @remarks
|
|
73
|
+
* Releasing producers is not tidying up. A producer parked on `put` is a pending `await emit`
|
|
74
|
+
* somewhere; leaving it parked when the call has already settled would hang the responder for
|
|
75
|
+
* good — turning a timed-out call into a wedged process, which is worse than the problem
|
|
76
|
+
* backpressure was added to solve.
|
|
77
|
+
*/
|
|
78
|
+
close(): void;
|
|
79
|
+
}
|