@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
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { EventKey, EventMapBase, EventUnion, MiddlewareFunction, MiddlewareInput, When } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Checks if an event matches a `When` matcher.
|
|
4
|
+
*
|
|
5
|
+
* @param when - The When matcher (or undefined for "all events").
|
|
6
|
+
* @param event - The event to check.
|
|
7
|
+
* @returns `true` if the event matches, `false` otherwise.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* - `undefined` or missing `when` matches ALL events.
|
|
11
|
+
* - `{ any: true }` matches ALL events.
|
|
12
|
+
* - `{ keys: [...] }` matches if event's `[channel, type]` is in the array.
|
|
13
|
+
* - `{ channel: 'x' }` matches if event's channel equals 'x'.
|
|
14
|
+
* - `{ channels: ['x', 'y'] }` matches if event's channel is in the array.
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export declare function matchesWhen<EM extends EventMapBase>(when: When<EM> | undefined, event: EventUnion<EM>): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Extracts the middleware function from a MiddlewareInput.
|
|
21
|
+
* Handles both raw functions (legacy) and MiddlewareSpec objects.
|
|
22
|
+
*
|
|
23
|
+
* @param input - MiddlewareInput (function or spec).
|
|
24
|
+
* @returns The middleware function.
|
|
25
|
+
*
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export declare function getMiddlewareFunction<St, EM extends EventMapBase>(input: MiddlewareInput<St, EM>): MiddlewareFunction<St, EM>;
|
|
29
|
+
/**
|
|
30
|
+
* Gets the `when` matcher from a MiddlewareInput.
|
|
31
|
+
*
|
|
32
|
+
* @param input - MiddlewareInput (function or spec).
|
|
33
|
+
* @returns The `when` matcher, or `undefined` for raw functions (match all).
|
|
34
|
+
*
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
export declare function getMiddlewareWhen<St, EM extends EventMapBase>(input: MiddlewareInput<St, EM>): When<EM> | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Normalizes event targeting from `when` to an array of EventKeys.
|
|
40
|
+
*
|
|
41
|
+
* @param spec - Object with an optional `when` matcher.
|
|
42
|
+
* @returns Array of `[channel, type]` pairs.
|
|
43
|
+
*
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
export declare function normalizeEventKeys<EM extends EventMapBase>(spec: {
|
|
47
|
+
when?: When<EM>;
|
|
48
|
+
events?: ReadonlyArray<EventKey<EM>>;
|
|
49
|
+
}): ReadonlyArray<EventKey<EM>>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading and expanding dotted state paths.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Moved out of `Store.ts` unchanged. Neither function touched an instance field.
|
|
6
|
+
*
|
|
7
|
+
* `Store` still exposes both as members, and deliberately so. `Store.buildAncestorPaths` is
|
|
8
|
+
* public API that appears in the committed reference, and `getAtPath` is replaced on the
|
|
9
|
+
* instance by a test that counts the walks a change description costs, so the internal callers
|
|
10
|
+
* have to keep reaching it through `this`.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Reads a dotted path from an object (supports numeric array indices via string keys).
|
|
16
|
+
*
|
|
17
|
+
* @param obj - Root object (slice or value).
|
|
18
|
+
* @param path - Dotted path; leading dot is ignored.
|
|
19
|
+
* @returns The value at the path, or `undefined`.
|
|
20
|
+
*
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export declare function getAtPath(obj: any, path: string): any;
|
|
24
|
+
/**
|
|
25
|
+
* Builds ancestor paths for a dotted path.
|
|
26
|
+
*
|
|
27
|
+
* For `"a.b.c"`, returns `["a", "a.b", "a.b.c"]`. Leading dots are trimmed.
|
|
28
|
+
*
|
|
29
|
+
* @param path - Dotted path string.
|
|
30
|
+
* @returns Array of ancestor paths.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* buildAncestorPaths('x.y.z'); // ['x','x.y','x.y.z']
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @public
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildAncestorPaths(path: string): string[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { DeepReadonly, EffectSpec, EmitOptions, EmitResult, EventMapBase, EventUnion } from '../types.js';
|
|
2
|
+
import { CallHandle, CallOptions } from './call.js';
|
|
3
|
+
/**
|
|
4
|
+
* What `performCall` needs from the store.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* Three members, named rather than structural over the whole class, because three is few enough
|
|
8
|
+
* that naming them documents the coupling instead of hiding it.
|
|
9
|
+
*/
|
|
10
|
+
export interface CallDeps<St, EM extends EventMapBase> {
|
|
11
|
+
readonly idFactory: () => string;
|
|
12
|
+
readonly registerEffect: (spec: EffectSpec<DeepReadonly<St>, EM>) => () => void;
|
|
13
|
+
readonly emit: <C extends keyof EM & string, T extends keyof EM[C] & string>(channel: C, type: T, payload: EM[C][T], opts?: EmitOptions) => Promise<EmitResult>;
|
|
14
|
+
}
|
|
15
|
+
export declare function performCall<St, EM extends EventMapBase, C extends keyof EM & string, T extends keyof EM[C] & string>(deps: CallDeps<St, EM>, channel: C, type: T, payload: EM[C][T], opts: CallOptions<EM>): CallHandle<EventUnion<EM>, EventUnion<EM>>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @yoltra/core
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Brand identifying a {@link Rejection}.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* `Symbol.for` rather than `Symbol()`, so the brand survives two copies of this package meeting
|
|
9
|
+
* at runtime — a duplicated dependency, a bundle that inlined a second copy, a consumer that
|
|
10
|
+
* pinned an older minor. With a unique symbol the check would silently answer `false` across that boundary and a
|
|
11
|
+
* refusal would read as ordinary state, which is the failure this whole feature exists to end.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
declare const REJECTED: unique symbol;
|
|
16
|
+
/**
|
|
17
|
+
* A reducer's refusal to apply a write, carrying the reason.
|
|
18
|
+
*
|
|
19
|
+
* @remarks
|
|
20
|
+
* Distinct from a reducer returning its state unchanged, which is indistinguishable from "the
|
|
21
|
+
* event did not concern me". A `Rejection` says *this write was considered and declined*, and it
|
|
22
|
+
* says why — which is what a contended store needs and what a lost update otherwise costs.
|
|
23
|
+
*
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
export interface Rejection {
|
|
27
|
+
readonly [REJECTED]: true;
|
|
28
|
+
/** Why the write was refused. Surfaced to the caller and to `onRejected`. */
|
|
29
|
+
readonly reason: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Builds a {@link Rejection} for a reducer to return instead of state.
|
|
33
|
+
*
|
|
34
|
+
* @param reason - Why the write is refused; surfaced verbatim to the caller.
|
|
35
|
+
*
|
|
36
|
+
* @remarks
|
|
37
|
+
* Rejecting is a whole-event act: no slice commits, no change notifications fire, and the
|
|
38
|
+
* caller's `emit` resolves reporting the refusal. A reducer that merely has nothing to do should
|
|
39
|
+
* return its state, not this.
|
|
40
|
+
*
|
|
41
|
+
* @example Compare-and-swap on a contended slice
|
|
42
|
+
* ```ts
|
|
43
|
+
* reducer: (state, event) =>
|
|
44
|
+
* event.payload.expectedVersion === state.version
|
|
45
|
+
* ? { ...state, ...event.payload.patch, version: state.version + 1 }
|
|
46
|
+
* : Rejected(`stale write: expected v${event.payload.expectedVersion}, have v${state.version}`)
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* @public
|
|
50
|
+
*/
|
|
51
|
+
export declare function Rejected(reason: string): Rejection;
|
|
52
|
+
/**
|
|
53
|
+
* Whether a reducer returned a {@link Rejection} rather than state.
|
|
54
|
+
*
|
|
55
|
+
* @public
|
|
56
|
+
*/
|
|
57
|
+
export declare function isRejected(value: unknown): value is Rejection;
|
|
58
|
+
export {};
|
package/dist/types/types.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*/
|
|
1
|
+
import { Rejection } from './store/rejection.js';
|
|
2
|
+
import { CallHandle, CallOptions } from './store/call.js';
|
|
4
3
|
/**
|
|
5
4
|
* A minimal "record of record" constraint for EventMaps.
|
|
6
5
|
*
|
|
@@ -104,6 +103,29 @@ export interface Event<EM extends EventMapBase = EventMapBase, C extends keyof E
|
|
|
104
103
|
* Absent entirely unless {@link EmitOptions.meta} was supplied. See {@link EventMeta}.
|
|
105
104
|
*/
|
|
106
105
|
readonly meta?: EventMeta;
|
|
106
|
+
/**
|
|
107
|
+
* The `id` of the event whose handling caused this one, when there was one.
|
|
108
|
+
*
|
|
109
|
+
* @remarks
|
|
110
|
+
* Absent on a **root** event — one emitted by application code rather than by a middleware,
|
|
111
|
+
* subscriber or effect reacting to another event. Together with {@link Event.depth} this makes
|
|
112
|
+
* a cascade legible after the fact: without it, a runaway chain is a pile of unrelated events
|
|
113
|
+
* with no way to tell which caused which.
|
|
114
|
+
*/
|
|
115
|
+
readonly parentId?: string;
|
|
116
|
+
/**
|
|
117
|
+
* How many events deep in a causal chain this one is. A root event is depth `0`; an event
|
|
118
|
+
* emitted while handling it is `1`, and so on.
|
|
119
|
+
*
|
|
120
|
+
* @remarks
|
|
121
|
+
* Absent on a root event rather than present as `0`, so an event emitted by application code
|
|
122
|
+
* stays byte-identical to one built before causality tracking existed — the same treatment
|
|
123
|
+
* {@link Event.meta} gets, and for the same reason: `Object.keys` and `toStrictEqual` are load
|
|
124
|
+
* bearing in consumer tests.
|
|
125
|
+
*
|
|
126
|
+
* This is the value {@link StoreSpec.maxReduceDepth} bounds.
|
|
127
|
+
*/
|
|
128
|
+
readonly depth?: number;
|
|
107
129
|
}
|
|
108
130
|
/**
|
|
109
131
|
* Generic "old → new" wrapper for fine-grained change notifications.
|
|
@@ -127,6 +149,20 @@ export interface Change<V = any> {
|
|
|
127
149
|
newValue: V;
|
|
128
150
|
/** Dotted path for fine-grained listeners; e.g., "data.items.0.title" */
|
|
129
151
|
path?: string;
|
|
152
|
+
/**
|
|
153
|
+
* The `id` of the event that caused this change.
|
|
154
|
+
*
|
|
155
|
+
* @remarks
|
|
156
|
+
* A change used to be anonymous, so a subscriber that needed to know *why* a value moved had
|
|
157
|
+
* to mirror the cause into state and store it twice. Absent when the change did not come from
|
|
158
|
+
* an event — a DevTools time-travel snapshot, for instance — which is itself the signal that
|
|
159
|
+
* no event caused it.
|
|
160
|
+
*/
|
|
161
|
+
eventId?: string;
|
|
162
|
+
/** Channel of the causing event. Absent for the same reason as {@link Change.eventId}. */
|
|
163
|
+
channel?: string;
|
|
164
|
+
/** Type of the causing event. Absent for the same reason as {@link Change.eventId}. */
|
|
165
|
+
type?: string;
|
|
130
166
|
}
|
|
131
167
|
/**
|
|
132
168
|
* Emit function narrowed to the developer's EventMap.
|
|
@@ -143,6 +179,58 @@ export interface Change<V = any> {
|
|
|
143
179
|
*
|
|
144
180
|
* @public
|
|
145
181
|
*/
|
|
182
|
+
/**
|
|
183
|
+
* What an `emit` resolves to once its effects have run.
|
|
184
|
+
*
|
|
185
|
+
* @remarks
|
|
186
|
+
* `emit` used to resolve to `void`, so a caller could not tell "the reducer applied my write"
|
|
187
|
+
* from "the reducer looked at my write and returned the state unchanged". On a single-writer
|
|
188
|
+
* store that distinction is academic; on a contended one it is a lost update the API could not
|
|
189
|
+
* report.
|
|
190
|
+
*
|
|
191
|
+
* Deliberately does **not** carry the changed paths. Building that list costs a string
|
|
192
|
+
* concatenation per changed path on every emit, and almost no caller reads it — the same reason
|
|
193
|
+
* change notifications are built lazily. Instrumentation already provides them to the observers
|
|
194
|
+
* that do want them.
|
|
195
|
+
*
|
|
196
|
+
* @public
|
|
197
|
+
*/
|
|
198
|
+
export interface EmitResult {
|
|
199
|
+
/**
|
|
200
|
+
* The event was not vetoed by middleware.
|
|
201
|
+
*
|
|
202
|
+
* @remarks
|
|
203
|
+
* Unchanged in meaning, and deliberately not narrowed to "state changed" — an event-only store
|
|
204
|
+
* commits every event and writes nothing, by construction.
|
|
205
|
+
*/
|
|
206
|
+
readonly committed: boolean;
|
|
207
|
+
/** A reducer actually changed state. */
|
|
208
|
+
readonly written: boolean;
|
|
209
|
+
/** Present when a reducer refused the write. See {@link Rejection}. */
|
|
210
|
+
readonly rejected?: Rejection;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Options for {@link StoreInstance.connect}.
|
|
214
|
+
*
|
|
215
|
+
* @public
|
|
216
|
+
*/
|
|
217
|
+
export interface ConnectOptions {
|
|
218
|
+
/**
|
|
219
|
+
* Deliver the current value once, immediately, before any change arrives.
|
|
220
|
+
*
|
|
221
|
+
* @remarks
|
|
222
|
+
* A subscription otherwise starts at "from now on", so a subscriber's first render has to read
|
|
223
|
+
* the path separately — the same path, spelled twice, which is one place for them to drift.
|
|
224
|
+
*
|
|
225
|
+
* The synthetic change has `oldValue: undefined` and no `eventId`, `channel` or `type`: no
|
|
226
|
+
* event caused it, and claiming one would be a lie a subscriber could act on.
|
|
227
|
+
*
|
|
228
|
+
* For a wildcard pattern the "current value" of a match set is not a thing, so the slice root
|
|
229
|
+
* is delivered with `path: ""`. React's hooks do not need this at all — `useSyncExternalStore`
|
|
230
|
+
* already reads a snapshot on mount — so it is aimed at imperative subscribers.
|
|
231
|
+
*/
|
|
232
|
+
readonly immediate?: boolean;
|
|
233
|
+
}
|
|
146
234
|
/**
|
|
147
235
|
* Per-emit options.
|
|
148
236
|
*
|
|
@@ -163,8 +251,8 @@ export interface EmitOptions {
|
|
|
163
251
|
* Use this exact id for the event instead of generating one.
|
|
164
252
|
*
|
|
165
253
|
* @remarks
|
|
166
|
-
* Intended for **idempotent re-emission**: a caller replaying an event from elsewhere (
|
|
167
|
-
*
|
|
254
|
+
* Intended for **idempotent re-emission**: a caller replaying an event from elsewhere (another
|
|
255
|
+
* store, a durable log) can preserve the original id so the same logical event keeps
|
|
168
256
|
* one identity everywhere, which makes it traceable across systems and in DevTools.
|
|
169
257
|
*
|
|
170
258
|
* The store does **not** enforce uniqueness — supplying a duplicate id does not dedupe the
|
|
@@ -195,7 +283,7 @@ export interface EmitOptions {
|
|
|
195
283
|
*/
|
|
196
284
|
skipDedup?: boolean;
|
|
197
285
|
}
|
|
198
|
-
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], opts?: EmitOptions) => Promise<
|
|
286
|
+
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], opts?: EmitOptions) => Promise<EmitResult>;
|
|
199
287
|
/**
|
|
200
288
|
* Basic unsubscribe handle.
|
|
201
289
|
*
|
|
@@ -235,6 +323,15 @@ export interface InstrumentedEvent<EM extends EventMapBase = EventMapBase> {
|
|
|
235
323
|
nextValues: Record<string, unknown>;
|
|
236
324
|
/** Wall-clock milliseconds spent in the synchronous reduce phase for this event. */
|
|
237
325
|
reduceTimeMs: number;
|
|
326
|
+
/**
|
|
327
|
+
* Present when a reducer refused the write, carrying its reason.
|
|
328
|
+
*
|
|
329
|
+
* @remarks
|
|
330
|
+
* Distinct from `committed: false`, which means middleware vetoed the event before any reducer
|
|
331
|
+
* saw it. This is a reducer having considered the write and declined it — the two look
|
|
332
|
+
* identical in state and are entirely different in cause.
|
|
333
|
+
*/
|
|
334
|
+
rejected?: Rejection;
|
|
238
335
|
}
|
|
239
336
|
/**
|
|
240
337
|
* Observer for {@link StoreInstance.instrument}. Called once per emitted event
|
|
@@ -432,7 +529,104 @@ export type StoreSpec<R extends string, S extends Record<R, any>, EM extends Eve
|
|
|
432
529
|
* @param slice - Name of the slice whose reducer threw.
|
|
433
530
|
*/
|
|
434
531
|
onReducerError?: (error: unknown, event: EventUnion<EM>, slice: string) => void;
|
|
532
|
+
/**
|
|
533
|
+
* Maximum causal depth of an event chain before the store refuses to extend it.
|
|
534
|
+
*
|
|
535
|
+
* @remarks
|
|
536
|
+
* An event emitted while handling another is one deeper than its cause. Two reducers wired to
|
|
537
|
+
* each other, or an effect that emits the event its own reducer answers, climb this without
|
|
538
|
+
* bound — and the reduce queue drains synchronously, so in a browser that is a frozen tab with
|
|
539
|
+
* no error and no stack, and on a server a pinned core.
|
|
540
|
+
*
|
|
541
|
+
* **On by default**, because the whole point is that the failure mode does not require
|
|
542
|
+
* configuration to avoid. The default is far past any legitimate chain: an event caused by an
|
|
543
|
+
* event caused by an event is normal, sixty-four deep is a bug. Raise it if an application
|
|
544
|
+
* genuinely nests deeper, or set `Infinity` to opt out entirely and own the consequences.
|
|
545
|
+
*
|
|
546
|
+
* Breaching does not throw — see {@link StoreSpec.onCascade}.
|
|
547
|
+
*
|
|
548
|
+
* @default 64
|
|
549
|
+
*/
|
|
550
|
+
maxReduceDepth?: number;
|
|
551
|
+
/**
|
|
552
|
+
* Maximum number of events one synchronous drain will process before refusing more.
|
|
553
|
+
*
|
|
554
|
+
* @remarks
|
|
555
|
+
* A drain processes one root event plus every event emitted *while it runs* — so this counts a
|
|
556
|
+
* single causal burst, not application traffic. A plain loop is unaffected: `emit` drains to
|
|
557
|
+
* completion before it returns, so `for (const row of rows) store.emit(…)` is a thousand drains
|
|
558
|
+
* of one event each, never one drain of a thousand.
|
|
559
|
+
*
|
|
560
|
+
* **Off by default** because a wide burst is not by itself a bug. One `sync` event whose
|
|
561
|
+
* subscriber fans out to five hundred `upsert`s is a legitimate shape, and a default low enough
|
|
562
|
+
* to catch a runaway would refuse it. Depth is what separates a cascade from a fan-out — a
|
|
563
|
+
* fan-out is wide and shallow, a cascade is narrow and deep — which is why
|
|
564
|
+
* {@link StoreSpec.maxReduceDepth} carries the default and this does not.
|
|
565
|
+
*
|
|
566
|
+
* Set it when a store's bursts are known to be bounded and an unexpectedly wide one is itself
|
|
567
|
+
* the symptom worth catching.
|
|
568
|
+
*
|
|
569
|
+
* @default undefined (no limit)
|
|
570
|
+
*/
|
|
571
|
+
maxTransitionsPerDrain?: number;
|
|
572
|
+
/**
|
|
573
|
+
* Called when a ceiling is breached, instead of throwing.
|
|
574
|
+
*
|
|
575
|
+
* @remarks
|
|
576
|
+
* The offending emit is refused and the chain stops there; everything already committed
|
|
577
|
+
* stands. It does not throw, because the throw would surface in whichever frame happened to be
|
|
578
|
+
* emitting — a subscriber, an effect, a middleware — which is the same species of
|
|
579
|
+
* hard-to-attribute failure the ceiling exists to prevent. A cascade is a wiring bug, and this
|
|
580
|
+
* is where the wiring gets named.
|
|
581
|
+
*
|
|
582
|
+
* @param info - Which ceiling, the event that would have extended the chain, and its causal
|
|
583
|
+
* chain of ids, newest last.
|
|
584
|
+
*/
|
|
585
|
+
onCascade?: (info: CascadeInfo<EM>) => void;
|
|
586
|
+
/**
|
|
587
|
+
* Called when a reducer refuses a write by returning {@link Rejected}.
|
|
588
|
+
*
|
|
589
|
+
* @remarks
|
|
590
|
+
* The caller learns of its own refusal from the `emit` result; this is for everyone else —
|
|
591
|
+
* logging, metrics, alerting on a rate of rejected writes. Shaped as a callback rather than a
|
|
592
|
+
* subscription for the same reason {@link StoreSpec.onReducerError} is: it is a rare global
|
|
593
|
+
* signal, not something several independent parties register and unregister for.
|
|
594
|
+
*
|
|
595
|
+
* A refusal is a normal outcome, not an error. It means a reducer considered the write and
|
|
596
|
+
* declined it — a stale compare-and-swap, an unmet precondition — and the event is rejected
|
|
597
|
+
* whole, so no slice writes.
|
|
598
|
+
*
|
|
599
|
+
* @param rejection - The refusal and its reason.
|
|
600
|
+
* @param event - The event that was refused.
|
|
601
|
+
* @param slice - Name of the slice whose reducer refused.
|
|
602
|
+
*/
|
|
603
|
+
onRejected?: (rejection: Rejection, event: EventUnion<EM>, slice: string) => void;
|
|
435
604
|
};
|
|
605
|
+
/**
|
|
606
|
+
* What {@link StoreSpec.onCascade} receives when a ceiling is breached.
|
|
607
|
+
*
|
|
608
|
+
* @typeParam EM - Event map.
|
|
609
|
+
*
|
|
610
|
+
* @public
|
|
611
|
+
*/
|
|
612
|
+
export interface CascadeInfo<EM extends EventMapBase = EventMapBase> {
|
|
613
|
+
/** Which ceiling was hit. */
|
|
614
|
+
readonly limit: "maxReduceDepth" | "maxTransitionsPerDrain";
|
|
615
|
+
/** The configured value that was exceeded. */
|
|
616
|
+
readonly limitValue: number;
|
|
617
|
+
/** The event that was refused — the one that would have extended the chain. */
|
|
618
|
+
readonly event: EventUnion<EM>;
|
|
619
|
+
/** Causal depth the refused event would have had. */
|
|
620
|
+
readonly depth: number;
|
|
621
|
+
/**
|
|
622
|
+
* Ids from the root of the chain to the refused event's parent, newest last.
|
|
623
|
+
*
|
|
624
|
+
* @remarks
|
|
625
|
+
* Bounded to the most recent entries: a cascade is long by definition, and the useful part is
|
|
626
|
+
* the cycle at the end rather than the thousand identical hops before it.
|
|
627
|
+
*/
|
|
628
|
+
readonly chain: readonly string[];
|
|
629
|
+
}
|
|
436
630
|
/**
|
|
437
631
|
* Public Store surface.
|
|
438
632
|
*
|
|
@@ -474,7 +668,16 @@ export interface StoreInstance<R extends string = string, S extends Record<R, an
|
|
|
474
668
|
connect(spec: {
|
|
475
669
|
reducer: R;
|
|
476
670
|
property: string;
|
|
477
|
-
}, handler: (change: Change) => void): Unsubscribe;
|
|
671
|
+
}, handler: (change: Change) => void, options?: ConnectOptions): Unsubscribe;
|
|
672
|
+
/**
|
|
673
|
+
* Sends a request and waits for the reply, correlating the two automatically.
|
|
674
|
+
*
|
|
675
|
+
* @remarks
|
|
676
|
+
* Awaitable for the terminal reply, async-iterable for progress. See the implementation on
|
|
677
|
+
* {@link Store.call} for the full contract: correlation, backpressure, timeouts, and why it
|
|
678
|
+
* is a local primitive.
|
|
679
|
+
*/
|
|
680
|
+
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>>;
|
|
478
681
|
/**
|
|
479
682
|
* Convenience helper to register an **effect** filtered by a single `(channel, type)` pair.
|
|
480
683
|
*
|
|
@@ -664,7 +867,8 @@ export interface StoreInstance<R extends string = string, S extends Record<R, an
|
|
|
664
867
|
* Use `when` for event targeting (preferred). The `events` property is
|
|
665
868
|
* kept for backward compatibility but `when` is recommended for new code.
|
|
666
869
|
*
|
|
667
|
-
* @example
|
|
870
|
+
* @example
|
|
871
|
+
* Using `when` (recommended)
|
|
668
872
|
* ```ts
|
|
669
873
|
* const counterSpec: ReducerSpec<{ value: number }, MyEM> = {
|
|
670
874
|
* state: { value: 0 },
|
|
@@ -706,7 +910,7 @@ export interface ReducerSpec<S = any, EM extends EventMapBase = EventMapBase> {
|
|
|
706
910
|
*
|
|
707
911
|
* @public
|
|
708
912
|
*/
|
|
709
|
-
export type ReducerFunction<S = any, EM extends EventMapBase = EventMapBase> = (state: S, event: EventUnion<EM>) => S;
|
|
913
|
+
export type ReducerFunction<S = any, EM extends EventMapBase = EventMapBase> = (state: S, event: EventUnion<EM>) => S | Rejection;
|
|
710
914
|
/**
|
|
711
915
|
* Effect specification (stateless async event consumer).
|
|
712
916
|
*
|
|
@@ -719,7 +923,8 @@ export type ReducerFunction<S = any, EM extends EventMapBase = EventMapBase> = (
|
|
|
719
923
|
* - Effects are keyed by event for O(1) lookup (no scanning).
|
|
720
924
|
* - Use `when` for event targeting (preferred over `events`).
|
|
721
925
|
*
|
|
722
|
-
* @example
|
|
926
|
+
* @example
|
|
927
|
+
* Using `when` (recommended)
|
|
723
928
|
* ```ts
|
|
724
929
|
* const logEffect: EffectSpec<AppState, MyEM> = {
|
|
725
930
|
* when: { keys: eventKeys<MyEM>()([['ui', 'increment']]) },
|
|
@@ -1130,11 +1335,39 @@ export type DeepReadonly<T> = T extends (...args: never[]) => unknown ? T : T ex
|
|
|
1130
1335
|
*
|
|
1131
1336
|
* - `'committed'`: Events that passed middleware and reached reducers (default)
|
|
1132
1337
|
* - `'uncommitted'`: Events rejected by middleware
|
|
1338
|
+
* - `'written'`: Events that actually changed state
|
|
1133
1339
|
* - `'all'`: Both committed and uncommitted events
|
|
1134
1340
|
*
|
|
1341
|
+
* @remarks
|
|
1342
|
+
* `'committed'` means **not vetoed**, and always has. It fires for an event that passed
|
|
1343
|
+
* middleware whether or not any reducer wrote anything — including every event in a store with
|
|
1344
|
+
* no reducers at all, which is the shape a notification or analytics bus takes. Toasts,
|
|
1345
|
+
* animations and tracking depend on that, so it is not narrowed.
|
|
1346
|
+
*
|
|
1347
|
+
* `'written'` is the stricter fact, added rather than substituted: state changed. It fires
|
|
1348
|
+
* **after** the commit, so a subscriber reading `getState()` from it sees the new value — which
|
|
1349
|
+
* is what people tend to assume `'committed'` does.
|
|
1350
|
+
*
|
|
1351
|
+
* `'all'` deliberately stays `committed | uncommitted`. Folding `'written'` into it would hand
|
|
1352
|
+
* every existing `'all'` subscriber a second notification per written event and quietly double
|
|
1353
|
+
* their counts.
|
|
1354
|
+
*
|
|
1355
|
+
* @public
|
|
1356
|
+
*/
|
|
1357
|
+
export type EventPhase = "committed" | "uncommitted" | "written" | "all";
|
|
1358
|
+
/**
|
|
1359
|
+
* The phases a handler is actually *told about*.
|
|
1360
|
+
*
|
|
1361
|
+
* @remarks
|
|
1362
|
+
* `'all'` is a subscription selector, not an outcome — nothing is ever delivered "in the all
|
|
1363
|
+
* phase". Naming the difference keeps the two from being conflated in a handler signature, which
|
|
1364
|
+
* is where they were previously spelled out by hand and drifted: adding `'written'` to
|
|
1365
|
+
* {@link EventPhase} left three copies in `@yoltra/react` still claiming a handler could only
|
|
1366
|
+
* ever see two phases, and the build failed on the mismatch.
|
|
1367
|
+
*
|
|
1135
1368
|
* @public
|
|
1136
1369
|
*/
|
|
1137
|
-
export type
|
|
1370
|
+
export type NotifiedPhase = Exclude<EventPhase, "all">;
|
|
1138
1371
|
/**
|
|
1139
1372
|
* Handler function for event subscriptions (receives full event union).
|
|
1140
1373
|
*
|
|
@@ -1163,7 +1396,7 @@ export type EventPhase = "committed" | "uncommitted" | "all";
|
|
|
1163
1396
|
*
|
|
1164
1397
|
* @public
|
|
1165
1398
|
*/
|
|
1166
|
-
export type EventSubscriptionHandler<S = any, EM extends EventMapBase = EventMapBase> = (event: EventUnion<EM>, getState: () => S, emit: Emit<EM>, phase:
|
|
1399
|
+
export type EventSubscriptionHandler<S = any, EM extends EventMapBase = EventMapBase> = (event: EventUnion<EM>, getState: () => S, emit: Emit<EM>, phase: NotifiedPhase) => void | Promise<void>;
|
|
1167
1400
|
/**
|
|
1168
1401
|
* Narrowed event subscription handler for specific `(channel, type)` pairs.
|
|
1169
1402
|
* Provides better type inference when subscribing to a single event type.
|
|
@@ -1188,4 +1421,4 @@ export type EventSubscriptionHandler<S = any, EM extends EventMapBase = EventMap
|
|
|
1188
1421
|
*
|
|
1189
1422
|
* @public
|
|
1190
1423
|
*/
|
|
1191
|
-
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:
|
|
1424
|
+
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: NotifiedPhase) => void | Promise<void>;
|