march-hare 0.8.0 → 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 +480 -209
- package/dist/{hooks → actions}/index.d.ts +2 -39
- 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 -13
- 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 +77 -39
- 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
package/dist/resource/types.d.ts
CHANGED
|
@@ -1,27 +1,100 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Env } from '../boundary/components/env/index';
|
|
2
|
+
import { Cache } from '../cache/index';
|
|
3
|
+
import { BroadcastPayload, MulticastPayload, Filter } from '../types/index';
|
|
2
4
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
+
* Dispatch surface exposed on a Resource fetcher's `context`. Restricted
|
|
6
|
+
* to broadcast and multicast actions — unicast targets the calling
|
|
7
|
+
* component, which a Resource fetcher does not have.
|
|
8
|
+
*/
|
|
9
|
+
export type Dispatch = {
|
|
10
|
+
<C extends Filter = never>(action: BroadcastPayload<never, C> | MulticastPayload<never, C>): Promise<void>;
|
|
11
|
+
<P, C extends Filter = never>(action: BroadcastPayload<P, C> | MulticastPayload<P, C>, payload: P): Promise<void>;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* `context` object passed to every {@link Fetcher}.
|
|
5
15
|
*
|
|
6
|
-
* - `
|
|
16
|
+
* - `env` — snapshot of the per-`<Boundary>` Env at the
|
|
7
17
|
* moment the fetcher is invoked.
|
|
8
18
|
* - `controller` — the `AbortController` auto-threaded from the
|
|
9
19
|
* calling handler's `context.task.controller`. Pass
|
|
10
20
|
* `controller.signal` to `fetch`/`ky`/`EventSource`, or call
|
|
11
21
|
* `controller.abort()` to fail fast.
|
|
12
22
|
* - `params` — the call-site params object. Defaults to `{}`.
|
|
23
|
+
* - `dispatch` — fire broadcast or multicast actions from inside
|
|
24
|
+
* the fetcher. Unicast is rejected at compile time.
|
|
13
25
|
*
|
|
14
26
|
* @internal
|
|
15
27
|
*/
|
|
16
28
|
export type Args<P extends object = Record<never, never>> = {
|
|
17
|
-
readonly
|
|
29
|
+
readonly env: Env;
|
|
18
30
|
readonly controller: AbortController;
|
|
19
31
|
readonly params: P;
|
|
32
|
+
readonly dispatch: Dispatch;
|
|
20
33
|
};
|
|
21
34
|
/**
|
|
22
|
-
* Fetcher signature accepted by `Resource`. Receives
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
|
|
35
|
+
* Fetcher signature accepted by `Resource`. Receives a single `context`
|
|
36
|
+
* argument carrying the Env snapshot, the abort controller, params,
|
|
37
|
+
* and a broadcast/multicast-only `dispatch`.
|
|
38
|
+
*/
|
|
39
|
+
export type Fetcher<T, P extends object = Record<never, never>> = (context: Args<P>) => Promise<T>;
|
|
40
|
+
/**
|
|
41
|
+
* Per-call coalescing token. Two callers with the same Resource, same
|
|
42
|
+
* structural params, and equal `Coalesce` value share a single in-flight
|
|
43
|
+
* promise; different tokens (or different params) fire independent
|
|
44
|
+
* fetches. Primitives compose naturally via stringification; objects
|
|
45
|
+
* are serialised with `JSON.stringify`.
|
|
46
|
+
*/
|
|
47
|
+
export type Coalesce = string | number | bigint | boolean | symbol | object;
|
|
48
|
+
/**
|
|
49
|
+
* Config form accepted by `Resource`. The fetcher shorthand
|
|
50
|
+
* `Resource(fetcher)` is equivalent to `Resource({ fetch: fetcher })`.
|
|
51
|
+
*
|
|
52
|
+
* - `fetch` — the fetcher.
|
|
53
|
+
* - `cache` — persist successful payloads via a {@link Cache}
|
|
54
|
+
* wired to an `Adapter` (localStorage, MMKV, etc). Omit for an
|
|
55
|
+
* in-memory cache scoped to this Resource.
|
|
56
|
+
*/
|
|
57
|
+
export type Config<T, P extends object = Record<never, never>> = {
|
|
58
|
+
readonly fetch: Fetcher<T, P>;
|
|
59
|
+
readonly cache?: Cache;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Snapshot of the most recent resource invocation. `resource.cat(params)`
|
|
63
|
+
* writes one of these into a module-scope slot; the next
|
|
64
|
+
* `context.actions.resource(...)` / `.set(...)` call consumes it via
|
|
65
|
+
* `consumePending` and then clears the slot.
|
|
66
|
+
*
|
|
67
|
+
* @internal
|
|
68
|
+
*/
|
|
69
|
+
export type PendingCall = {
|
|
70
|
+
readonly run: (env: Env, controller: AbortController, params: object, dispatch: Dispatch) => Promise<unknown>;
|
|
71
|
+
readonly read: (params: object) => {
|
|
72
|
+
data: unknown;
|
|
73
|
+
at: Temporal.Instant | null;
|
|
74
|
+
};
|
|
75
|
+
readonly seed: (params: object, data: unknown, at: Temporal.Instant) => void;
|
|
76
|
+
readonly params: object;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Resource handle returned by `Resource(...)` or `Resource.Cachable(...)`.
|
|
80
|
+
* Call it with `params` to read the per-params cache slot synchronously
|
|
81
|
+
* and prime the slot consumed by `context.actions.resource(...)` for a
|
|
82
|
+
* follow-up fetch or `context.actions.resource.set(...)` for an
|
|
83
|
+
* out-of-band write.
|
|
84
|
+
*
|
|
85
|
+
* ```ts
|
|
86
|
+
* // Sync cache read in a model literal.
|
|
87
|
+
* { cat: resource.cat({ id: 5 }) }
|
|
88
|
+
*
|
|
89
|
+
* // Fetch with `.exceeds(...)` for cache-aware refresh.
|
|
90
|
+
* await context.actions
|
|
91
|
+
* .resource(resource.cat({ id: 5 }))
|
|
92
|
+
* .exceeds({ minutes: 5 });
|
|
93
|
+
*
|
|
94
|
+
* // Write through to the per-params cache slot.
|
|
95
|
+
* context.actions.resource.set(resource.cat({ id: 5 }), data);
|
|
96
|
+
* ```
|
|
26
97
|
*/
|
|
27
|
-
export type
|
|
98
|
+
export type ResourceHandle<T, P extends object = Record<never, never>> = [
|
|
99
|
+
keyof P
|
|
100
|
+
] extends [never] ? (params?: P) => T | null : (params: P) => T | null;
|
|
@@ -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
|
/**
|
|
@@ -238,14 +278,14 @@ export declare enum Phase {
|
|
|
238
278
|
*/
|
|
239
279
|
export type Pk<T> = undefined | symbol | T;
|
|
240
280
|
/**
|
|
241
|
-
*
|
|
242
|
-
* `null` / `undefined` while loading, awaiting a fetch, or before
|
|
281
|
+
* Maybe-present field type — a value that may be a concrete `T`,
|
|
282
|
+
* or `null` / `undefined` while loading, awaiting a fetch, or before
|
|
243
283
|
* upstream data has arrived. Use this for model fields whose presence
|
|
244
284
|
* is determined by async or external state.
|
|
245
285
|
*
|
|
246
286
|
* @template T - The concrete value type
|
|
247
287
|
*/
|
|
248
|
-
export type
|
|
288
|
+
export type Maybe<T> = T | null | undefined;
|
|
249
289
|
/**
|
|
250
290
|
* Base constraint type for model state objects.
|
|
251
291
|
* Models must be plain objects with string keys.
|
|
@@ -315,7 +355,7 @@ export type ChanneledAction<P = unknown, C = unknown, Name extends string = stri
|
|
|
315
355
|
* to check whether a cached value exists before performing default fetches.
|
|
316
356
|
*
|
|
317
357
|
* This type extends `HandlerPayload<P, C>` with an additional brand to enforce at compile-time
|
|
318
|
-
* that only broadcast actions can be passed to `context.actions.
|
|
358
|
+
* that only broadcast actions can be passed to `context.actions.final()`.
|
|
319
359
|
*
|
|
320
360
|
* @template P - The payload type for the action
|
|
321
361
|
* @template C - The channel type for channeled dispatches (defaults to never)
|
|
@@ -325,7 +365,7 @@ export type ChanneledAction<P = unknown, C = unknown, Name extends string = stri
|
|
|
325
365
|
* const SignedOut = Action<User>("SignedOut", Distribution.Broadcast);
|
|
326
366
|
*
|
|
327
367
|
* // Resolve the latest value inside a handler
|
|
328
|
-
* const user = await context.actions.
|
|
368
|
+
* const user = await context.actions.final(SignedOut);
|
|
329
369
|
* ```
|
|
330
370
|
*/
|
|
331
371
|
export type BroadcastPayload<P = unknown, C extends Filter = never, Name extends string = string> = HandlerPayload<P, C, Name> & {
|
|
@@ -475,23 +515,21 @@ export type HandlerContext<M extends Model | void, AC extends Actions | void, D
|
|
|
475
515
|
readonly task: Task;
|
|
476
516
|
readonly data: DeepReadonly<D>;
|
|
477
517
|
readonly tasks: ReadonlySet<Task>;
|
|
478
|
-
readonly
|
|
518
|
+
readonly env: DeepReadonly<Env>;
|
|
479
519
|
readonly actions: {
|
|
480
520
|
produce<F extends (draft: {
|
|
481
521
|
model: M;
|
|
482
|
-
|
|
522
|
+
env: Env;
|
|
483
523
|
readonly inspect: Readonly<Inspect<M>>;
|
|
484
524
|
}) => void>(ƒ: F & AssertSync<F>): void;
|
|
485
525
|
dispatch(action: NoPayloadActions<Dispatchable<AC>>): Promise<void>;
|
|
486
526
|
dispatch<A extends WithPayloadActions<Dispatchable<AC>>>(action: A, payload: Payload<A>): Promise<void>;
|
|
487
527
|
annotate<T>(value: T, operation?: Operation): T;
|
|
488
528
|
readonly inspect: Readonly<Inspect<M>>;
|
|
489
|
-
resource: (<T>(invocation: T | null) =>
|
|
490
|
-
readonly exceeds: (duration: Temporal.DurationLike) => Promise<T>;
|
|
491
|
-
}) & {
|
|
529
|
+
resource: (<T>(invocation: T | null) => ResourceCall<T>) & {
|
|
492
530
|
set<T>(invocation: T | null, data: T): void;
|
|
493
531
|
};
|
|
494
|
-
|
|
532
|
+
final<T>(action: BroadcastPayload<T> | MulticastPayload<T>): Promise<T | null>;
|
|
495
533
|
peek<T>(action: BroadcastPayload<T> | MulticastPayload<T>): T | null;
|
|
496
534
|
};
|
|
497
535
|
};
|
|
@@ -557,7 +595,7 @@ type OwnKeys<AC> = Exclude<keyof AC & string, "prototype">;
|
|
|
557
595
|
*
|
|
558
596
|
* Used to constrain `dispatch` and `useAction` so that only actions owned by
|
|
559
597
|
* the component's `AC` (plus the global `Lifecycle.Fault` /
|
|
560
|
-
* `Lifecycle.
|
|
598
|
+
* `Lifecycle.Env`) can be referenced.
|
|
561
599
|
*/
|
|
562
600
|
export type LeafActions<AC> = AC extends void ? never : {
|
|
563
601
|
[K in OwnKeys<AC>]: OwnKeys<AC[K]> extends never ? AC[K] : LeafActions<AC[K]>;
|
|
@@ -576,10 +614,10 @@ export type ChanneledOf<A> = A extends HandlerPayload<infer P, infer C> ? [C] ex
|
|
|
576
614
|
export type Dispatchable<AC> = LeafActions<AC> | ChanneledOf<LeafActions<AC>>;
|
|
577
615
|
/**
|
|
578
616
|
* Everything `useAction` will subscribe to for a given `AC`: same as
|
|
579
|
-
* `Dispatchable<AC>` plus the shared `Lifecycle.Fault` and `Lifecycle.
|
|
617
|
+
* `Dispatchable<AC>` plus the shared `Lifecycle.Fault` and `Lifecycle.Env`
|
|
580
618
|
* broadcasts which live outside `AC` but are subscribable by any component.
|
|
581
619
|
*/
|
|
582
|
-
export type Subscribable<AC> = Dispatchable<AC> | typeof Lifecycle.Fault | typeof Lifecycle.
|
|
620
|
+
export type Subscribable<AC> = Dispatchable<AC> | typeof Lifecycle.Fault | typeof Lifecycle.Env;
|
|
583
621
|
/**
|
|
584
622
|
* Subset of a union of actions whose payload type is `never`. Used to split
|
|
585
623
|
* `dispatch`/`useAction` into a no-payload and a with-payload overload so
|
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
|
/**
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Actions, HandlerContext, Model, Props } from '../types/index';
|
|
2
|
+
/**
|
|
3
|
+
* Handler factories that wire an action directly to a model field.
|
|
4
|
+
*
|
|
5
|
+
* - {@link With.Update} assigns the dispatched payload to `model[key]`.
|
|
6
|
+
* - {@link With.Invert} flips a boolean field on `model[key]`.
|
|
7
|
+
*
|
|
8
|
+
* Both are typed so the call site fails to compile when `key` is missing or
|
|
9
|
+
* has an incompatible type.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { With } from "march-hare";
|
|
14
|
+
*
|
|
15
|
+
* type Model = { name: string; sidebar: boolean };
|
|
16
|
+
*
|
|
17
|
+
* class Actions {
|
|
18
|
+
* static SetName = Action<string>("SetName");
|
|
19
|
+
* static ToggleSidebar = Action("ToggleSidebar");
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* actions.useAction(Actions.SetName, With.Update("name"));
|
|
23
|
+
* actions.useAction(Actions.ToggleSidebar, With.Invert("sidebar"));
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare const With: {
|
|
27
|
+
/**
|
|
28
|
+
* Returns a handler that assigns the action payload to `model[key]`.
|
|
29
|
+
*
|
|
30
|
+
* Type-checks at the call site: the payload type must be assignable to
|
|
31
|
+
* the model property's type, and the key must exist on the model.
|
|
32
|
+
*/
|
|
33
|
+
Update<K extends string>(key: K): <M extends Model, A extends Actions | void, D extends Props, P extends K extends keyof M ? M[K] : never>(context: HandlerContext<M, A, D>, payload: P) => void;
|
|
34
|
+
/**
|
|
35
|
+
* Returns a handler that inverts a boolean field on the model.
|
|
36
|
+
*
|
|
37
|
+
* Type-checks at the call site: `model[key]` must be a boolean.
|
|
38
|
+
*/
|
|
39
|
+
Invert<K extends string>(key: K): <M extends Model & Record<K, boolean>, A extends Actions | void, D extends Props>(context: HandlerContext<M, A, D>) => void;
|
|
40
|
+
};
|
package/package.json
CHANGED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { Props } from './types';
|
|
2
|
-
import * as React from "react";
|
|
3
|
-
export { useStore } from './utils';
|
|
4
|
-
/**
|
|
5
|
-
* App-wide store of cross-cutting, mutable state. The interface is
|
|
6
|
-
* declared empty here and **augmented** by consumer code via module
|
|
7
|
-
* augmentation:
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* ```ts
|
|
11
|
-
* declare module "march-hare" {
|
|
12
|
-
* interface Store {
|
|
13
|
-
* session: Session | null;
|
|
14
|
-
* locale: string;
|
|
15
|
-
* featureFlags: Record<string, boolean>;
|
|
16
|
-
* }
|
|
17
|
-
* }
|
|
18
|
-
* ```
|
|
19
|
-
*
|
|
20
|
-
* Every key declared here flows into:
|
|
21
|
-
*
|
|
22
|
-
* - `useStore()` — the per-`<Boundary>` handle for reads and writes.
|
|
23
|
-
* - `context.store` inside `useActions` handlers.
|
|
24
|
-
* - The `store` field on every `Resource` fetcher's args object.
|
|
25
|
-
*
|
|
26
|
-
* The Store is **not** reactive. Mutating it does not re-render. Drive
|
|
27
|
-
* view state through the model; use the Store for cross-handler
|
|
28
|
-
* coordination, session tokens, locale, feature flags, etc.
|
|
29
|
-
*/
|
|
30
|
-
export interface Store {
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Provides a per-Boundary {@link Store} value to every component inside
|
|
34
|
-
* the boundary. Usually wired in via the `<Boundary store={initial}>`
|
|
35
|
-
* prop rather than used directly.
|
|
36
|
-
*
|
|
37
|
-
* The Store is **not** reactive. Mutating it does not trigger a
|
|
38
|
-
* re-render. Drive view state through the model; use the Store for
|
|
39
|
-
* cross-handler coordination.
|
|
40
|
-
*/
|
|
41
|
-
export declare function Store({ initial, children }: Props): React.ReactNode;
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { ReactNode } from 'react';
|
|
2
|
-
import { Store } from './index';
|
|
3
|
-
export type { Store } from './index';
|
|
4
|
-
/**
|
|
5
|
-
* Props for the Store provider component. Accepts the initial Store
|
|
6
|
-
* value that satisfies the augmented {@link Store} interface.
|
|
7
|
-
*/
|
|
8
|
-
export type Props = {
|
|
9
|
-
initial: Store;
|
|
10
|
-
children: ReactNode;
|
|
11
|
-
};
|