@yoltra/core 0.7.0 → 0.8.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 +127 -11
- package/README.md +126 -12
- package/dist/types/index.d.ts +2 -1
- package/dist/types/persistence/persist.d.ts +2 -2
- package/dist/types/store/Store.d.ts +368 -14
- package/dist/types/store/fingerprint.d.ts +62 -0
- package/dist/types/types.d.ts +524 -14
- package/dist/yoltra.cjs +2 -2
- package/dist/yoltra.cjs.map +1 -1
- package/dist/yoltra.mjs +1668 -880
- package/dist/yoltra.mjs.map +1 -1
- package/dist/yoltra.umd.js +2 -2
- package/dist/yoltra.umd.js.map +1 -1
- package/package.json +3 -2
package/dist/types/types.d.ts
CHANGED
|
@@ -208,7 +208,41 @@ export interface EmitResult {
|
|
|
208
208
|
readonly written: boolean;
|
|
209
209
|
/** Present when a reducer refused the write. See {@link Rejection}. */
|
|
210
210
|
readonly rejected?: Rejection;
|
|
211
|
+
/**
|
|
212
|
+
* Why the event did not commit. Absent when it did.
|
|
213
|
+
*
|
|
214
|
+
* @remarks
|
|
215
|
+
* `committed: false` used to arrive from three unrelated causes through one shared frozen
|
|
216
|
+
* object, so a caller could not tell a guard refusing an action from a double-click being
|
|
217
|
+
* deduplicated - which want opposite responses. A submit button should show the refusal and
|
|
218
|
+
* say nothing about the duplicate.
|
|
219
|
+
*
|
|
220
|
+
* See {@link EmitResult.vetoedBy} for which middleware refused it.
|
|
221
|
+
*/
|
|
222
|
+
readonly reason?: NotCommittedReason;
|
|
223
|
+
/**
|
|
224
|
+
* The name of the middleware that vetoed, when it declared one through `meta.name`.
|
|
225
|
+
*
|
|
226
|
+
* @remarks
|
|
227
|
+
* A reducer refusal has always named its slice, through `rejectedBy` and `onRejected`. A
|
|
228
|
+
* middleware veto named nobody, so "the event vanished" had no attribution at all. A bare
|
|
229
|
+
* middleware function contributes its own function name; an anonymous one leaves this
|
|
230
|
+
* absent.
|
|
231
|
+
*/
|
|
232
|
+
readonly vetoedBy?: string;
|
|
211
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* Why an event did not commit.
|
|
236
|
+
*
|
|
237
|
+
* @remarks
|
|
238
|
+
* - `vetoed` - middleware returned `false`, or threw.
|
|
239
|
+
* - `deduped` - an identical event was seen inside the dedup window.
|
|
240
|
+
* - `cascade` - the event exceeded `maxReduceDepth` or the per-drain transition ceiling, so
|
|
241
|
+
* the store refused it rather than letting a cycle run away.
|
|
242
|
+
*
|
|
243
|
+
* @public
|
|
244
|
+
*/
|
|
245
|
+
export type NotCommittedReason = "vetoed" | "deduped" | "cascade";
|
|
212
246
|
/**
|
|
213
247
|
* Options for {@link StoreInstance.connect}.
|
|
214
248
|
*
|
|
@@ -439,7 +473,10 @@ export type StoreSpec<R extends string, S extends Record<R, any>, EM extends Eve
|
|
|
439
473
|
/**
|
|
440
474
|
* Middleware chain executed before reducers/effects.
|
|
441
475
|
* Accepts either functions (legacy) or MiddlewareSpec objects (recommended).
|
|
442
|
-
*
|
|
476
|
+
*
|
|
477
|
+
* An event stops propagating only when a middleware returns an explicit `false`, or
|
|
478
|
+
* throws. Returning nothing allows it. Middleware is synchronous: a `Promise` is not
|
|
479
|
+
* `false`, so it cannot veto.
|
|
443
480
|
*/
|
|
444
481
|
middleware?: MiddlewareInput<DeepReadonly<S>, EM>[];
|
|
445
482
|
/**
|
|
@@ -529,6 +566,18 @@ export type StoreSpec<R extends string, S extends Record<R, any>, EM extends Eve
|
|
|
529
566
|
* @param slice - Name of the slice whose reducer threw.
|
|
530
567
|
*/
|
|
531
568
|
onReducerError?: (error: unknown, event: EventUnion<EM>, slice: string) => void;
|
|
569
|
+
/**
|
|
570
|
+
* Called when an `onEvent` subscriber throws, or rejects.
|
|
571
|
+
*
|
|
572
|
+
* @remarks
|
|
573
|
+
* The fourth of a set: reducers, effects, rejections and cascades all had a hook, and event
|
|
574
|
+
* subscribers had `console.error` and nothing else - so an application could not route a
|
|
575
|
+
* failing subscriber to its own error reporting. Subscribers are the seam a decoration is
|
|
576
|
+
* told to use, which makes the gap more visible than it was.
|
|
577
|
+
*
|
|
578
|
+
* A throwing subscriber never stops the others, with or without this hook.
|
|
579
|
+
*/
|
|
580
|
+
onSubscriberError?: (error: unknown, event: EventUnion<EM>, phase: NotifiedPhase) => void;
|
|
532
581
|
/**
|
|
533
582
|
* Maximum causal depth of an event chain before the store refuses to extend it.
|
|
534
583
|
*
|
|
@@ -639,7 +688,7 @@ export interface CascadeInfo<EM extends EventMapBase = EventMapBase> {
|
|
|
639
688
|
*
|
|
640
689
|
* @public
|
|
641
690
|
*/
|
|
642
|
-
export interface StoreInstance<R extends string = string, S extends Record<R, any> = Record<string, any>, EM extends EventMapBase = EventMapBase> {
|
|
691
|
+
export interface StoreInstance<R extends string = string, S extends Record<R, any> = Record<string, any>, EM extends EventMapBase = EventMapBase> extends StoreDecoration<R, S, EM> {
|
|
643
692
|
/**
|
|
644
693
|
* Store name (used by DevTools to identify the instance).
|
|
645
694
|
*/
|
|
@@ -693,15 +742,26 @@ export interface StoreInstance<R extends string = string, S extends Record<R, an
|
|
|
693
742
|
/**
|
|
694
743
|
* Register a post-reducer effect (sees final state). Returns an unsubscribe.
|
|
695
744
|
*/
|
|
696
|
-
registerEffect(spec:
|
|
745
|
+
registerEffect<Spec extends EffectSpec<any, any>>(spec: Spec): Unsubscribe & {
|
|
746
|
+
store: DecoratableStore<R, S, Merge<EM, EMAddOf<Spec>>>;
|
|
747
|
+
dispose(): void;
|
|
748
|
+
};
|
|
697
749
|
/**
|
|
698
750
|
* Dynamically add middleware, in either the function or the spec form.
|
|
699
751
|
*/
|
|
700
|
-
registerMiddleware(mw:
|
|
752
|
+
registerMiddleware<M extends MiddlewareInput<any, any>>(mw: M): Unsubscribe & {
|
|
753
|
+
store: DecoratableStore<R, S, Merge<EM, EMAddOf<M>>>;
|
|
754
|
+
dispose(): void;
|
|
755
|
+
};
|
|
701
756
|
/**
|
|
702
757
|
* Dynamically add/remove a namespaced reducer slice at runtime.
|
|
703
758
|
*/
|
|
704
|
-
registerReducer(name: string, spec: ReducerSpec<any, EM
|
|
759
|
+
registerReducer(name: string, spec: ReducerSpec<any, EM>, options?: {
|
|
760
|
+
owner?: string;
|
|
761
|
+
}): Unsubscribe & {
|
|
762
|
+
store: StoreInstance<string, Record<string, any>, EM>;
|
|
763
|
+
dispose(): void;
|
|
764
|
+
};
|
|
705
765
|
/**
|
|
706
766
|
* Cleanup resources (timers, etc.) when disposing the store.
|
|
707
767
|
* Call this if you're dynamically creating/destroying stores.
|
|
@@ -717,7 +777,10 @@ export interface StoreInstance<R extends string = string, S extends Record<R, an
|
|
|
717
777
|
* **Phases:**
|
|
718
778
|
* - `'committed'` (default): Events that passed middleware and reached reducers
|
|
719
779
|
* - `'uncommitted'`: Events rejected by middleware
|
|
720
|
-
* - `'
|
|
780
|
+
* - `'written'`: Events that actually changed state
|
|
781
|
+
* - `'all'`: Both committed and uncommitted events (handler receives phase parameter).
|
|
782
|
+
* Deliberately not `written` as well: an event that writes is also committed, so folding
|
|
783
|
+
* it in would notify every existing `all` subscriber twice for one event.
|
|
721
784
|
*
|
|
722
785
|
* @typeParam C - Channel key within `EM`.
|
|
723
786
|
* @typeParam T - Event type key within channel `C`.
|
|
@@ -748,19 +811,82 @@ export interface StoreInstance<R extends string = string, S extends Record<R, an
|
|
|
748
811
|
* }, 'all');
|
|
749
812
|
* ```
|
|
750
813
|
*/
|
|
751
|
-
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
|
|
814
|
+
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, options?: {
|
|
815
|
+
/**
|
|
816
|
+
* Also call this handler while devtools is replaying, which it does not by default.
|
|
817
|
+
*
|
|
818
|
+
* @remarks
|
|
819
|
+
* Opt in only for a handler that derives view state purely from the event stream and
|
|
820
|
+
* performs no I/O. A handler that publishes, writes or notifies must stay out: replay
|
|
821
|
+
* is a debugging operation, and a scrub of the timeline should not reach a peer, a
|
|
822
|
+
* socket or an analytics endpoint.
|
|
823
|
+
*/
|
|
824
|
+
duringReplay?: boolean;
|
|
825
|
+
}): Unsubscribe;
|
|
826
|
+
/**
|
|
827
|
+
* Called when the store gains or loses a reducer, middleware or effect.
|
|
828
|
+
*
|
|
829
|
+
* @remarks
|
|
830
|
+
* A push seam, because `__devtoolsIntrospect()` is pull-only: a devtools panel's
|
|
831
|
+
* subscription list goes stale the moment a decoration mounts anything, and a library that
|
|
832
|
+
* needs to react to another library has nothing to wait on.
|
|
833
|
+
*
|
|
834
|
+
* Delivered as an **array, one batch per public call**. `replaceReducers` unmounts and then
|
|
835
|
+
* remounts, so between those steps a slice that is merely being updated does not exist; a
|
|
836
|
+
* per-change observer would see a spurious unmount. `hotReplace` delivers a single batch
|
|
837
|
+
* spanning all three kinds.
|
|
838
|
+
*
|
|
839
|
+
* Observers run **after** the state broadcast, so the view layer has already been told a
|
|
840
|
+
* fact before a library gets to react to it. A registration made *by* an observer is
|
|
841
|
+
* legitimate and is queued rather than delivered re-entrantly: depth-first work,
|
|
842
|
+
* breadth-first notification, so no observer ever sees a half-built topology.
|
|
843
|
+
*
|
|
844
|
+
* Synchronous. A `Promise` returned from an observer is not awaited, and is reported in
|
|
845
|
+
* development, because the store has already moved on by the time it would resolve.
|
|
846
|
+
*
|
|
847
|
+
* **Replay never produces a change.** `__replayEvents` and `__applyExternalState` alter
|
|
848
|
+
* state and never topology, so there is no `duringReplay` option here and none is needed.
|
|
849
|
+
*
|
|
850
|
+
* `dispose()` fires nothing: the store is going away, not being dismantled slice by slice.
|
|
851
|
+
*
|
|
852
|
+
* @param observer - Receives one batch per registration change.
|
|
853
|
+
* @param options - `emitCurrent` synthesizes a `"mounted"` batch for everything already
|
|
854
|
+
* installed, delivered synchronously before this call returns. Spec-time registrations
|
|
855
|
+
* happen inside `createStore`, so a decorator applied afterwards never saw them arrive;
|
|
856
|
+
* this closes that gap without a separate pull API to race against. The synthesized
|
|
857
|
+
* changes carry their **real** origins, never a synthetic marker, because filtering on
|
|
858
|
+
* provenance is the main thing an observer does.
|
|
859
|
+
* @returns Unsubscribe function.
|
|
860
|
+
*/
|
|
861
|
+
onRegistrationChange(observer: RegistrationObserver<EM>, options?: {
|
|
862
|
+
emitCurrent?: boolean;
|
|
863
|
+
}): Unsubscribe;
|
|
864
|
+
/**
|
|
865
|
+
* `true` while devtools is applying a snapshot or replaying events.
|
|
866
|
+
*
|
|
867
|
+
* @remarks
|
|
868
|
+
* For anything that must branch rather than simply skip. Most code needs nothing: replay
|
|
869
|
+
* does not notify event subscribers unless they opted in.
|
|
870
|
+
*
|
|
871
|
+
* A getter, so destructuring it takes a snapshot rather than a live view.
|
|
872
|
+
*/
|
|
873
|
+
readonly isReplaying: boolean;
|
|
752
874
|
/**
|
|
753
875
|
* Replaces the entire middleware pipeline (HMR-friendly).
|
|
754
876
|
*
|
|
755
877
|
* @param next - New middleware array.
|
|
756
878
|
*/
|
|
757
|
-
replaceMiddleware(next:
|
|
879
|
+
replaceMiddleware(next: MiddlewareInput<DeepReadonly<S>, EM>[], opts?: {
|
|
880
|
+
scope?: ReplaceScope;
|
|
881
|
+
}): void;
|
|
758
882
|
/**
|
|
759
883
|
* Replaces all registered effects (HMR-friendly).
|
|
760
884
|
*
|
|
761
885
|
* @param next - New effects array (as EffectSpecs).
|
|
762
886
|
*/
|
|
763
|
-
replaceEffects(next: Array<EffectSpec<DeepReadonly<S>, EM
|
|
887
|
+
replaceEffects(next: Array<EffectSpec<DeepReadonly<S>, EM>>, opts?: {
|
|
888
|
+
scope?: ReplaceScope;
|
|
889
|
+
}): void;
|
|
764
890
|
/**
|
|
765
891
|
* Replaces the entire reducer set (HMR-friendly).
|
|
766
892
|
*
|
|
@@ -769,6 +895,7 @@ export interface StoreInstance<R extends string = string, S extends Record<R, an
|
|
|
769
895
|
*/
|
|
770
896
|
replaceReducers(next: Record<R, ReducerSpec<S[R], EM>>, opts?: {
|
|
771
897
|
preserveState?: boolean;
|
|
898
|
+
scope?: ReplaceScope;
|
|
772
899
|
}): void;
|
|
773
900
|
/**
|
|
774
901
|
* Convenience API to replace any subset of store parts (HMR patterns).
|
|
@@ -780,6 +907,7 @@ export interface StoreInstance<R extends string = string, S extends Record<R, an
|
|
|
780
907
|
middleware?: MiddlewareInput<DeepReadonly<S>, EM>[];
|
|
781
908
|
effects?: Array<EffectSpec<DeepReadonly<S>, EM>>;
|
|
782
909
|
preserveState?: boolean;
|
|
910
|
+
scope?: ReplaceScope;
|
|
783
911
|
}): void;
|
|
784
912
|
/**
|
|
785
913
|
* Replays a sequence of events from a snapshot through reducers and event
|
|
@@ -812,26 +940,32 @@ export interface StoreInstance<R extends string = string, S extends Record<R, an
|
|
|
812
940
|
reducers: Array<{
|
|
813
941
|
name: string;
|
|
814
942
|
when?: unknown;
|
|
943
|
+
origin: Origin;
|
|
944
|
+
owner?: string;
|
|
815
945
|
}>;
|
|
816
946
|
effects: Array<{
|
|
817
947
|
channel: string;
|
|
818
948
|
type: string;
|
|
819
949
|
name?: string;
|
|
820
950
|
description?: string;
|
|
951
|
+
origin: Origin;
|
|
821
952
|
}>;
|
|
822
953
|
middleware: Array<{
|
|
823
954
|
name?: string;
|
|
824
955
|
description?: string;
|
|
825
956
|
when?: unknown;
|
|
957
|
+
origin: Origin;
|
|
826
958
|
}>;
|
|
827
959
|
atomic: Array<{
|
|
828
960
|
reducer: string;
|
|
829
961
|
property: string;
|
|
830
962
|
}>;
|
|
963
|
+
/** `duringReplay` says whether a subscription hears replayed events. */
|
|
831
964
|
event: Array<{
|
|
832
965
|
channel: string;
|
|
833
966
|
type: string;
|
|
834
967
|
phase: string;
|
|
968
|
+
duringReplay: boolean;
|
|
835
969
|
}>;
|
|
836
970
|
coarse: number;
|
|
837
971
|
dedupHits: number;
|
|
@@ -864,8 +998,8 @@ export interface StoreInstance<R extends string = string, S extends Record<R, an
|
|
|
864
998
|
* @typeParam EM - Event map.
|
|
865
999
|
*
|
|
866
1000
|
* @remarks
|
|
867
|
-
* Use `when` for event targeting
|
|
868
|
-
*
|
|
1001
|
+
* Use `when` for event targeting. An earlier `events` array was removed; this remark
|
|
1002
|
+
* outlived it and described a property that no longer exists.
|
|
869
1003
|
*
|
|
870
1004
|
* @example
|
|
871
1005
|
* Using `when` (recommended)
|
|
@@ -975,19 +1109,31 @@ export type EventUnion<EM extends EventMapBase> = {
|
|
|
975
1109
|
}[keyof EM & string];
|
|
976
1110
|
/**
|
|
977
1111
|
* Middleware function: log, guard, or veto an event **synchronously**.
|
|
978
|
-
* Return `true` to continue, `false` to swallow / cancel propagation.
|
|
979
1112
|
*
|
|
980
1113
|
* @remarks
|
|
1114
|
+
* **Only an explicit `false` vetoes.** Returning `true`, or returning nothing at all, allows
|
|
1115
|
+
* the event, so middleware that only logs or measures can simply fall off the end.
|
|
1116
|
+
*
|
|
1117
|
+
* The return type is `boolean | void` rather than `boolean` for that reason: under these
|
|
1118
|
+
* semantics an omitted `return` is correct, so making the compiler demand one would be
|
|
1119
|
+
* wrong. It was `boolean` while any falsy value vetoed, which made a missing `return`
|
|
1120
|
+
* silently swallow every event the middleware matched.
|
|
1121
|
+
*
|
|
981
1122
|
* Middleware runs in the synchronous reduce phase (so `getState()` is correct
|
|
982
1123
|
* immediately after `emit()`), and therefore must be synchronous. Perform async
|
|
983
|
-
* work in effects instead
|
|
1124
|
+
* work in effects instead - a `Promise` is not `false`, so an async middleware allows the
|
|
1125
|
+
* event while it is still deciding, and the store logs an error in development when it sees
|
|
1126
|
+
* one returned.
|
|
1127
|
+
*
|
|
1128
|
+
* A middleware that **throws** vetoes the event and logs, naming the event: a guard that
|
|
1129
|
+
* crashed has not decided the event is safe.
|
|
984
1130
|
*
|
|
985
1131
|
* @typeParam S - Store state (readonly).
|
|
986
1132
|
* @typeParam EM - Event map.
|
|
987
1133
|
*
|
|
988
1134
|
* @public
|
|
989
1135
|
*/
|
|
990
|
-
export type MiddlewareFunction<S = any, EM extends EventMapBase = EventMapBase> = (state: S, event: EventUnion<EM>, emit: Emit<EM>) => boolean;
|
|
1136
|
+
export type MiddlewareFunction<S = any, EM extends EventMapBase = EventMapBase> = (state: S, event: EventUnion<EM>, emit: Emit<EM>) => boolean | void;
|
|
991
1137
|
/**
|
|
992
1138
|
* Middleware specification with optional event targeting and metadata.
|
|
993
1139
|
*
|
|
@@ -1397,6 +1543,100 @@ export type NotifiedPhase = Exclude<EventPhase, "all">;
|
|
|
1397
1543
|
* @public
|
|
1398
1544
|
*/
|
|
1399
1545
|
export type EventSubscriptionHandler<S = any, EM extends EventMapBase = EventMapBase> = (event: EventUnion<EM>, getState: () => S, emit: Emit<EM>, phase: NotifiedPhase) => void | Promise<void>;
|
|
1546
|
+
/**
|
|
1547
|
+
* One change to a store's registrations.
|
|
1548
|
+
*
|
|
1549
|
+
* @remarks
|
|
1550
|
+
* Self-sufficient on purpose: an observer should never need a follow-up
|
|
1551
|
+
* `__devtoolsIntrospect()` call to act on what it was told.
|
|
1552
|
+
*
|
|
1553
|
+
* @public
|
|
1554
|
+
*/
|
|
1555
|
+
export interface RegistrationChange<EM extends EventMapBase = EventMapBase> {
|
|
1556
|
+
readonly kind: "reducer" | "middleware" | "effect";
|
|
1557
|
+
readonly op: "mounted" | "unmounted";
|
|
1558
|
+
/** Slice name for a reducer; `meta.name` for middleware and effects; absent when unnamed. */
|
|
1559
|
+
readonly name?: string;
|
|
1560
|
+
readonly origin: Origin;
|
|
1561
|
+
/** Introspection only, and only ever what a library passed. */
|
|
1562
|
+
readonly owner?: string;
|
|
1563
|
+
readonly description?: string;
|
|
1564
|
+
/**
|
|
1565
|
+
* The **normalized** matcher, as `matchesWhen` will actually use it.
|
|
1566
|
+
*
|
|
1567
|
+
* @remarks
|
|
1568
|
+
* Not the raw spec's `when`. `registerEffect` normalizes three ways, including turning no
|
|
1569
|
+
* targeting at all into `{ any: true }`, so handing back the raw form would describe
|
|
1570
|
+
* something other than what will fire.
|
|
1571
|
+
*/
|
|
1572
|
+
readonly when?: When<EM>;
|
|
1573
|
+
/**
|
|
1574
|
+
* Reducers only: what happened to the slice's state.
|
|
1575
|
+
*
|
|
1576
|
+
* @remarks
|
|
1577
|
+
* Four values, and the fourth is the one that matters. `replaceReducers` updates an
|
|
1578
|
+
* existing slice by unmounting it with its state intact and remounting, so an observer
|
|
1579
|
+
* treating every `"unmounted"` as destruction would tear down a subscription it is about
|
|
1580
|
+
* to need. `"retained"` says the state survived; `"deleted"` says it did not.
|
|
1581
|
+
*/
|
|
1582
|
+
readonly state?: "initialized" | "preserved" | "deleted" | "retained";
|
|
1583
|
+
/** Whether this registration is dispatched by key (O(1)) or by runtime matching. */
|
|
1584
|
+
readonly dispatch?: "keyed" | "pattern";
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Observer for {@link StoreInstance.onRegistrationChange}.
|
|
1588
|
+
*
|
|
1589
|
+
* @public
|
|
1590
|
+
*/
|
|
1591
|
+
export type RegistrationObserver<EM extends EventMapBase = EventMapBase> = (changes: readonly RegistrationChange<EM>[]) => void;
|
|
1592
|
+
/**
|
|
1593
|
+
* Where a registration came from.
|
|
1594
|
+
*
|
|
1595
|
+
* @remarks
|
|
1596
|
+
* The distinction already existed in the API surface and simply was not honoured. `replace*`
|
|
1597
|
+
* exists to replace *what the application authored*; a registration a library made through
|
|
1598
|
+
* `registerReducer` / `registerMiddleware` / `registerEffect` after construction was never in
|
|
1599
|
+
* that set, and no caller of `replaceReducers(myReducers)` means "and also delete the slice
|
|
1600
|
+
* devtools or a decoration mounted".
|
|
1601
|
+
*
|
|
1602
|
+
* - `spec` - supplied to `createStore`, or installed by a `replace*` call.
|
|
1603
|
+
* - `dynamic` - registered after construction, which is the only way to decorate a store
|
|
1604
|
+
* that already exists.
|
|
1605
|
+
* - `internal` - the store's own machinery, currently the reply listener behind
|
|
1606
|
+
* `store.call()`. Preserved even under `{ scope: "all" }`, because a test harness resetting
|
|
1607
|
+
* a store between cases never means "and abandon the call that is in flight".
|
|
1608
|
+
*
|
|
1609
|
+
* Recorded internally. No public signature takes it, and **no library declares it**: getting
|
|
1610
|
+
* this right must not depend on anyone remembering to pass a string.
|
|
1611
|
+
*
|
|
1612
|
+
* @public
|
|
1613
|
+
*/
|
|
1614
|
+
export type Origin = "spec" | "dynamic" | "internal";
|
|
1615
|
+
/**
|
|
1616
|
+
* Which registrations a `replace*` call is allowed to remove.
|
|
1617
|
+
*
|
|
1618
|
+
* @remarks
|
|
1619
|
+
* `"spec"` is the default and replaces only what the application authored. `"all"` restores
|
|
1620
|
+
* the pre-0.8.0 behaviour exactly, for a caller that genuinely wants it, such as a test
|
|
1621
|
+
* harness resetting a store between cases. `internal` registrations survive both.
|
|
1622
|
+
*
|
|
1623
|
+
* @public
|
|
1624
|
+
*/
|
|
1625
|
+
export type ReplaceScope = "spec" | "all";
|
|
1626
|
+
/**
|
|
1627
|
+
* One `onEvent` subscription: the handler plus whether it asked to hear replayed events.
|
|
1628
|
+
*
|
|
1629
|
+
* @remarks
|
|
1630
|
+
* An entry per subscription rather than the bare handler, for two reasons. It is where the
|
|
1631
|
+
* replay opt-in lives; and it gives each subscription its own identity, so two subscriptions
|
|
1632
|
+
* sharing one handler function are two Set members and disposing one no longer removes both.
|
|
1633
|
+
*
|
|
1634
|
+
* @internal
|
|
1635
|
+
*/
|
|
1636
|
+
export interface EventSubscriberEntry<S, EM extends EventMapBase> {
|
|
1637
|
+
readonly handler: EventSubscriptionHandler<S, EM>;
|
|
1638
|
+
readonly duringReplay: boolean;
|
|
1639
|
+
}
|
|
1400
1640
|
/**
|
|
1401
1641
|
* Narrowed event subscription handler for specific `(channel, type)` pairs.
|
|
1402
1642
|
* Provides better type inference when subscribing to a single event type.
|
|
@@ -1422,3 +1662,273 @@ export type EventSubscriptionHandler<S = any, EM extends EventMapBase = EventMap
|
|
|
1422
1662
|
* @public
|
|
1423
1663
|
*/
|
|
1424
1664
|
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>;
|
|
1665
|
+
/**
|
|
1666
|
+
* Flattens an intersection into a single object type.
|
|
1667
|
+
*
|
|
1668
|
+
* @remarks
|
|
1669
|
+
* Chaining decorations produces `S & Record<"a", A> & Record<"b", B>`, which is correct but
|
|
1670
|
+
* displays as an intersection in every hover and error message. This collapses it.
|
|
1671
|
+
*
|
|
1672
|
+
* Apply it at the **top level only**. It is a homomorphic mapped type, so running it over a
|
|
1673
|
+
* slice whose state *is* a `Map`, `Set` or `Date` destroys that type - the same failure
|
|
1674
|
+
* {@link DeepReadonly} handles the built-ins explicitly to avoid.
|
|
1675
|
+
*
|
|
1676
|
+
* @public
|
|
1677
|
+
*/
|
|
1678
|
+
export type Prettify<T> = {
|
|
1679
|
+
[K in keyof T]: T[K];
|
|
1680
|
+
} & {};
|
|
1681
|
+
/**
|
|
1682
|
+
* Merges `B` into `A`, flattening the result. An empty `B` leaves `A` untouched, so a
|
|
1683
|
+
* decoration that adds no events costs nothing at the type level.
|
|
1684
|
+
*
|
|
1685
|
+
* @public
|
|
1686
|
+
*/
|
|
1687
|
+
export type Merge<A, B> = [keyof B] extends [never] ? A : Prettify<A & B>;
|
|
1688
|
+
/**
|
|
1689
|
+
* The slice-name union after adding `N`.
|
|
1690
|
+
*
|
|
1691
|
+
* @remarks
|
|
1692
|
+
* The `string extends N` guard is load-bearing. Passing a `string`-typed variable rather than
|
|
1693
|
+
* a literal would otherwise widen the union to `string`, and every `S[R1]` lookup downstream
|
|
1694
|
+
* would resolve to the union of every slice's state - silently destroying `useAtomicProp`
|
|
1695
|
+
* inference across the whole application. Degrading to "no widening" is the safe failure.
|
|
1696
|
+
*
|
|
1697
|
+
* @public
|
|
1698
|
+
*/
|
|
1699
|
+
export type WidenNames<R extends string, N extends string> = string extends N ? R : R | N;
|
|
1700
|
+
/**
|
|
1701
|
+
* The state record after adding slice `N` with state `St`. Degrades to `S` when `N` is not a
|
|
1702
|
+
* string literal, for the reason given on {@link WidenNames}.
|
|
1703
|
+
*
|
|
1704
|
+
* @public
|
|
1705
|
+
*/
|
|
1706
|
+
export type WidenState<S, N extends string, St> = string extends N ? S : Prettify<S & Record<N, St>>;
|
|
1707
|
+
/**
|
|
1708
|
+
* Phantom carrier for the event map a spec contributes.
|
|
1709
|
+
*
|
|
1710
|
+
* @remarks
|
|
1711
|
+
* `EMAdd` cannot be inferred from a spec's `when`: `{ keys: [["chan", "evt"]] }` carries
|
|
1712
|
+
* channel and type strings and no payload types, so there is nothing to infer a map from. And
|
|
1713
|
+
* TypeScript has no partial type-argument inference, so a `registerSlice<N, St, EMAdd>` would
|
|
1714
|
+
* force a caller who names `EMAdd` to hand-write `N` and `St` too.
|
|
1715
|
+
*
|
|
1716
|
+
* The way out is to put `EMAdd` in a **value** position, where inference works. The builders
|
|
1717
|
+
* ({@link defineSlice}, {@link defineMiddleware}, {@link defineEffect}) brand a spec with this
|
|
1718
|
+
* interface, and the register methods read it back with {@link EMAddOf}. Nothing exists at
|
|
1719
|
+
* runtime; the property is never assigned.
|
|
1720
|
+
*
|
|
1721
|
+
* The property is **required, not optional**: an optional one makes
|
|
1722
|
+
* `X extends EventMapCarrier<infer E>` match every object and infer `unknown`. And it is a
|
|
1723
|
+
* *function* type so `EMAdd` sits in both co- and contravariant position, which keeps the
|
|
1724
|
+
* inference exact rather than widening to a supertype.
|
|
1725
|
+
*
|
|
1726
|
+
* @public
|
|
1727
|
+
*/
|
|
1728
|
+
export interface EventMapCarrier<EMAdd extends EventMapBase> {
|
|
1729
|
+
/** Phantom. Never present at runtime, and never read. */
|
|
1730
|
+
readonly "~yoltraEventMap": (em: EMAdd) => EMAdd;
|
|
1731
|
+
}
|
|
1732
|
+
/**
|
|
1733
|
+
* Reads the event map a spec contributes, or `{}` when it declares none.
|
|
1734
|
+
*
|
|
1735
|
+
* @remarks
|
|
1736
|
+
* Only a branded spec widens the event map. An unbranded object literal contributes `{}`,
|
|
1737
|
+
* which is today's behaviour and therefore always safe.
|
|
1738
|
+
*
|
|
1739
|
+
* @public
|
|
1740
|
+
*/
|
|
1741
|
+
export type EMAddOf<X> = X extends {
|
|
1742
|
+
readonly "~yoltraEventMap": (em: infer E) => unknown;
|
|
1743
|
+
} ? E extends EventMapBase ? E : EmptyEventMap : EmptyEventMap;
|
|
1744
|
+
/**
|
|
1745
|
+
* The event map a spec contributes when it declares none.
|
|
1746
|
+
*
|
|
1747
|
+
* @remarks
|
|
1748
|
+
* `Record<never, never>` rather than `{}`: the bare empty-object type accepts any non-nullish
|
|
1749
|
+
* value, including `0` and `""`, so it would let nonsense through {@link Merge}. This has no
|
|
1750
|
+
* keys, which is the actual claim being made, and {@link Merge} short-circuits on it.
|
|
1751
|
+
*
|
|
1752
|
+
* @public
|
|
1753
|
+
*/
|
|
1754
|
+
export type EmptyEventMap = Record<never, never>;
|
|
1755
|
+
/**
|
|
1756
|
+
* Reads a reducer spec's state type.
|
|
1757
|
+
*
|
|
1758
|
+
* @public
|
|
1759
|
+
*/
|
|
1760
|
+
export type StateOfSpec<X> = X extends ReducerSpec<infer St, any> ? St : never;
|
|
1761
|
+
/**
|
|
1762
|
+
* What a decoration contributes to a store: some slices, some events, either possibly empty.
|
|
1763
|
+
*
|
|
1764
|
+
* @remarks
|
|
1765
|
+
* Phantom. Never constructed, and never present at runtime; it exists so a library can state
|
|
1766
|
+
* its contribution once and have {@link Decorated} and {@link StoreDecorator} read it back.
|
|
1767
|
+
*
|
|
1768
|
+
* @example
|
|
1769
|
+
* ```ts
|
|
1770
|
+
* type TransfersDecoration = Decoration<{ transfers: TransferState }, TransfersEM>;
|
|
1771
|
+
* ```
|
|
1772
|
+
*
|
|
1773
|
+
* @public
|
|
1774
|
+
*/
|
|
1775
|
+
export interface Decoration<AddS extends Record<string, any> = Record<never, never>, AddEM extends EventMapBase = EmptyEventMap> {
|
|
1776
|
+
readonly slices: AddS;
|
|
1777
|
+
readonly events: AddEM;
|
|
1778
|
+
}
|
|
1779
|
+
/**
|
|
1780
|
+
* The store type that results from applying a {@link Decoration}.
|
|
1781
|
+
*
|
|
1782
|
+
* @public
|
|
1783
|
+
*/
|
|
1784
|
+
export type Decorated<R extends string, S extends Record<R, any>, EM extends EventMapBase, D> = D extends Decoration<infer AddS, infer AddEM> ? StoreInstance<WidenNames<R, keyof AddS & string>, SatisfiesSlices<Prettify<S & AddS>, WidenNames<R, keyof AddS & string>>, Merge<EM, AddEM>> : never;
|
|
1785
|
+
/**
|
|
1786
|
+
* The shape a `withX(store, config)` decorator conforms to, with `config` curried away.
|
|
1787
|
+
*
|
|
1788
|
+
* @remarks
|
|
1789
|
+
* **Generic over the incoming store on purpose**, and that is what makes composition work
|
|
1790
|
+
* rather than a variance rule. `R`, `S` and `EM` are inference sites, so at each call in a
|
|
1791
|
+
* nest TypeScript instantiates them from whatever the argument actually is: an EM-only
|
|
1792
|
+
* decorator nested inside one that also adds a slice infers the already-widened `R` and `S`
|
|
1793
|
+
* and carries them through untouched. Either order composes, and nothing is lost.
|
|
1794
|
+
*
|
|
1795
|
+
* Nesting is the composition mechanism; there is no `pipe`. Every decorator takes
|
|
1796
|
+
* `(store, config)`, so each step in a pipe needs a lambda to become unary, which makes
|
|
1797
|
+
* `pipe(store, s => withA(s, cfgA), s => withB(s, cfgB))` **longer** than
|
|
1798
|
+
* `withB(withA(store, cfgA), cfgB)`. A pipe only pays for curried decorators, which would be
|
|
1799
|
+
* a different convention from the one `withDevtools` already set.
|
|
1800
|
+
*
|
|
1801
|
+
* A dependency on another decoration needs no registry either: constrain the input.
|
|
1802
|
+
* `EM extends EventMapBase & RequiredEM` fails at the call site naming the channels that are
|
|
1803
|
+
* missing, and still composes, because TypeScript infers `EM` and then checks the constraint.
|
|
1804
|
+
*
|
|
1805
|
+
* @example
|
|
1806
|
+
* ```ts
|
|
1807
|
+
* export function withTransfers<
|
|
1808
|
+
* R extends string,
|
|
1809
|
+
* S extends Record<R, any>,
|
|
1810
|
+
* EM extends EventMapBase,
|
|
1811
|
+
* >(store: StoreInstance<R, S, EM>, config: TransfersConfig) {
|
|
1812
|
+
* return store.withSlice("transfers", defineSlice<TransfersEM>()({ ... }), {
|
|
1813
|
+
* owner: "@scope/transfers",
|
|
1814
|
+
* });
|
|
1815
|
+
* }
|
|
1816
|
+
* ```
|
|
1817
|
+
*
|
|
1818
|
+
* @public
|
|
1819
|
+
*/
|
|
1820
|
+
export type StoreDecorator<D extends Decoration<any, any>> = <R extends string, S extends Record<R, any>, EM extends EventMapBase>(store: StoreInstance<R, S, EM>) => Decorated<R, S, EM, D>;
|
|
1821
|
+
/**
|
|
1822
|
+
* A store that can be decorated, and whose type grows as it is.
|
|
1823
|
+
*
|
|
1824
|
+
* @public
|
|
1825
|
+
*/
|
|
1826
|
+
export type DecoratableStore<R extends string, S extends Record<R, any>, EM extends EventMapBase> = StoreInstance<R, S, EM>;
|
|
1827
|
+
/**
|
|
1828
|
+
* Proves to the compiler that a widened state record still covers every slice name.
|
|
1829
|
+
*
|
|
1830
|
+
* @remarks
|
|
1831
|
+
* `StoreInstance` constrains `S extends Record<R, any>`, and TypeScript cannot correlate
|
|
1832
|
+
* {@link WidenState} with {@link WidenNames} well enough to see that the widened record
|
|
1833
|
+
* always carries the widened key set - both branch on `string extends N`, but it checks each
|
|
1834
|
+
* in isolation.
|
|
1835
|
+
*
|
|
1836
|
+
* The intersection is with `unknown`, **never `any`**. `T & unknown` reduces to `T`, so every
|
|
1837
|
+
* slice keeps its exact type; `T & any` is `any`, which silently collapses every slice's
|
|
1838
|
+
* state and destroys the inference this feature exists to provide. That was a real bug caught
|
|
1839
|
+
* by the spike, and it is the reason this helper is written out rather than inlined.
|
|
1840
|
+
*
|
|
1841
|
+
* @public
|
|
1842
|
+
*/
|
|
1843
|
+
export type SatisfiesSlices<T, K extends string> = Prettify<T & Record<K, unknown>>;
|
|
1844
|
+
/**
|
|
1845
|
+
* The store type after mounting slice `N` from `Spec`.
|
|
1846
|
+
*
|
|
1847
|
+
* @public
|
|
1848
|
+
*/
|
|
1849
|
+
export type WidenedSlice<R extends string, S extends Record<R, any>, EM extends EventMapBase, N extends string, Spec> = DecoratableStore<WidenNames<R, N>, SatisfiesSlices<WidenState<S, N, StateOfSpec<Spec>>, WidenNames<R, N>>, Merge<EM, EMAddOf<Spec>>>;
|
|
1850
|
+
/**
|
|
1851
|
+
* The registration surface whose return types carry the widening.
|
|
1852
|
+
*
|
|
1853
|
+
* @remarks
|
|
1854
|
+
* Every method returns the **same runtime object**, re-typed. Subscriptions, effects,
|
|
1855
|
+
* middleware, the dedup cache, both buses and any in-flight `call()` are untouched; the only
|
|
1856
|
+
* runtime effect is the registration itself.
|
|
1857
|
+
*
|
|
1858
|
+
* Note there is no explicit type parameter for the added event map anywhere. It is inferred
|
|
1859
|
+
* from a single value position, so the partial-inference problem never arises and no call
|
|
1860
|
+
* site needs a type argument or a cast.
|
|
1861
|
+
*
|
|
1862
|
+
* @public
|
|
1863
|
+
*/
|
|
1864
|
+
export interface StoreDecoration<R extends string, S extends Record<R, any>, EM extends EventMapBase> {
|
|
1865
|
+
/**
|
|
1866
|
+
* Mounts a slice and hands back both the widened store and a disposer.
|
|
1867
|
+
*
|
|
1868
|
+
* The disposer is **library-private**: after it runs, the widened type still promises a
|
|
1869
|
+
* slice that is gone. Application code should take {@link StoreDecoration.withSlice}
|
|
1870
|
+
* instead, which returns no disposer at all.
|
|
1871
|
+
*/
|
|
1872
|
+
registerSlice<N extends string, Spec extends ReducerSpec<any, any>>(name: N, spec: Spec, options?: {
|
|
1873
|
+
owner?: string;
|
|
1874
|
+
}): Unsubscribe & {
|
|
1875
|
+
store: WidenedSlice<R, S, EM, N, Spec>;
|
|
1876
|
+
dispose(): void;
|
|
1877
|
+
};
|
|
1878
|
+
/** Mounts a slice and returns the widened store, for chaining. */
|
|
1879
|
+
withSlice<N extends string, Spec extends ReducerSpec<any, any>>(name: N, spec: Spec, options?: {
|
|
1880
|
+
owner?: string;
|
|
1881
|
+
}): WidenedSlice<R, S, EM, N, Spec>;
|
|
1882
|
+
/**
|
|
1883
|
+
* Registers middleware and returns the store widened by whatever event map it declares.
|
|
1884
|
+
*
|
|
1885
|
+
* Only the **spec form** can widen: `MiddlewareFunction`'s event parameter is
|
|
1886
|
+
* `EventUnion<EM>`, a mapped type TypeScript cannot infer `EM` back out of. A bare function
|
|
1887
|
+
* therefore contributes `{}`.
|
|
1888
|
+
*/
|
|
1889
|
+
withMiddleware<M extends MiddlewareInput<any, any>>(mw: M): DecoratableStore<R, S, Merge<EM, EMAddOf<M>>>;
|
|
1890
|
+
/** Registers an effect and returns the store widened by whatever event map it declares. */
|
|
1891
|
+
withEffect<Spec extends EffectSpec<any, any>>(spec: Spec): DecoratableStore<R, S, Merge<EM, EMAddOf<Spec>>>;
|
|
1892
|
+
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Declares a reducer spec together with the event map it contributes.
|
|
1895
|
+
*
|
|
1896
|
+
* @remarks
|
|
1897
|
+
* Curried so `EMAdd` is named once and `St` is inferred from `state`, which is what lets every
|
|
1898
|
+
* registration site stay free of type arguments. Identity at runtime.
|
|
1899
|
+
*
|
|
1900
|
+
* @example
|
|
1901
|
+
* ```ts
|
|
1902
|
+
* type LibEM = { "lib.transfer": { granted: { id: string } } };
|
|
1903
|
+
*
|
|
1904
|
+
* const transfers = defineSlice<LibEM>()({
|
|
1905
|
+
* state: { granted: [] as string[] },
|
|
1906
|
+
* when: { keys: [["lib.transfer", "granted"]] },
|
|
1907
|
+
* reducer: (s, e) => (e.type === "granted" ? { granted: [...s.granted, e.payload.id] } : s),
|
|
1908
|
+
* });
|
|
1909
|
+
*
|
|
1910
|
+
* const widened = store.withSlice("transfers", transfers);
|
|
1911
|
+
* // widened.getState().transfers.granted is string[], and `lib.transfer` is emittable
|
|
1912
|
+
* ```
|
|
1913
|
+
*
|
|
1914
|
+
* @public
|
|
1915
|
+
*/
|
|
1916
|
+
export declare const defineSlice: <EMAdd extends EventMapBase>() => <St>(spec: ReducerSpec<St, EMAdd>) => ReducerSpec<St, EMAdd> & EventMapCarrier<EMAdd>;
|
|
1917
|
+
/**
|
|
1918
|
+
* Declares a middleware spec together with the event map it contributes.
|
|
1919
|
+
*
|
|
1920
|
+
* @remarks
|
|
1921
|
+
* The spec form is the **only** form that can widen an event map. Identity at runtime.
|
|
1922
|
+
*
|
|
1923
|
+
* @public
|
|
1924
|
+
*/
|
|
1925
|
+
export declare const defineMiddleware: <EMAdd extends EventMapBase, St = any>() => (spec: MiddlewareSpec<DeepReadonly<St>, EMAdd>) => MiddlewareSpec<DeepReadonly<St>, EMAdd> & EventMapCarrier<EMAdd>;
|
|
1926
|
+
/**
|
|
1927
|
+
* Declares an effect spec together with the event map it contributes.
|
|
1928
|
+
*
|
|
1929
|
+
* @remarks
|
|
1930
|
+
* Identity at runtime.
|
|
1931
|
+
*
|
|
1932
|
+
* @public
|
|
1933
|
+
*/
|
|
1934
|
+
export declare const defineEffect: <EMAdd extends EventMapBase, St = any>() => (spec: EffectSpec<DeepReadonly<St>, EMAdd>) => EffectSpec<DeepReadonly<St>, EMAdd> & EventMapCarrier<EMAdd>;
|