march-hare 0.7.5 → 0.9.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.
Files changed (36) hide show
  1. package/README.md +496 -204
  2. package/dist/{hooks → actions}/index.d.ts +1 -2
  3. package/dist/{hooks → actions}/utils.d.ts +0 -39
  4. package/dist/app/index.d.ts +112 -0
  5. package/dist/app/types.d.ts +49 -0
  6. package/dist/boundary/components/broadcast/utils.d.ts +1 -1
  7. package/dist/boundary/components/env/index.d.ts +26 -0
  8. package/dist/boundary/components/env/types.d.ts +11 -0
  9. package/dist/boundary/components/env/utils.d.ts +36 -0
  10. package/dist/boundary/components/scope/index.d.ts +1 -39
  11. package/dist/boundary/components/scope/types.d.ts +17 -13
  12. package/dist/boundary/components/scope/utils.d.ts +12 -8
  13. package/dist/boundary/components/sharing/index.d.ts +43 -0
  14. package/dist/boundary/index.d.ts +10 -10
  15. package/dist/boundary/types.d.ts +6 -16
  16. package/dist/cache/index.d.ts +4 -4
  17. package/dist/coalesce/index.d.ts +57 -0
  18. package/dist/context/index.d.ts +39 -0
  19. package/dist/context/types.d.ts +14 -0
  20. package/dist/error/index.d.ts +1 -1
  21. package/dist/error/types.d.ts +8 -19
  22. package/dist/index.d.ts +8 -12
  23. package/dist/march-hare.js +7 -5
  24. package/dist/march-hare.umd.cjs +1 -1
  25. package/dist/resource/index.d.ts +52 -78
  26. package/dist/resource/types.d.ts +83 -10
  27. package/dist/scope/index.d.ts +63 -0
  28. package/dist/scope/types.d.ts +55 -0
  29. package/dist/types/index.d.ts +116 -229
  30. package/dist/utils/index.d.ts +6 -5
  31. package/dist/with/index.d.ts +40 -0
  32. package/package.json +1 -1
  33. package/dist/boundary/components/store/index.d.ts +0 -41
  34. package/dist/boundary/components/store/types.d.ts +0 -11
  35. package/dist/boundary/components/store/utils.d.ts +0 -64
  36. /package/dist/{hooks → actions}/types.d.ts +0 -0
@@ -0,0 +1,63 @@
1
+ import { AppContextHandle, AppResource } from '../app/types';
2
+ import { Actions, Model, Props } from '../types/index';
3
+ import * as React from "react";
4
+ /**
5
+ * Handle returned by `app.Scope<MulticastActions>()`. Mirrors the {@link App}
6
+ * surface (`Boundary`, `useContext`, `useEnv`, `Resource`) but typed
7
+ * against a specific multicast action surface `MulticastActions` and the
8
+ * enclosing App's Env shape `S`.
9
+ *
10
+ * Notably absent: a nested `Scope` method. Nesting scopes is supported
11
+ * at the React-tree level &mdash; just render two `<scope.Boundary>`s
12
+ * &mdash; but each scope must come from a distinct
13
+ * `app.Scope<MulticastActions>()` call so that its multicast surface is
14
+ * declared up-front.
15
+ *
16
+ * @template S The enclosing App's Env shape.
17
+ * @template MulticastActions The multicast Actions class (or union of
18
+ * classes) this scope's `useContext().actions.dispatch` is allowed
19
+ * to fire.
20
+ */
21
+ export type Scope<S extends object, MulticastActions> = {
22
+ /**
23
+ * Boundary component. Wrap a subtree to open a fresh multicast scope
24
+ * &mdash; every `Distribution.Multicast` action dispatched inside this
25
+ * subtree routes through this boundary's emitter, and every handler
26
+ * subscribed via `scope.useContext().useActions(...)` on that subtree
27
+ * receives the event.
28
+ *
29
+ * Each render of `<scope.Boundary>` opens a distinct scope instance;
30
+ * unmounting tears the emitter down.
31
+ */
32
+ readonly Boundary: React.FC<{
33
+ children: React.ReactNode;
34
+ }>;
35
+ /**
36
+ * Hook returning a stable `Context` handle. Identical to
37
+ * `app.useContext` except `actions.dispatch` accepts the multicast
38
+ * surface `MulticastActions` in addition to the local `AC` &mdash; mirroring
39
+ * the way `Actions.Broadcast = BroadcastActions` already widens the
40
+ * dispatch surface for broadcasts.
41
+ */
42
+ readonly useContext: <LocalModel extends Model | void = void, AC extends Actions | void = void, D extends Props = Props>() => AppContextHandle<LocalModel, MulticastActions extends Actions ? AC extends Actions ? AC & MulticastActions : MulticastActions : AC, D, S>;
43
+ /**
44
+ * Read-only Proxy over the enclosing App's Env. Identical to
45
+ * `app.useEnv` &mdash; the Scope does not introduce its own Env;
46
+ * scopes are about multicast routing, not ambient state.
47
+ */
48
+ readonly useEnv: () => Readonly<S>;
49
+ /**
50
+ * Resource factory bound to the enclosing App's Env. Identical to
51
+ * `app.Resource`; provided on the scope handle for convenience so a
52
+ * scoped feature can keep all its primitives in one place.
53
+ */
54
+ readonly Resource: AppResource<S>;
55
+ };
56
+ /**
57
+ * Internal constructor for a {@link Scope} handle. Called from inside
58
+ * `App<S>()` so the enclosing Env shape `S` is captured at the type
59
+ * level.
60
+ *
61
+ * @internal
62
+ */
63
+ export declare function createScope<S extends object, MulticastActions>(): Scope<S, MulticastActions>;
@@ -0,0 +1,55 @@
1
+ import { AppContextHandle, AppResource } from '../app/types';
2
+ import { Actions, Model, Props } from '../types/index';
3
+ import type * as React from "react";
4
+ /**
5
+ * Handle returned by `app.Scope<MulticastActions>()`. Mirrors the
6
+ * `App` surface (`Boundary`, `useContext`, `useEnv`, `Resource`) but
7
+ * typed against a specific multicast action surface `MulticastActions`
8
+ * and the enclosing App's Env shape `S`.
9
+ *
10
+ * Notably absent: a nested `Scope` method. Nesting scopes is supported
11
+ * at the React-tree level &mdash; just render two `<scope.Boundary>`s
12
+ * &mdash; but each scope must come from a distinct
13
+ * `app.Scope<MulticastActions>()` call so that its multicast surface is
14
+ * declared up-front.
15
+ *
16
+ * @template S The enclosing App's Env shape.
17
+ * @template MulticastActions The multicast Actions class (or union of
18
+ * classes) this scope's `useContext().actions.dispatch` is allowed
19
+ * to fire.
20
+ */
21
+ export type Scope<S extends object, MulticastActions> = {
22
+ /**
23
+ * Boundary component. Wrap a subtree to open a fresh multicast scope
24
+ * &mdash; every `Distribution.Multicast` action dispatched inside this
25
+ * subtree routes through this boundary's emitter, and every handler
26
+ * subscribed via `scope.useContext().useActions(...)` on that subtree
27
+ * receives the event.
28
+ *
29
+ * Each render of `<scope.Boundary>` opens a distinct scope instance;
30
+ * unmounting tears the emitter down.
31
+ */
32
+ readonly Boundary: React.FC<{
33
+ children: React.ReactNode;
34
+ }>;
35
+ /**
36
+ * Hook returning a stable `Context` handle. Identical to
37
+ * `app.useContext` except `actions.dispatch` accepts the multicast
38
+ * surface `MulticastActions` in addition to the local `AC` &mdash;
39
+ * mirroring the way `Actions.Broadcast = BroadcastActions` already
40
+ * widens the dispatch surface for broadcasts.
41
+ */
42
+ readonly useContext: <LocalModel extends Model | void = void, AC extends Actions | void = void, D extends Props = Props>() => AppContextHandle<LocalModel, MulticastActions extends Actions ? AC extends Actions ? AC & MulticastActions : MulticastActions : AC, D, S>;
43
+ /**
44
+ * Read-only Proxy over the enclosing App's Env. Identical to
45
+ * `app.useEnv` &mdash; the Scope does not introduce its own Env;
46
+ * scopes are about multicast routing, not ambient state.
47
+ */
48
+ readonly useEnv: () => Readonly<S>;
49
+ /**
50
+ * Resource factory bound to the enclosing App's Env. Identical to
51
+ * `app.Resource`; provided on the scope handle for convenience so a
52
+ * scoped feature can keep all its primitives in one place.
53
+ */
54
+ readonly Resource: AppResource<S>;
55
+ };
@@ -1,8 +1,40 @@
1
1
  import { Operation, Process, Inspect, Box } from 'immertation';
2
2
  import { ActionId, Task, Tasks } from '../boundary/components/tasks/types';
3
3
  import { Fault } from '../error/types';
4
- import { Store } from '../boundary/components/store/index';
4
+ import { Env } from '../boundary/components/env/index';
5
+ import { Coalesce } from '../resource/types';
5
6
  import * as React from "react";
7
+ /**
8
+ * Chainable handle returned from `context.actions.resource(invocation)`.
9
+ *
10
+ * - `.exceeds(duration)` short-circuits the fetch when the per-params
11
+ * cache age is within the supplied freshness window.
12
+ * - `.coalesce(token)` opts the call into in-flight sharing: any other
13
+ * caller with the same Resource, same structural params, and equal
14
+ * `token` joins the same promise.
15
+ *
16
+ * Awaiting the handle (`await context.actions.resource(...)`) triggers
17
+ * the fetch with whichever options have been set on the chain.
18
+ */
19
+ export type ResourceCall<T> = PromiseLike<T> & {
20
+ /**
21
+ * Skip the fetch when the cached payload is within `duration`.
22
+ * Accepts a `Temporal.Duration`, a `DurationLike` object
23
+ * (`{ minutes: 5 }`), or an ISO 8601 string (`"PT5M"`).
24
+ */
25
+ readonly exceeds: (duration: Temporal.DurationLike) => ResourceCall<T>;
26
+ /**
27
+ * Join an in-flight fetch for the same `(resource, params, token)`
28
+ * tuple. The shared fetch runs against a detached `AbortController`
29
+ * so a single caller's abort never cancels work other callers are
30
+ * waiting on; each caller still sees its own `context.task.controller`
31
+ * abort as a rejection of its personal await.
32
+ *
33
+ * `token` is optional &mdash; omit it to share with every other
34
+ * untokened caller for the same `(resource, params)` slot.
35
+ */
36
+ readonly coalesce: (token?: Coalesce) => ResourceCall<T>;
37
+ };
6
38
  export type { ActionId, Box, Task, Tasks };
7
39
  /**
8
40
  * Type for objects with a Brand.Action symbol property.
@@ -34,7 +66,7 @@ export type BrandedObject = {
34
66
  };
35
67
  /**
36
68
  * Recursive readonly. Locks every nested property so that read-only
37
- * projections on `context` (model, data, store) reject direct assignment
69
+ * projections on `context` (model, data, env) reject direct assignment
38
70
  * &mdash; mutation must go through `context.actions.produce(...)`.
39
71
  *
40
72
  * Function types pass through untouched so method calls (e.g.
@@ -85,13 +117,13 @@ export declare class Brand {
85
117
  */
86
118
  export declare const FaultSymbol: unique symbol;
87
119
  /**
88
- * Internal symbol for the global `Lifecycle.Store` broadcast. The store
120
+ * Internal symbol for the global `Lifecycle.Env` broadcast. The env
89
121
  * mutation path in `useActions` fires this symbol whenever a
90
- * `produce({ store })` call changes the slot reference.
122
+ * `produce({ env })` call changes the slot reference.
91
123
  *
92
124
  * @internal
93
125
  */
94
- export declare const StoreSymbol: unique symbol;
126
+ export declare const EnvSymbol: unique symbol;
95
127
  /**
96
128
  * Factory functions for lifecycle actions.
97
129
  *
@@ -111,10 +143,10 @@ export declare const StoreSymbol: unique symbol;
111
143
  * }
112
144
  * ```
113
145
  *
114
- * `Lifecycle.Fault` and `Lifecycle.Store` are singleton broadcasts (not
146
+ * `Lifecycle.Fault` and `Lifecycle.Env` are singleton broadcasts (not
115
147
  * factories). All components subscribe to the same shared symbol &mdash;
116
- * `Fault` delivers global fault notifications, `Store` delivers per-`Boundary`
117
- * store-change notifications.
148
+ * `Fault` delivers global fault notifications, `Env` delivers per-`Boundary`
149
+ * env-change notifications.
118
150
  */
119
151
  export declare class Lifecycle {
120
152
  /** Creates a Mount lifecycle action. Triggered once on component mount (`useLayoutEffect`). */
@@ -146,30 +178,30 @@ export declare class Lifecycle {
146
178
  */
147
179
  static Fault: BroadcastPayload<Fault, never, "Fault">;
148
180
  /**
149
- * Global store-change broadcast. Receives the latest {@link Store}
150
- * snapshot whenever a `context.actions.produce(({ store }) => ...)` call
181
+ * Global env-change broadcast. Receives the latest {@link Env}
182
+ * snapshot whenever a `context.actions.produce(({ env }) => ...)` call
151
183
  * mutates the slot. Subscribe via
152
- * `actions.useAction(Lifecycle.Store, handler)` &mdash; or render against
153
- * it directly with `actions.stream(Lifecycle.Store, (store) => ...)`.
184
+ * `actions.useAction(Lifecycle.Env, handler)` &mdash; or render against
185
+ * it directly with `actions.stream(Lifecycle.Env, (env) => ...)`.
154
186
  *
155
187
  * Like `Lifecycle.Fault`, this is a singleton broadcast (not a factory):
156
188
  * every subscriber points at the same shared symbol. The latest value is
157
189
  * cached on the broadcast emitter so that late-mounting handlers and
158
- * streams receive the current store on mount.
190
+ * streams receive the current env on mount.
159
191
  *
160
192
  * @example
161
193
  * ```tsx
162
- * actions.useAction(Lifecycle.Store, (context, store) => {
163
- * console.log("store changed", store);
194
+ * actions.useAction(Lifecycle.Env, (context, env) => {
195
+ * console.log("env changed", env);
164
196
  * });
165
197
  *
166
198
  * // In JSX:
167
- * {actions.stream(Lifecycle.Store, (store) => (
168
- * <span>{store.locale}</span>
199
+ * {actions.stream(Lifecycle.Env, (env) => (
200
+ * <span>{env.locale}</span>
169
201
  * ))}
170
202
  * ```
171
203
  */
172
- static Store: BroadcastPayload<Store, never, "Store">;
204
+ static Env: BroadcastPayload<Env, never, "Env">;
173
205
  }
174
206
  /**
175
207
  * Distribution modes for actions.
@@ -179,21 +211,29 @@ export declare class Lifecycle {
179
211
  * - **Broadcast** &ndash; Action is distributed to all mounted components that have
180
212
  * defined a handler for it. Values are cached for late-mounting components.
181
213
  * - **Multicast** &ndash; Action defines its own scope. Components reach it by
182
- * wrapping a subtree in `withScope(<theMulticastAction>, Component)`.
214
+ * rendering inside a `<scope.Boundary>` produced by `app.Scope<MulticastActions>()`.
183
215
  *
184
216
  * @example
185
217
  * ```ts
186
- * export class Scope {
187
- * // The action itself acts as the scope identifier.
218
+ * export class MulticastActions {
188
219
  * static Mood = Action<Mood>("Mood", Distribution.Multicast);
189
220
  * }
190
221
  *
222
+ * export const scope = app.Scope<typeof MulticastActions>();
223
+ *
191
224
  * // Wrap the subtree where the scope applies.
192
- * export default withScope(Scope.Mood, Component);
225
+ * export default function Mood() {
226
+ * return (
227
+ * <scope.Boundary>
228
+ * <Happy />
229
+ * <Sad />
230
+ * </scope.Boundary>
231
+ * );
232
+ * }
193
233
  *
194
234
  * // Dispatch / subscribe — no extra options.
195
- * actions.dispatch(Scope.Mood, mood);
196
- * actions.useAction(Scope.Mood, (context, mood) => { ... });
235
+ * actions.dispatch(MulticastActions.Mood, mood);
236
+ * actions.useAction(MulticastActions.Mood, (context, mood) => { ... });
197
237
  * ```
198
238
  */
199
239
  export declare enum Distribution {
@@ -201,7 +241,7 @@ export declare enum Distribution {
201
241
  Unicast = "unicast",
202
242
  /** Action is broadcast to all mounted components and can be consumed. */
203
243
  Broadcast = "broadcast",
204
- /** Action is multicast to every component inside its `withScope` boundary. */
244
+ /** Action is multicast to every component inside its `<scope.Boundary>`. */
205
245
  Multicast = "multicast"
206
246
  }
207
247
  /**
@@ -237,6 +277,15 @@ export declare enum Phase {
237
277
  * @template T - The concrete primary key type (e.g., string, number)
238
278
  */
239
279
  export type Pk<T> = undefined | symbol | T;
280
+ /**
281
+ * Maybe-present field type &mdash; a value that may be a concrete `T`,
282
+ * or `null` / `undefined` while loading, awaiting a fetch, or before
283
+ * upstream data has arrived. Use this for model fields whose presence
284
+ * is determined by async or external state.
285
+ *
286
+ * @template T - The concrete value type
287
+ */
288
+ export type Maybe<T> = T | null | undefined;
240
289
  /**
241
290
  * Base constraint type for model state objects.
242
291
  * Models must be plain objects with string keys.
@@ -306,7 +355,7 @@ export type ChanneledAction<P = unknown, C = unknown, Name extends string = stri
306
355
  * to check whether a cached value exists before performing default fetches.
307
356
  *
308
357
  * This type extends `HandlerPayload<P, C>` with an additional brand to enforce at compile-time
309
- * that only broadcast actions can be passed to `context.actions.resolution()`.
358
+ * that only broadcast actions can be passed to `context.actions.final()`.
310
359
  *
311
360
  * @template P - The payload type for the action
312
361
  * @template C - The channel type for channeled dispatches (defaults to never)
@@ -316,7 +365,7 @@ export type ChanneledAction<P = unknown, C = unknown, Name extends string = stri
316
365
  * const SignedOut = Action<User>("SignedOut", Distribution.Broadcast);
317
366
  *
318
367
  * // Resolve the latest value inside a handler
319
- * const user = await context.actions.resolution(SignedOut);
368
+ * const user = await context.actions.final(SignedOut);
320
369
  * ```
321
370
  */
322
371
  export type BroadcastPayload<P = unknown, C extends Filter = never, Name extends string = string> = HandlerPayload<P, C, Name> & {
@@ -462,217 +511,25 @@ export type Result = {
462
511
  };
463
512
  export type HandlerContext<M extends Model | void, AC extends Actions | void, D extends Props = Props> = {
464
513
  readonly model: DeepReadonly<M>;
465
- /**
466
- * The current lifecycle phase of the component.
467
- * Useful for determining if the handler was called during mount (e.g., from a cached
468
- * distributed action value) vs after the component is fully mounted.
469
- *
470
- * @example
471
- * ```ts
472
- * actions.useAction(Actions.Broadcast.Counter, (context, payload) => {
473
- * if (context.phase === Phase.Mounting) {
474
- * // Called with cached value during mount
475
- * console.log("Received cached value:", payload);
476
- * }
477
- * });
478
- * ```
479
- */
480
514
  readonly phase: Phase;
481
- /**
482
- * The current task for the executing action handler.
483
- * Contains the AbortController, action identifier, and payload for this specific invocation.
484
- *
485
- * Use `task.controller.signal` to check if the action was aborted, or `task.controller.abort()` to cancel it.
486
- * The `task.action` and `task.payload` properties identify which action triggered this handler.
487
- *
488
- * @example
489
- * ```ts
490
- * actions.useAction(Actions.Fetch, async (context) => {
491
- * const response = await fetch("/api", {
492
- * signal: context.task.controller.signal,
493
- * });
494
- *
495
- * if (context.task.controller.signal.aborted) return;
496
- *
497
- * context.actions.produce((draft) => {
498
- * draft.model.data = response;
499
- * });
500
- * });
501
- * ```
502
- */
503
515
  readonly task: Task;
504
- /**
505
- * Reactive data values passed to useActions.
506
- * Always returns the latest values, even after awaits in async handlers.
507
- *
508
- * @example
509
- * ```ts
510
- * const [name, setName] = useState("Adam");
511
- * const actions = useActions<Model, typeof Actions>(model, () => ({ name }));
512
- *
513
- * actions.useAction(Actions.Fetch, async (context) => {
514
- * await fetch("/api");
515
- * // context.data.name is always the latest value
516
- * console.log(context.data.name);
517
- * });
518
- * ```
519
- */
520
516
  readonly data: DeepReadonly<D>;
521
- /**
522
- * Set of all running tasks across all components in the context.
523
- * Tasks are ordered by creation time (oldest first).
524
- *
525
- * Each task contains:
526
- * - `controller`: The AbortController to cancel this task
527
- * - `action`: The action identifier that triggered this task
528
- * - `payload`: The payload passed when the action was dispatched
529
- *
530
- * @example
531
- * ```ts
532
- * // Abort all tasks for a specific action
533
- * for (const runningTask of context.tasks) {
534
- * if (runningTask.action === Actions.Fetch) {
535
- * runningTask.controller.abort();
536
- * }
537
- * }
538
- *
539
- * // Abort the oldest task
540
- * const oldest = context.tasks.values().next().value;
541
- * oldest?.controller.abort();
542
- *
543
- * // Abort all tasks except the current one
544
- * for (const runningTask of context.tasks) {
545
- * if (runningTask !== context.task) {
546
- * runningTask.controller.abort();
547
- * }
548
- * }
549
- * ```
550
- */
551
517
  readonly tasks: ReadonlySet<Task>;
552
- /**
553
- * Read-only view of the per-`<Boundary>` Store &mdash; ambient,
554
- * cross-cutting state (session, locale, feature flags, etc.) typed
555
- * via module augmentation on the library's `Store` interface.
556
- * Identical to the value returned by `useStore()` at the hook level.
557
- *
558
- * Reads use plain dot notation and always reflect the latest value,
559
- * even after `await` boundaries. Writes go through
560
- * `context.actions.produce(({ store }) => { store.x = ... })`
561
- * &mdash; the same Immer-style recipe used for the model.
562
- *
563
- * @example
564
- * ```ts
565
- * actions.useAction(Actions.SignIn, async (context, credentials) => {
566
- * const result = await context.actions.resource(signIn(credentials));
567
- * context.actions.produce(({ store }) => {
568
- * store.session = result;
569
- * });
570
- * });
571
- *
572
- * actions.useAction(Actions.Refresh, async (context) => {
573
- * if (context.store.session === null) return;
574
- * // ...
575
- * });
576
- * ```
577
- */
578
- readonly store: DeepReadonly<Store>;
518
+ readonly env: DeepReadonly<Env>;
579
519
  readonly actions: {
580
520
  produce<F extends (draft: {
581
521
  model: M;
582
- store: Store;
522
+ env: Env;
583
523
  readonly inspect: Readonly<Inspect<M>>;
584
524
  }) => void>(ƒ: F & AssertSync<F>): void;
585
525
  dispatch(action: NoPayloadActions<Dispatchable<AC>>): Promise<void>;
586
526
  dispatch<A extends WithPayloadActions<Dispatchable<AC>>>(action: A, payload: Payload<A>): Promise<void>;
587
527
  annotate<T>(value: T, operation?: Operation): T;
588
- /**
589
- * Fetches a {@link Resource} with the abort controller and Store
590
- * snapshot auto-threaded from the current handler context. The
591
- * argument is a resource invocation (`cat({ id: 5 })`) &mdash; the
592
- * call primes a slot with the resource and params, and
593
- * `.resource(...)` reads it. The return value is a thenable &mdash;
594
- * `await` it to fire the fetch unconditionally, or use
595
- * `.exceeds(duration)` to short-circuit when the per-params cache
596
- * slot is still within the supplied freshness window (i.e. fetch
597
- * only when the cache age *exceeds* the duration).
598
- *
599
- * @example
600
- * ```ts
601
- * actions.useAction(Actions.Mount, async (context) => {
602
- * // Always fetch.
603
- * const fresh = await context.actions.resource(user({ id: 5 }));
604
- *
605
- * // Reuse cache when < 5 minutes old.
606
- * const maybe = await context.actions
607
- * .resource(user({ id: 5 }))
608
- * .exceeds({ minutes: 5 });
609
- *
610
- * context.actions.produce(({ model }) => void (model.user = fresh));
611
- * });
612
- * ```
613
- */
614
- resource: (<T>(invocation: T | null) => PromiseLike<T> & {
615
- readonly exceeds: (duration: Temporal.DurationLike) => Promise<T>;
616
- }) & {
617
- /**
618
- * Writes `data` into the per-params cache slot of the resource
619
- * invocation passed as the first argument, with a fresh timestamp.
620
- * Use this when payloads arrive out-of-band (SSE, WebSocket,
621
- * postMessage) and need to be reflected in the Resource cache
622
- * without a fetcher round-trip.
623
- *
624
- * @example
625
- * ```ts
626
- * actions.useAction(Actions.Broadcast.UserSSE, (context, payload) => {
627
- * context.actions.resource.set(user({ id: payload.id }), payload);
628
- * });
629
- * ```
630
- */
528
+ readonly inspect: Readonly<Inspect<M>>;
529
+ resource: (<T>(invocation: T | null) => ResourceCall<T>) & {
631
530
  set<T>(invocation: T | null, data: T): void;
632
531
  };
633
- /**
634
- * Returns the resolved broadcast or multicast value, waiting for any
635
- * pending annotations to settle before resolving.
636
- *
637
- * If a value has already been dispatched it resolves immediately.
638
- * Otherwise it waits until the next dispatch of the action.
639
- * Resolves with `null` if the task is aborted before a value arrives.
640
- *
641
- * @param action - The broadcast or multicast action to resolve. Multicast
642
- * actions read their scope from the action declaration.
643
- * @returns The dispatched value, or `null` if aborted.
644
- *
645
- * @example
646
- * ```ts
647
- * actions.useAction(Actions.FetchPosts, async (context) => {
648
- * const user = await context.actions.resolution(Actions.Broadcast.User);
649
- * if (!user) return;
650
- * const posts = await fetchPosts(user.id, {
651
- * signal: context.task.controller.signal,
652
- * });
653
- * context.actions.produce(({ model }) => { model.posts = posts; });
654
- * });
655
- * ```
656
- */
657
- resolution<T>(action: BroadcastPayload<T> | MulticastPayload<T>): Promise<T | null>;
658
- /**
659
- * Returns the latest broadcast or multicast value immediately without
660
- * waiting for annotations to settle. Use this when you need the current
661
- * cached value and do not need to wait for pending operations to complete.
662
- *
663
- * @param action - The broadcast or multicast action to peek at. Multicast
664
- * actions read their scope from the action declaration.
665
- * @returns The cached value, or `null` if no value has been dispatched.
666
- *
667
- * @example
668
- * ```ts
669
- * actions.useAction(Actions.Check, (context) => {
670
- * const user = context.actions.peek(Actions.Broadcast.User);
671
- * if (!user) return;
672
- * console.log(user.name);
673
- * });
674
- * ```
675
- */
532
+ final<T>(action: BroadcastPayload<T> | MulticastPayload<T>): Promise<T | null>;
676
533
  peek<T>(action: BroadcastPayload<T> | MulticastPayload<T>): T | null;
677
534
  };
678
535
  };
@@ -738,7 +595,7 @@ type OwnKeys<AC> = Exclude<keyof AC & string, "prototype">;
738
595
  *
739
596
  * Used to constrain `dispatch` and `useAction` so that only actions owned by
740
597
  * the component's `AC` (plus the global `Lifecycle.Fault` /
741
- * `Lifecycle.Store`) can be referenced.
598
+ * `Lifecycle.Env`) can be referenced.
742
599
  */
743
600
  export type LeafActions<AC> = AC extends void ? never : {
744
601
  [K in OwnKeys<AC>]: OwnKeys<AC[K]> extends never ? AC[K] : LeafActions<AC[K]>;
@@ -757,10 +614,10 @@ export type ChanneledOf<A> = A extends HandlerPayload<infer P, infer C> ? [C] ex
757
614
  export type Dispatchable<AC> = LeafActions<AC> | ChanneledOf<LeafActions<AC>>;
758
615
  /**
759
616
  * Everything `useAction` will subscribe to for a given `AC`: same as
760
- * `Dispatchable<AC>` plus the shared `Lifecycle.Fault` and `Lifecycle.Store`
617
+ * `Dispatchable<AC>` plus the shared `Lifecycle.Fault` and `Lifecycle.Env`
761
618
  * broadcasts which live outside `AC` but are subscribable by any component.
762
619
  */
763
- export type Subscribable<AC> = Dispatchable<AC> | typeof Lifecycle.Fault | typeof Lifecycle.Store;
620
+ export type Subscribable<AC> = Dispatchable<AC> | typeof Lifecycle.Fault | typeof Lifecycle.Env;
764
621
  /**
765
622
  * Subset of a union of actions whose payload type is `never`. Used to split
766
623
  * `dispatch`/`useAction` into a no-payload and a with-payload overload so
@@ -860,7 +717,8 @@ export type UseActions<M extends Model | void, AC extends Actions | void, D exte
860
717
  dispatch<A extends WithPayloadActions<Dispatchable<AC>>>(action: A, payload: Payload<A>): Promise<void>;
861
718
  /**
862
719
  * Registers an action handler with the current scope.
863
- * Types are pre-baked from the useActions call, so no type parameter is needed.
720
+ * Types are pre-baked from the `channel.use(...)` call, so no type
721
+ * parameter is needed.
864
722
  *
865
723
  * Supports two subscription patterns:
866
724
  * 1. **Plain action** - Receives ALL dispatches for that action (including channeled ones)
@@ -871,7 +729,8 @@ export type UseActions<M extends Model | void, AC extends Actions | void, D exte
871
729
  *
872
730
  * @example
873
731
  * ```ts
874
- * const actions = useActions<typeof Actions>(model);
732
+ * const context = useContext<Model, typeof Actions>();
733
+ * const actions = context.useActions(model);
875
734
  *
876
735
  * // Subscribe to ALL UserUpdated events
877
736
  * actions.useAction(Actions.UserUpdated, (context, user) => {
@@ -887,3 +746,31 @@ export type UseActions<M extends Model | void, AC extends Actions | void, D exte
887
746
  useAction(action: NoPayloadActions<Subscribable<AC>>, handler: (context: HandlerContext<M, AC, D>) => void | Promise<void> | AsyncGenerator | Generator): void;
888
747
  useAction<A extends WithPayloadActions<Subscribable<AC>>>(action: A, handler: (context: HandlerContext<M, AC, D>, payload: Payload<A>) => void | Promise<void> | AsyncGenerator | Generator): void;
889
748
  };
749
+ /**
750
+ * Stable, typed dispatch function for the actions class `AC`. Same call
751
+ * signatures as `actions.dispatch` returned by `useActions`, available
752
+ * before the paired `useActions` has run via {@link Context}.
753
+ */
754
+ export type Dispatch<AC extends Actions | void> = {
755
+ (action: NoPayloadActions<Dispatchable<AC>>): Promise<void>;
756
+ <A extends WithPayloadActions<Dispatchable<AC>>>(action: A, payload: Payload<A>): Promise<void>;
757
+ };
758
+ /**
759
+ * Handle returned by `useContext<M, AC, D>()`. Exposes
760
+ * `dispatch(action, payload?)` and a `useActions` method that materialises
761
+ * the component-local model and reactive data against the same dispatch
762
+ * target. Generics are declared on `useContext`; `useActions` inherits
763
+ * them &mdash; the call site does not re-state `Model` / `Actions` /
764
+ * `Data`.
765
+ *
766
+ * Note: this `Context` type is distinct from React's `useContext` /
767
+ * `React.Context` &mdash; it's the March Hare action surface returned by
768
+ * the `useContext` hook of this library.
769
+ */
770
+ export type Context<M extends Model | void, AC extends Actions | void, D extends Props = Props> = {
771
+ readonly actions: {
772
+ dispatch: Dispatch<AC>;
773
+ };
774
+ useActions(getData?: () => D): UseActions<M, AC, D>;
775
+ useActions(initialModel: M extends void ? never : M, getData?: () => D): UseActions<M, AC, D>;
776
+ };
@@ -3,15 +3,15 @@ export { unset } from './utils';
3
3
  export type { Stored, Unset } from './types';
4
4
  /**
5
5
  * Returns a promise that resolves after the specified number of
6
- * milliseconds, or rejects with an {@link AbortError} when the signal is
7
- * aborted. Use to inject a cancellable delay into an action handler.
6
+ * milliseconds, or rejects with an {@link Aborted} when the signal is aborted. Use to inject a cancellable
7
+ * delay into an action handler.
8
8
  *
9
9
  * @param ms How long to wait before resolving.
10
10
  * @param signal Optional {@link AbortSignal} that cancels the sleep early.
11
11
  * Pass `context.task.controller.signal` to tie the wait to
12
12
  * the lifetime of the current action.
13
13
  * @returns A promise that resolves after `ms` milliseconds or rejects with
14
- * an {@link AbortError} if `signal` aborts first.
14
+ * an {@link Aborted} if `signal` aborts first.
15
15
  */
16
16
  export declare function sleep(ms: number, signal: AbortSignal | undefined): Promise<void>;
17
17
  /**
@@ -21,12 +21,13 @@ export declare function sleep(ms: number, signal: AbortSignal | undefined): Prom
21
21
  *
22
22
  * @param ms Interval in milliseconds between invocations of `fn`.
23
23
  * @param signal Optional {@link AbortSignal} that cancels polling early.
24
- * Aborts propagate as an {@link AbortError} rejection.
24
+ * Aborts propagate as an {@link Aborted} rejection.
25
25
  * @param fn Predicate invoked each iteration. Return `true` to stop
26
26
  * polling, `false` to schedule another invocation after `ms`.
27
27
  * May be sync or async.
28
28
  * @returns A promise that resolves when `fn` returns `true`, or rejects
29
- * with an {@link AbortError} if `signal` aborts first.
29
+ * with a `DOMException("aborted", "Aborted")` if `signal`
30
+ * aborts first.
30
31
  */
31
32
  export declare function poll(ms: number, signal: AbortSignal | undefined, fn: () => boolean | Promise<boolean>): Promise<void>;
32
33
  /**