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.
- package/README.md +453 -0
- package/dist/march-hare.js +6 -0
- package/dist/march-hare.umd.cjs +1 -0
- package/dist/src/library/action/index.d.ts +66 -0
- package/dist/src/library/action/utils.d.ts +89 -0
- package/dist/src/library/annotate/index.d.ts +25 -0
- package/dist/src/library/boundary/components/broadcast/index.d.ts +12 -0
- package/dist/src/library/boundary/components/broadcast/types.d.ts +19 -0
- package/dist/src/library/boundary/components/broadcast/utils.d.ts +39 -0
- package/dist/src/library/boundary/components/consumer/components/partition/index.d.ts +27 -0
- package/dist/src/library/boundary/components/consumer/components/partition/types.d.ts +9 -0
- package/dist/src/library/boundary/components/consumer/index.d.ts +19 -0
- package/dist/src/library/boundary/components/consumer/types.d.ts +37 -0
- package/dist/src/library/boundary/components/consumer/utils.d.ts +13 -0
- package/dist/src/library/boundary/components/mode/index.d.ts +15 -0
- package/dist/src/library/boundary/components/mode/types.d.ts +7 -0
- package/dist/src/library/boundary/components/mode/utils.d.ts +55 -0
- package/dist/src/library/boundary/components/scope/index.d.ts +40 -0
- package/dist/src/library/boundary/components/scope/types.d.ts +20 -0
- package/dist/src/library/boundary/components/scope/utils.d.ts +19 -0
- package/dist/src/library/boundary/components/tasks/index.d.ts +14 -0
- package/dist/src/library/boundary/components/tasks/types.d.ts +43 -0
- package/dist/src/library/boundary/components/tasks/utils.d.ts +26 -0
- package/dist/src/library/boundary/index.d.ts +20 -0
- package/dist/src/library/boundary/types.d.ts +4 -0
- package/dist/src/library/error/index.d.ts +2 -0
- package/dist/src/library/error/types.d.ts +75 -0
- package/dist/src/library/error/utils.d.ts +15 -0
- package/dist/src/library/hooks/index.d.ts +43 -0
- package/dist/src/library/hooks/types.d.ts +72 -0
- package/dist/src/library/hooks/utils.d.ts +198 -0
- package/dist/src/library/index.d.ts +16 -0
- package/dist/src/library/resource/index.d.ts +99 -0
- package/dist/src/library/types/index.d.ts +718 -0
- package/dist/src/library/utils/index.d.ts +42 -0
- package/dist/src/library/utils/utils.d.ts +5 -0
- package/dist/src/library/utils.d.ts +37 -0
- 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** – Action is scoped to the component that defines it and cannot be
|
|
121
|
+
* consumed by other components. This is the default behaviour.
|
|
122
|
+
* - **Broadcast** – Action is distributed to all mounted components that have
|
|
123
|
+
* defined a handler for it. Values are cached for late-mounting components.
|
|
124
|
+
* - **Multicast** – 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 – `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 — 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
|
+
};
|