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.
- package/README.md +496 -204
- package/dist/{hooks → actions}/index.d.ts +1 -2
- package/dist/{hooks → actions}/utils.d.ts +0 -39
- package/dist/app/index.d.ts +112 -0
- package/dist/app/types.d.ts +49 -0
- package/dist/boundary/components/broadcast/utils.d.ts +1 -1
- package/dist/boundary/components/env/index.d.ts +26 -0
- package/dist/boundary/components/env/types.d.ts +11 -0
- package/dist/boundary/components/env/utils.d.ts +36 -0
- package/dist/boundary/components/scope/index.d.ts +1 -39
- package/dist/boundary/components/scope/types.d.ts +17 -13
- package/dist/boundary/components/scope/utils.d.ts +12 -8
- package/dist/boundary/components/sharing/index.d.ts +43 -0
- package/dist/boundary/index.d.ts +10 -10
- package/dist/boundary/types.d.ts +6 -16
- package/dist/cache/index.d.ts +4 -4
- package/dist/coalesce/index.d.ts +57 -0
- package/dist/context/index.d.ts +39 -0
- package/dist/context/types.d.ts +14 -0
- package/dist/error/index.d.ts +1 -1
- package/dist/error/types.d.ts +8 -19
- package/dist/index.d.ts +8 -12
- package/dist/march-hare.js +7 -5
- package/dist/march-hare.umd.cjs +1 -1
- package/dist/resource/index.d.ts +52 -78
- package/dist/resource/types.d.ts +83 -10
- package/dist/scope/index.d.ts +63 -0
- package/dist/scope/types.d.ts +55 -0
- package/dist/types/index.d.ts +116 -229
- package/dist/utils/index.d.ts +6 -5
- package/dist/with/index.d.ts +40 -0
- package/package.json +1 -1
- package/dist/boundary/components/store/index.d.ts +0 -41
- package/dist/boundary/components/store/types.d.ts +0 -11
- package/dist/boundary/components/store/utils.d.ts +0 -64
- /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 — just render two `<scope.Boundary>`s
|
|
12
|
+
* — 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
|
+
* — 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` — 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` — 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 — just render two `<scope.Boundary>`s
|
|
12
|
+
* — 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
|
+
* — 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` —
|
|
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` — 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
|
+
};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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 {
|
|
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 — 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,
|
|
69
|
+
* projections on `context` (model, data, env) reject direct assignment
|
|
38
70
|
* — 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.
|
|
120
|
+
* Internal symbol for the global `Lifecycle.Env` broadcast. The env
|
|
89
121
|
* mutation path in `useActions` fires this symbol whenever a
|
|
90
|
-
* `produce({
|
|
122
|
+
* `produce({ env })` call changes the slot reference.
|
|
91
123
|
*
|
|
92
124
|
* @internal
|
|
93
125
|
*/
|
|
94
|
-
export declare const
|
|
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.
|
|
146
|
+
* `Lifecycle.Fault` and `Lifecycle.Env` are singleton broadcasts (not
|
|
115
147
|
* factories). All components subscribe to the same shared symbol —
|
|
116
|
-
* `Fault` delivers global fault notifications, `
|
|
117
|
-
*
|
|
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
|
|
150
|
-
* snapshot whenever a `context.actions.produce(({
|
|
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.
|
|
153
|
-
* it directly with `actions.stream(Lifecycle.
|
|
184
|
+
* `actions.useAction(Lifecycle.Env, handler)` — 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
|
|
190
|
+
* streams receive the current env on mount.
|
|
159
191
|
*
|
|
160
192
|
* @example
|
|
161
193
|
* ```tsx
|
|
162
|
-
* actions.useAction(Lifecycle.
|
|
163
|
-
* console.log("
|
|
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.
|
|
168
|
-
* <span>{
|
|
199
|
+
* {actions.stream(Lifecycle.Env, (env) => (
|
|
200
|
+
* <span>{env.locale}</span>
|
|
169
201
|
* ))}
|
|
170
202
|
* ```
|
|
171
203
|
*/
|
|
172
|
-
static
|
|
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** – 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** – Action defines its own scope. Components reach it by
|
|
182
|
-
*
|
|
214
|
+
* rendering inside a `<scope.Boundary>` produced by `app.Scope<MulticastActions>()`.
|
|
183
215
|
*
|
|
184
216
|
* @example
|
|
185
217
|
* ```ts
|
|
186
|
-
* export class
|
|
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
|
|
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(
|
|
196
|
-
* actions.useAction(
|
|
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
|
|
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 — 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.
|
|
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.
|
|
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 — 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
|
-
* — 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
|
-
|
|
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
|
-
|
|
590
|
-
* snapshot auto-threaded from the current handler context. The
|
|
591
|
-
* argument is a resource invocation (`cat({ id: 5 })`) — the
|
|
592
|
-
* call primes a slot with the resource and params, and
|
|
593
|
-
* `.resource(...)` reads it. The return value is a thenable —
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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 — 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` — 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
|
+
};
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -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
|
|
7
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
/**
|