march-hare 0.6.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 (38) hide show
  1. package/README.md +453 -0
  2. package/dist/march-hare.js +6 -0
  3. package/dist/march-hare.umd.cjs +1 -0
  4. package/dist/src/library/action/index.d.ts +66 -0
  5. package/dist/src/library/action/utils.d.ts +89 -0
  6. package/dist/src/library/annotate/index.d.ts +25 -0
  7. package/dist/src/library/boundary/components/broadcast/index.d.ts +12 -0
  8. package/dist/src/library/boundary/components/broadcast/types.d.ts +19 -0
  9. package/dist/src/library/boundary/components/broadcast/utils.d.ts +39 -0
  10. package/dist/src/library/boundary/components/consumer/components/partition/index.d.ts +27 -0
  11. package/dist/src/library/boundary/components/consumer/components/partition/types.d.ts +9 -0
  12. package/dist/src/library/boundary/components/consumer/index.d.ts +19 -0
  13. package/dist/src/library/boundary/components/consumer/types.d.ts +37 -0
  14. package/dist/src/library/boundary/components/consumer/utils.d.ts +13 -0
  15. package/dist/src/library/boundary/components/mode/index.d.ts +15 -0
  16. package/dist/src/library/boundary/components/mode/types.d.ts +7 -0
  17. package/dist/src/library/boundary/components/mode/utils.d.ts +55 -0
  18. package/dist/src/library/boundary/components/scope/index.d.ts +40 -0
  19. package/dist/src/library/boundary/components/scope/types.d.ts +20 -0
  20. package/dist/src/library/boundary/components/scope/utils.d.ts +19 -0
  21. package/dist/src/library/boundary/components/tasks/index.d.ts +14 -0
  22. package/dist/src/library/boundary/components/tasks/types.d.ts +43 -0
  23. package/dist/src/library/boundary/components/tasks/utils.d.ts +26 -0
  24. package/dist/src/library/boundary/index.d.ts +20 -0
  25. package/dist/src/library/boundary/types.d.ts +4 -0
  26. package/dist/src/library/error/index.d.ts +2 -0
  27. package/dist/src/library/error/types.d.ts +75 -0
  28. package/dist/src/library/error/utils.d.ts +15 -0
  29. package/dist/src/library/hooks/index.d.ts +43 -0
  30. package/dist/src/library/hooks/types.d.ts +72 -0
  31. package/dist/src/library/hooks/utils.d.ts +198 -0
  32. package/dist/src/library/index.d.ts +16 -0
  33. package/dist/src/library/resource/index.d.ts +99 -0
  34. package/dist/src/library/types/index.d.ts +718 -0
  35. package/dist/src/library/utils/index.d.ts +42 -0
  36. package/dist/src/library/utils/utils.d.ts +5 -0
  37. package/dist/src/library/utils.d.ts +37 -0
  38. package/package.json +104 -0
@@ -0,0 +1,718 @@
1
+ import { Operation, Process, Inspect, Box } from 'immertation';
2
+ import { ActionId, Task, Tasks } from '../boundary/components/tasks/types.ts';
3
+ import { Fault } from '../error/types.ts';
4
+ import * as React from "react";
5
+ export type { ActionId, Box, Task, Tasks };
6
+ /**
7
+ * Type for objects with a Brand.Action symbol property.
8
+ * Used for type-safe access to the action symbol.
9
+ */
10
+ export type BrandedAction = {
11
+ readonly [K in typeof Brand.Action]: symbol;
12
+ };
13
+ /**
14
+ * Type for objects with a Brand.Broadcast symbol property.
15
+ * Used for type-safe access to the broadcast flag.
16
+ */
17
+ export type BrandedBroadcast = {
18
+ readonly [K in typeof Brand.Broadcast]: boolean;
19
+ };
20
+ /**
21
+ * Type for objects with a Brand.Multicast symbol property.
22
+ * Used for type-safe access to the multicast flag.
23
+ */
24
+ export type BrandedMulticast = {
25
+ readonly [K in typeof Brand.Multicast]: boolean;
26
+ };
27
+ /**
28
+ * Base type for any object that may contain branded symbol properties.
29
+ * Used as a permissive input type for action utilities.
30
+ */
31
+ export type BrandedObject = {
32
+ readonly [x: symbol]: unknown;
33
+ };
34
+ /**
35
+ * Union type representing any valid action that can be passed to action utilities.
36
+ * This includes raw ActionIds (symbol/string), and any branded object.
37
+ */
38
+ export type AnyAction = ActionId | BrandedObject;
39
+ /**
40
+ * Internal symbols used as brand keys to distinguish typed objects at runtime.
41
+ * These enable TypeScript to differentiate between HandlerPayload, BroadcastPayload,
42
+ * and channeled actions through branded types.
43
+ * @internal
44
+ */
45
+ export declare class Brand {
46
+ /** Brand key for HandlerPayload type */
47
+ static readonly Payload: unique symbol;
48
+ /** Brand key for BroadcastPayload type */
49
+ static readonly Broadcast: unique symbol;
50
+ /** Brand key for MulticastPayload type */
51
+ static readonly Multicast: unique symbol;
52
+ /** Access the underlying symbol from an action */
53
+ static readonly Action: unique symbol;
54
+ /** Identifies channeled actions (result of calling Action(channel)) */
55
+ static readonly Channel: unique symbol;
56
+ }
57
+ /**
58
+ * Internal symbol for the global `Lifecycle.Fault` broadcast. Exposed so the
59
+ * dispatch pipeline can fire faults without depending on the `Lifecycle`
60
+ * class at runtime.
61
+ *
62
+ * @internal
63
+ */
64
+ export declare const FaultSymbol: unique symbol;
65
+ /**
66
+ * Factory functions for lifecycle actions.
67
+ *
68
+ * Each call returns a **unique** action symbol so that each component can
69
+ * subscribe independently. Assign the result as a static property in your
70
+ * Actions class:
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * export class Actions {
75
+ * static Mount = Lifecycle.Mount();
76
+ * static Unmount = Lifecycle.Unmount();
77
+ * static Error = Lifecycle.Error();
78
+ * static Update = Lifecycle.Update();
79
+ *
80
+ * static Increment = Action("Increment");
81
+ * }
82
+ * ```
83
+ *
84
+ * `Lifecycle.Fault` is a singleton broadcast (not a factory). All components
85
+ * subscribe to the same shared symbol to receive global fault notifications.
86
+ */
87
+ export declare class Lifecycle {
88
+ /** Creates a Mount lifecycle action. Triggered once on component mount (`useLayoutEffect`). */
89
+ static Mount(): HandlerPayload<never>;
90
+ /** Creates an Unmount lifecycle action. Triggered when the component unmounts. */
91
+ static Unmount(): HandlerPayload<never>;
92
+ /** Creates an Error lifecycle action. Triggered when an action throws. Receives `Fault` as payload. */
93
+ static Error(): HandlerPayload<Fault>;
94
+ /** Creates an Update lifecycle action. Triggered when `context.data` changes (not on initial mount). */
95
+ static Update(): HandlerPayload<Record<string, unknown>>;
96
+ /**
97
+ * Global fault broadcast. Receives a `Fault` whenever any action in the
98
+ * `<Boundary>` errors, times out, or is supplanted. Subscribe via
99
+ * `actions.useAction(Lifecycle.Fault, handler)`.
100
+ *
101
+ * Unlike the per-component `Lifecycle.Error()` factory, `Fault` is a single
102
+ * shared broadcast — every subscriber points at the same symbol.
103
+ *
104
+ * @example
105
+ * ```tsx
106
+ * const actions = useActions<void, typeof Actions>();
107
+ *
108
+ * actions.useAction(Lifecycle.Fault, (context, fault) => {
109
+ * if (fault.reason === Reason.Errored) {
110
+ * console.error(`Action "${fault.action}" failed`, fault.error);
111
+ * }
112
+ * });
113
+ * ```
114
+ */
115
+ static Fault: BroadcastPayload<Fault>;
116
+ }
117
+ /**
118
+ * Distribution modes for actions.
119
+ *
120
+ * - **Unicast** &ndash; Action is scoped to the component that defines it and cannot be
121
+ * consumed by other components. This is the default behaviour.
122
+ * - **Broadcast** &ndash; Action is distributed to all mounted components that have
123
+ * defined a handler for it. Values are cached for late-mounting components.
124
+ * - **Multicast** &ndash; Action defines its own scope. Components reach it by
125
+ * wrapping a subtree in `withScope(<theMulticastAction>, Component)`.
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * export class Scope {
130
+ * // The action itself acts as the scope identifier.
131
+ * static Mood = Action<Mood>("Mood", Distribution.Multicast);
132
+ * }
133
+ *
134
+ * // Wrap the subtree where the scope applies.
135
+ * export default withScope(Scope.Mood, Component);
136
+ *
137
+ * // Dispatch / subscribe — no extra options.
138
+ * actions.dispatch(Scope.Mood, mood);
139
+ * actions.useAction(Scope.Mood, (context, mood) => { ... });
140
+ * ```
141
+ */
142
+ export declare enum Distribution {
143
+ /** Action is scoped to the component that defines it. This is the default. */
144
+ Unicast = "unicast",
145
+ /** Action is broadcast to all mounted components and can be consumed. */
146
+ Broadcast = "broadcast",
147
+ /** Action is multicast to every component inside its `withScope` boundary. */
148
+ Multicast = "multicast"
149
+ }
150
+ /**
151
+ * Lifecycle phase of a component using useActions.
152
+ * Tracks whether the component is in the process of mounting, fully mounted,
153
+ * unmounting, or completely unmounted.
154
+ *
155
+ * @example
156
+ * ```ts
157
+ * actions.useAction(Actions.Counter, (context, payload) => {
158
+ * if (context.phase === Phase.Mounting) {
159
+ * // Handler called during mount (e.g., cached distributed action value)
160
+ * } else if (context.phase === Phase.Mounted) {
161
+ * // Handler called after component is fully mounted
162
+ * }
163
+ * });
164
+ * ```
165
+ */
166
+ export declare enum Phase {
167
+ /** Component is in the process of mounting (before useLayoutEffect completes). */
168
+ Mounting = "mounting",
169
+ /** Component has fully mounted (after useLayoutEffect). */
170
+ Mounted = "mounted",
171
+ /** Component is in the process of unmounting. */
172
+ Unmounting = "unmounting",
173
+ /** Component has fully unmounted. */
174
+ Unmounted = "unmounted"
175
+ }
176
+ /**
177
+ * Primary key type for identifying entities in collections.
178
+ * Can be undefined (not yet assigned), a symbol (temporary/local), or a concrete value T.
179
+ *
180
+ * @template T - The concrete primary key type (e.g., string, number)
181
+ */
182
+ export type Pk<T> = undefined | symbol | T;
183
+ /**
184
+ * Base constraint type for model state objects.
185
+ * Models must be plain objects with string keys.
186
+ *
187
+ * @template M - The specific model shape
188
+ */
189
+ export type Model<M = Record<string, unknown>> = M;
190
+ /**
191
+ * Branded type for action objects created with `Action()`.
192
+ * The phantom type parameters carry the payload and channel types at the type level.
193
+ *
194
+ * Actions wrap an internal symbol (used as event emitter keys) in a callable object.
195
+ * When a channel type is specified, the action can be called to create a channeled dispatch.
196
+ *
197
+ * @template P - The payload type for the action
198
+ * @template C - The channel type for channeled dispatches (defaults to never = no channel)
199
+ *
200
+ * @example
201
+ * ```ts
202
+ * // Action without channel support
203
+ * const Increment = Action<number>("Increment");
204
+ * dispatch(Increment, 5);
205
+ *
206
+ * // Action with channel support
207
+ * const UserUpdated = Action<User, { UserId: number }>("UserUpdated");
208
+ * dispatch(UserUpdated, user); // broadcast to all handlers
209
+ * dispatch(UserUpdated({ UserId: 5 }), user); // channeled dispatch
210
+ * ```
211
+ */
212
+ export type HandlerPayload<P = unknown, C extends Filter = never> = {
213
+ readonly [Brand.Action]: symbol;
214
+ readonly [Brand.Payload]: P;
215
+ readonly [Brand.Broadcast]?: boolean;
216
+ } & ([C] extends [never] ? unknown : {
217
+ (channel: C): ChanneledAction<P, C>;
218
+ });
219
+ /**
220
+ * Result of calling an action with a channel argument.
221
+ * Contains the action reference and the channel data for filtered dispatch.
222
+ *
223
+ * @template P - The payload type for the action
224
+ * @template C - The channel type
225
+ *
226
+ * @example
227
+ * ```ts
228
+ * const UserUpdated = Action<User, { UserId: number }>("UserUpdated");
229
+ *
230
+ * // UserUpdated({ UserId: 5 }) returns ChanneledAction<User, { UserId: number }>
231
+ * dispatch(UserUpdated({ UserId: 5 }), user);
232
+ * ```
233
+ */
234
+ export type ChanneledAction<P = unknown, C = unknown> = {
235
+ readonly [Brand.Action]: symbol;
236
+ readonly [Brand.Payload]: P;
237
+ readonly [Brand.Channel]: C;
238
+ readonly channel: C;
239
+ };
240
+ /**
241
+ * Branded type for broadcast action objects created with `Action()` and `Distribution.Broadcast`.
242
+ * Broadcast actions are sent to all mounted components. Values are cached so that
243
+ * late-mounting components receive the most recent payload.
244
+ *
245
+ * Late-mounting components receive the most recent cached payload via their
246
+ * `useAction` handler during mount. Use `peek()` in a `Lifecycle.Mount` handler
247
+ * to check whether a cached value exists before performing default fetches.
248
+ *
249
+ * This type extends `HandlerPayload<P, C>` with an additional brand to enforce at compile-time
250
+ * that only broadcast actions can be passed to `context.actions.resolution()`.
251
+ *
252
+ * @template P - The payload type for the action
253
+ * @template C - The channel type for channeled dispatches (defaults to never)
254
+ *
255
+ * @example
256
+ * ```ts
257
+ * const SignedOut = Action<User>("SignedOut", Distribution.Broadcast);
258
+ *
259
+ * // Resolve the latest value inside a handler
260
+ * const user = await context.actions.resolution(SignedOut);
261
+ * ```
262
+ */
263
+ export type BroadcastPayload<P = unknown, C extends Filter = never> = HandlerPayload<P, C> & {
264
+ readonly [Brand.Broadcast]: true;
265
+ };
266
+ /**
267
+ * Branded type for multicast action objects created with `Action()` and `Distribution.Multicast`.
268
+ * Multicast actions are dispatched to all components within a named scope boundary.
269
+ *
270
+ * When dispatching a multicast action, you MUST provide the scope name as the third argument:
271
+ * ```ts
272
+ * actions.dispatch(Actions.Multicast.Update, payload, { scope: Actions.Multicast.Scope });
273
+ * ```
274
+ *
275
+ * Components receive multicast events only if they are descendants of a `<Scope of={...}>`.
276
+ *
277
+ * @template P - The payload type for the action
278
+ * @template C - The channel type for channeled dispatches (defaults to never)
279
+ *
280
+ * @example
281
+ * ```tsx
282
+ * export enum Scope {
283
+ * Counter = "counter",
284
+ * }
285
+ *
286
+ * class MulticastActions {
287
+ * static Update = Action<number>("Update", Distribution.Multicast(Scope.Counter));
288
+ * }
289
+ *
290
+ * // Reference from component-level Actions
291
+ * class Actions {
292
+ * static Multicast = MulticastActions;
293
+ * }
294
+ *
295
+ * // Wrap the subtree where the scope applies via the withScope HOC.
296
+ * export default withScope(Scope.Counter, function Counters() {
297
+ * return (
298
+ * <>
299
+ * <CounterA />
300
+ * <CounterB />
301
+ * </>
302
+ * );
303
+ * });
304
+ *
305
+ * // Dispatch — the scope is read from the action itself.
306
+ * actions.dispatch(Actions.Multicast.Update, 42);
307
+ * ```
308
+ */
309
+ export type MulticastPayload<P = unknown, C extends Filter = never> = HandlerPayload<P, C> & {
310
+ readonly [Brand.Multicast]: true;
311
+ };
312
+ /**
313
+ * Extracts the payload type `P` from a `HandlerPayload<P>` or `ChanneledAction<P, C>`.
314
+ * Use this in handler signatures to get the action's payload type.
315
+ *
316
+ * Works with both plain actions and channeled actions:
317
+ * - `Payload<Action<User>>` → `User`
318
+ * - `Payload<ChanneledAction<User, { UserId: number }>>` → `User`
319
+ *
320
+ * @template A - The action type (HandlerPayload or ChanneledAction)
321
+ */
322
+ export type Payload<A> = A extends {
323
+ readonly [Brand.Payload]: infer P;
324
+ } ? P : never;
325
+ /**
326
+ * Filter object for channeled actions.
327
+ * Must be an object where each value is a non-nullable primitive.
328
+ *
329
+ * By convention, use uppercase keys (e.g., `{UserId: 4}` not `{userId: 4}`)
330
+ * to distinguish filter keys from payload properties.
331
+ *
332
+ * When dispatching, handlers are invoked if ALL properties in the dispatch filter
333
+ * match the corresponding properties in the registered filter.
334
+ *
335
+ * @example
336
+ * ```ts
337
+ * // Register a handler for a specific user
338
+ * actions.useAction([Actions.User, { UserId: 1 }], handler);
339
+ *
340
+ * // Dispatch matches if all dispatch properties match registered properties
341
+ * dispatch([Actions.User, { UserId: 1 }], payload); // Matches
342
+ * dispatch([Actions.User, { UserId: 2 }], payload); // No match
343
+ * dispatch([Actions.User, { UserId: 1, Role: "admin" }], payload); // Matches
344
+ * dispatch([Actions.User, {}], payload); // Matches all
345
+ * dispatch(Actions.User, payload); // Matches ALL handlers
346
+ * ```
347
+ */
348
+ export type Filter = Record<string, string | number | bigint | boolean | symbol>;
349
+ /**
350
+ * Union type representing either a plain action or a channeled action.
351
+ * Used in `useAction` and `dispatch` signatures to accept both forms.
352
+ *
353
+ * @template A - The action type
354
+ *
355
+ * @example
356
+ * ```ts
357
+ * class Actions {
358
+ * static UserUpdated = Action<User, { UserId: number }>("UserUpdated", Distribution.Broadcast);
359
+ * }
360
+ *
361
+ * // Subscribe to updates for a specific user (channeled)
362
+ * actions.useAction(Actions.UserUpdated({ UserId: props.userId }), (context, user) => {
363
+ * context.actions.produce((draft) => {
364
+ * draft.model.user = user;
365
+ * });
366
+ * });
367
+ *
368
+ * // Dispatch to specific user (channeled)
369
+ * actions.dispatch(Actions.UserUpdated({ UserId: user.id }), user);
370
+ *
371
+ * // Dispatch to ALL handlers (plain)
372
+ * actions.dispatch(Actions.UserUpdated, user);
373
+ * ```
374
+ */
375
+ export type ActionOrChanneled<A extends HandlerPayload = HandlerPayload> = A | ChanneledAction;
376
+ /**
377
+ * Checks if a function type returns a Promise.
378
+ * @internal
379
+ */
380
+ type IsAsync<F> = F extends (...args: unknown[]) => Promise<unknown> ? true : false;
381
+ /**
382
+ * Type guard that produces a compile-time error if an async function is passed.
383
+ * Used to enforce synchronous callbacks in `produce()`.
384
+ * @internal
385
+ */
386
+ type AssertSync<F> = IsAsync<F> extends true ? "Error: async functions are not allowed in produce" : F;
387
+ /**
388
+ * Base type for data props passed to useActions.
389
+ * Represents any object that can be captured as reactive data.
390
+ */
391
+ export type Props = Record<string, unknown>;
392
+ /**
393
+ * Constraint type for action containers.
394
+ * Actions are symbols grouped in an object (typically a class with static properties).
395
+ */
396
+ export type Actions = object;
397
+ /**
398
+ * Internal result container for tracking Immertation processes during action execution.
399
+ * @internal
400
+ */
401
+ export type Result = {
402
+ processes: Set<Process>;
403
+ };
404
+ export type HandlerContext<M extends Model | void, _AC extends Actions | void, D extends Props = Props> = {
405
+ readonly model: Readonly<M>;
406
+ /**
407
+ * The current lifecycle phase of the component.
408
+ * Useful for determining if the handler was called during mount (e.g., from a cached
409
+ * distributed action value) vs after the component is fully mounted.
410
+ *
411
+ * @example
412
+ * ```ts
413
+ * actions.useAction(Actions.Broadcast.Counter, (context, payload) => {
414
+ * if (context.phase === Phase.Mounting) {
415
+ * // Called with cached value during mount
416
+ * console.log("Received cached value:", payload);
417
+ * }
418
+ * });
419
+ * ```
420
+ */
421
+ readonly phase: Phase;
422
+ /**
423
+ * The current task for the executing action handler.
424
+ * Contains the AbortController, action identifier, and payload for this specific invocation.
425
+ *
426
+ * Use `task.controller.signal` to check if the action was aborted, or `task.controller.abort()` to cancel it.
427
+ * The `task.action` and `task.payload` properties identify which action triggered this handler.
428
+ *
429
+ * @example
430
+ * ```ts
431
+ * actions.useAction(Actions.Fetch, async (context) => {
432
+ * const response = await fetch("/api", {
433
+ * signal: context.task.controller.signal,
434
+ * });
435
+ *
436
+ * if (context.task.controller.signal.aborted) return;
437
+ *
438
+ * context.actions.produce((draft) => {
439
+ * draft.model.data = response;
440
+ * });
441
+ * });
442
+ * ```
443
+ */
444
+ readonly task: Task;
445
+ /**
446
+ * Reactive data values passed to useActions.
447
+ * Always returns the latest values, even after awaits in async handlers.
448
+ *
449
+ * @example
450
+ * ```ts
451
+ * const [name, setName] = useState("Adam");
452
+ * const actions = useActions<Model, typeof Actions>(model, () => ({ name }));
453
+ *
454
+ * actions.useAction(Actions.Fetch, async (context) => {
455
+ * await fetch("/api");
456
+ * // context.data.name is always the latest value
457
+ * console.log(context.data.name);
458
+ * });
459
+ * ```
460
+ */
461
+ readonly data: D;
462
+ /**
463
+ * Set of all running tasks across all components in the context.
464
+ * Tasks are ordered by creation time (oldest first).
465
+ *
466
+ * Each task contains:
467
+ * - `controller`: The AbortController to cancel this task
468
+ * - `action`: The action identifier that triggered this task
469
+ * - `payload`: The payload passed when the action was dispatched
470
+ *
471
+ * @example
472
+ * ```ts
473
+ * // Abort all tasks for a specific action
474
+ * for (const runningTask of context.tasks) {
475
+ * if (runningTask.action === Actions.Fetch) {
476
+ * runningTask.controller.abort();
477
+ * }
478
+ * }
479
+ *
480
+ * // Abort the oldest task
481
+ * const oldest = context.tasks.values().next().value;
482
+ * oldest?.controller.abort();
483
+ *
484
+ * // Abort all tasks except the current one
485
+ * for (const runningTask of context.tasks) {
486
+ * if (runningTask !== context.task) {
487
+ * runningTask.controller.abort();
488
+ * }
489
+ * }
490
+ * ```
491
+ */
492
+ readonly tasks: ReadonlySet<Task>;
493
+ readonly actions: {
494
+ produce<F extends (draft: {
495
+ model: M;
496
+ readonly inspect: Readonly<Inspect<M>>;
497
+ }) => void>(ƒ: F & AssertSync<F>): void;
498
+ dispatch(action: ActionOrChanneled, payload?: unknown): Promise<void>;
499
+ annotate<T>(value: T, operation?: Operation): T;
500
+ /**
501
+ * Returns the resolved broadcast or multicast value, waiting for any
502
+ * pending annotations to settle before resolving.
503
+ *
504
+ * If a value has already been dispatched it resolves immediately.
505
+ * Otherwise it waits until the next dispatch of the action.
506
+ * Resolves with `null` if the task is aborted before a value arrives.
507
+ *
508
+ * @param action - The broadcast or multicast action to resolve. Multicast
509
+ * actions read their scope from the action declaration.
510
+ * @returns The dispatched value, or `null` if aborted.
511
+ *
512
+ * @example
513
+ * ```ts
514
+ * actions.useAction(Actions.FetchPosts, async (context) => {
515
+ * const user = await context.actions.resolution(Actions.Broadcast.User);
516
+ * if (!user) return;
517
+ * const posts = await fetchPosts(user.id, {
518
+ * signal: context.task.controller.signal,
519
+ * });
520
+ * context.actions.produce(({ model }) => { model.posts = posts; });
521
+ * });
522
+ * ```
523
+ */
524
+ resolution<T>(action: BroadcastPayload<T> | MulticastPayload<T>): Promise<T | null>;
525
+ /**
526
+ * Returns the latest broadcast or multicast value immediately without
527
+ * waiting for annotations to settle. Use this when you need the current
528
+ * cached value and do not need to wait for pending operations to complete.
529
+ *
530
+ * @param action - The broadcast or multicast action to peek at. Multicast
531
+ * actions read their scope from the action declaration.
532
+ * @returns The cached value, or `null` if no value has been dispatched.
533
+ *
534
+ * @example
535
+ * ```ts
536
+ * actions.useAction(Actions.Check, (context) => {
537
+ * const user = context.actions.peek(Actions.Broadcast.User);
538
+ * if (!user) return;
539
+ * console.log(user.name);
540
+ * });
541
+ * ```
542
+ */
543
+ peek<T>(action: BroadcastPayload<T> | MulticastPayload<T>): T | null;
544
+ };
545
+ };
546
+ /**
547
+ * Return type for the useActions hook.
548
+ *
549
+ * A tuple containing:
550
+ * 1. The current model state of type M
551
+ * 2. An actions object with dispatch and inspect capabilities
552
+ *
553
+ * @template M - The model type representing the component's state
554
+ * @template AC - The actions class containing action definitions
555
+ *
556
+ * @example
557
+ * ```tsx
558
+ * const [model, actions] = useActions<typeof Actions>(initialModel);
559
+ *
560
+ * // Access state
561
+ * model.count;
562
+ *
563
+ * // Dispatch actions
564
+ * actions.dispatch(Actions.Increment, 5);
565
+ *
566
+ * // Check pending state
567
+ * actions.inspect.count.pending();
568
+ * ```
569
+ */
570
+ /**
571
+ * Utility type for defining a single action handler function.
572
+ * Use this when you need to type a specific handler directly.
573
+ *
574
+ * @template M - The model type
575
+ * @template AC - The actions class type
576
+ * @template K - The action key (keyof AC) — determines payload type via lookup
577
+ * @template D - Optional data/props type (defaults to Props)
578
+ *
579
+ * @see {@link Handlers} for the recommended HKT pattern
580
+ */
581
+ export type Handler<M extends Model | void, AC extends Actions | void, K extends keyof AC & string, D extends Props = Props> = (context: HandlerContext<M, AC, D>, ...args: [Payload<AC[K] & HandlerPayload<unknown>>] extends [never] ? [] : [payload: Payload<AC[K] & HandlerPayload<unknown>>]) => void | Promise<void> | AsyncGenerator | Generator;
582
+ /**
583
+ * String keys of `AC` excluding inherited `prototype` from class constructors.
584
+ * When action containers are classes (`typeof MyActions`), TypeScript includes
585
+ * `"prototype"` in `keyof`. Excluding it prevents `prototype` from appearing
586
+ * as a handler key and avoids recursion into Function internals.
587
+ */
588
+ type OwnKeys<AC> = Exclude<keyof AC & string, "prototype">;
589
+ /**
590
+ * Recursive mapped type for action handlers that mirrors the action class hierarchy.
591
+ *
592
+ * For leaf actions (values with no own string keys, i.e. `HandlerPayload`), produces
593
+ * a handler function signature. For namespace objects (containing nested actions),
594
+ * produces a nested `Handlers` object.
595
+ *
596
+ * Access handlers using bracket notation matching the action structure:
597
+ *
598
+ * @template M - The model type
599
+ * @template AC - The actions class type
600
+ * @template D - Optional data/props type (defaults to Props)
601
+ *
602
+ * @example
603
+ * ```ts
604
+ * import { Action, Distribution, Handlers } from "march-hare";
605
+ *
606
+ * class BroadcastActions {
607
+ * static PaymentSent = Action("PaymentSent", Distribution.Broadcast);
608
+ * static PaymentLink = Action<PaymentLinkData>("PaymentLink", Distribution.Broadcast);
609
+ * }
610
+ *
611
+ * class Actions {
612
+ * static SetName = Action<string>("SetName");
613
+ * static Broadcast = BroadcastActions;
614
+ * }
615
+ *
616
+ * type H = Handlers<Model, typeof Actions>;
617
+ *
618
+ * // Flat actions
619
+ * export const handleSetName: H["SetName"] = (context, name) => { ... };
620
+ *
621
+ * // Nested actions use chained bracket notation
622
+ * export const handlePaymentSent: H["Broadcast"]["PaymentSent"] = (context) => { ... };
623
+ * ```
624
+ */
625
+ export type Handlers<M extends Model | void, AC extends Actions | void, D extends Props = Props> = {
626
+ [K in OwnKeys<AC>]: OwnKeys<AC[K]> extends never ? (context: HandlerContext<M, AC, D>, ...args: [Payload<AC[K] & HandlerPayload<unknown>>] extends [never] ? [] : [payload: Payload<AC[K] & HandlerPayload<unknown>>]) => void | Promise<void> | AsyncGenerator | Generator : Handlers<M, AC[K] & Actions, D>;
627
+ };
628
+ export type UseActions<M extends Model | void, AC extends Actions | void, D extends Props = Props> = [
629
+ Readonly<M>,
630
+ {
631
+ /**
632
+ * Dispatches an action with an optional payload. Multicast actions read
633
+ * their scope from the action declaration, so no extra options are
634
+ * required at the call site.
635
+ */
636
+ dispatch<P>(action: HandlerPayload<P>, payload?: P): Promise<void>;
637
+ dispatch<P>(action: BroadcastPayload<P>, payload?: P): Promise<void>;
638
+ dispatch<P>(action: MulticastPayload<P>, payload?: P): Promise<void>;
639
+ dispatch<P, C extends Filter>(action: ChanneledAction<P, C>, payload?: P): Promise<void>;
640
+ inspect: Inspect<M>;
641
+ /**
642
+ * Streams broadcast values declaratively in JSX using a render-prop pattern.
643
+ *
644
+ * Subscribes to the given broadcast action and re-renders when a new value
645
+ * is dispatched. Returns `null` until the first dispatch. The renderer
646
+ * receives the value and an inspect proxy for annotation tracking.
647
+ *
648
+ * @param action - The broadcast action to subscribe to.
649
+ * @param renderer - Callback that receives value and inspect, returns React nodes.
650
+ * @returns React nodes from the renderer, or null if no value has been dispatched.
651
+ *
652
+ * @example
653
+ * ```tsx
654
+ * return (
655
+ * <div>
656
+ * {actions.stream(Actions.Broadcast.User, (user, inspect) => (
657
+ * <span>{user.name}</span>
658
+ * ))}
659
+ * </div>
660
+ * );
661
+ * ```
662
+ */
663
+ stream<T extends object>(action: BroadcastPayload<T>, renderer: (value: T, inspect: Inspect<T>) => React.ReactNode): React.ReactNode;
664
+ }
665
+ ] & {
666
+ /**
667
+ * Registers an action handler with the current scope.
668
+ * Types are pre-baked from the useActions call, so no type parameter is needed.
669
+ *
670
+ * Supports two subscription patterns:
671
+ * 1. **Plain action** - Receives ALL dispatches for that action (including channeled ones)
672
+ * 2. **Channeled action** `Action(channel)` - Receives only dispatches matching the channel
673
+ *
674
+ * @param action - The action or channeled action (e.g., `Action({ UserId: 1 })`)
675
+ * @param handler - The handler function receiving context and payload
676
+ *
677
+ * @example
678
+ * ```ts
679
+ * const actions = useActions<typeof Actions>(model);
680
+ *
681
+ * // Subscribe to ALL UserUpdated events
682
+ * actions.useAction(Actions.UserUpdated, (context, user) => {
683
+ * // Fires for any UserUpdated dispatch
684
+ * });
685
+ *
686
+ * // Subscribe to UserUpdated for a specific user only (channeled)
687
+ * actions.useAction(Actions.UserUpdated({ UserId: props.userId }), (context, user) => {
688
+ * // Only fires when dispatched with matching channel
689
+ * });
690
+ * ```
691
+ */
692
+ useAction<A extends ActionId | HandlerPayload | ChanneledAction>(action: A, handler: (context: HandlerContext<M, AC, D>, ...args: [Payload<A>] extends [never] ? [] : [payload: Payload<A>]) => void | Promise<void> | AsyncGenerator | Generator): void;
693
+ /**
694
+ * Connects a {@link Resource} declared at module scope to this component.
695
+ * Returns a frozen `{ run, data, at }` object &ndash; `run` triggers a
696
+ * fresh network call (concurrent calls share the in-flight promise),
697
+ * while `data` and `at` are read-only snapshots of the most recent
698
+ * successful payload and the instant it resolved.
699
+ *
700
+ * `data` and `at` are non-reactive &mdash; reading them does not
701
+ * subscribe the component to updates. Drive UI from the model.
702
+ *
703
+ * @example
704
+ * ```ts
705
+ * const user = actions.useResource(resources.user);
706
+ *
707
+ * actions.useAction(Actions.Mount, async (context) => {
708
+ * const data = await user.run();
709
+ * context.actions.produce(({ model }) => { model.user = data; });
710
+ * });
711
+ * ```
712
+ */
713
+ useResource<T, P extends object>(resource: import('../resource/index.ts').ResourceHandle<T, P>): Readonly<{
714
+ run: import('../resource/index.ts').BoundRun<T, P>;
715
+ data: T | null;
716
+ at: Temporal.Instant | null;
717
+ }>;
718
+ };