@yoltra/core 0.6.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.
@@ -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
  *
@@ -251,8 +285,8 @@ export interface EmitOptions {
251
285
  * Use this exact id for the event instead of generating one.
252
286
  *
253
287
  * @remarks
254
- * Intended for **idempotent re-emission**: a caller replaying an event from elsewhere (a
255
- * peer store, a durable log) can preserve the original id so the same logical event keeps
288
+ * Intended for **idempotent re-emission**: a caller replaying an event from elsewhere (another
289
+ * store, a durable log) can preserve the original id so the same logical event keeps
256
290
  * one identity everywhere, which makes it traceable across systems and in DevTools.
257
291
  *
258
292
  * The store does **not** enforce uniqueness — supplying a duplicate id does not dedupe the
@@ -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
- * If any middleware returns false (or resolves to false), the event will not propagate.
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
  */
@@ -674,8 +723,8 @@ export interface StoreInstance<R extends string = string, S extends Record<R, an
674
723
  *
675
724
  * @remarks
676
725
  * 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 rather than something that federates.
726
+ * {@link Store.call} for the full contract: correlation, backpressure, timeouts, and why it
727
+ * is a local primitive.
679
728
  */
680
729
  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>>;
681
730
  /**
@@ -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: EffectSpec<DeepReadonly<S>, EM>): Unsubscribe;
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: MiddlewareInput<DeepReadonly<S>, EM>): Unsubscribe;
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>): Unsubscribe;
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
- * - `'all'`: Both committed and uncommitted events (handler receives phase parameter)
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): Unsubscribe;
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: MiddlewareFunction<DeepReadonly<S>, EM>[]): void;
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>>): void;
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 (preferred). The `events` property is
868
- * kept for backward compatibility but `when` is recommended for new code.
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>;