march-hare 0.6.1 → 0.7.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 +89 -96
- package/dist/march-hare.js +6 -6
- package/dist/march-hare.umd.cjs +1 -1
- package/dist/src/library/action/index.d.ts +17 -13
- package/dist/src/library/boundary/components/store/index.d.ts +41 -0
- package/dist/src/library/boundary/components/store/types.d.ts +11 -0
- package/dist/src/library/boundary/components/store/utils.d.ts +64 -0
- package/dist/src/library/boundary/components/tasks/types.d.ts +3 -3
- package/dist/src/library/boundary/index.d.ts +8 -7
- package/dist/src/library/boundary/types.d.ts +18 -0
- package/dist/src/library/cache/index.d.ts +44 -0
- package/dist/src/library/cache/types.d.ts +54 -0
- package/dist/src/library/index.d.ts +8 -6
- package/dist/src/library/resource/index.d.ts +82 -45
- package/dist/src/library/resource/types.d.ts +20 -143
- package/dist/src/library/resource/utils.d.ts +24 -10
- package/dist/src/library/types/index.d.ts +160 -19
- package/dist/src/library/utils/index.d.ts +1 -41
- package/dist/src/library/utils/types.d.ts +3 -86
- package/package.json +2 -2
- package/dist/src/library/boundary/components/mode/index.d.ts +0 -15
- package/dist/src/library/boundary/components/mode/types.d.ts +0 -7
- package/dist/src/library/boundary/components/mode/utils.d.ts +0 -55
|
@@ -1,150 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export type { Unset } from '../utils/index.ts';
|
|
1
|
+
import { Store } from '../boundary/components/store/index.tsx';
|
|
3
2
|
/**
|
|
4
|
-
*
|
|
3
|
+
* Args object passed to every {@link Fetcher}. The fetcher destructures
|
|
4
|
+
* whatever it needs; unused fields can be omitted.
|
|
5
5
|
*
|
|
6
|
-
* - `
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* - `store` — snapshot of the per-`<Boundary>` Store at the
|
|
7
|
+
* moment the fetcher is invoked.
|
|
8
|
+
* - `controller` — the `AbortController` auto-threaded from the
|
|
9
|
+
* calling handler's `context.task.controller`. Pass
|
|
10
|
+
* `controller.signal` to `fetch`/`ky`/`EventSource`, or call
|
|
11
|
+
* `controller.abort()` to fail fast.
|
|
12
|
+
* - `params` — the call-site params object. Defaults to `{}`.
|
|
11
13
|
*
|
|
12
|
-
* @
|
|
13
|
-
* ```ts
|
|
14
|
-
* await user.if({ over: { minutes: 5 } });
|
|
15
|
-
* await user.if({ over: "PT5M" });
|
|
16
|
-
* await user.if({ over: Temporal.Duration.from({ minutes: 5 }) });
|
|
17
|
-
* ```
|
|
14
|
+
* @internal
|
|
18
15
|
*/
|
|
19
|
-
export type
|
|
20
|
-
readonly
|
|
16
|
+
export type Args<P extends object = Record<never, never>> = {
|
|
17
|
+
readonly store: Store;
|
|
18
|
+
readonly controller: AbortController;
|
|
19
|
+
readonly params: P;
|
|
21
20
|
};
|
|
22
21
|
/**
|
|
23
|
-
* Fetcher signature accepted by `Resource`. Receives the
|
|
24
|
-
* `
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* calling `useAction` handler, not inside the fetcher.
|
|
22
|
+
* Fetcher signature accepted by `Resource`. Receives the args object
|
|
23
|
+
* `{ store, controller, params }`. Side-effects (dispatching broadcasts,
|
|
24
|
+
* analytics, etc.) belong in the calling `useAction` handler, not
|
|
25
|
+
* inside the fetcher.
|
|
28
26
|
*/
|
|
29
|
-
export type
|
|
30
|
-
/**
|
|
31
|
-
* Module-scope handle returned by `Resource`. Pass to `useResource` to
|
|
32
|
-
* obtain the bound, component-scoped callable.
|
|
33
|
-
*
|
|
34
|
-
* Every call to the underlying fetcher fires its own request. The most
|
|
35
|
-
* recent successful response is cached in a module-level `WeakMap`
|
|
36
|
-
* keyed by the fetcher itself, so `.if(...)` and `.else(...)` on the
|
|
37
|
-
* bound handle have something to read from.
|
|
38
|
-
*/
|
|
39
|
-
export type ResourceHandle<T, P extends object = Record<never, never>> = {
|
|
40
|
-
/** @internal */
|
|
41
|
-
readonly run: (signal: AbortSignal | undefined, params: P) => Promise<T>;
|
|
42
|
-
/**
|
|
43
|
-
* Most recent successfully-resolved payload, or the shared `unset`
|
|
44
|
-
* sentinel if no successful run has happened yet.
|
|
45
|
-
* @internal
|
|
46
|
-
*/
|
|
47
|
-
readonly data: T | Unset;
|
|
48
|
-
/** @internal */
|
|
49
|
-
readonly at: Temporal.Instant | null;
|
|
50
|
-
/**
|
|
51
|
-
* Populates the cache slot with `data` and `at` without invoking the
|
|
52
|
-
* fetcher. Used by the bound handle's `.else(stored)` overload to
|
|
53
|
-
* hydrate the cache from a {@link Stored} fallback (typically the
|
|
54
|
-
* return value of `Store.get(key)` after a page reload).
|
|
55
|
-
* @internal
|
|
56
|
-
*/
|
|
57
|
-
readonly seed: (data: T, at: Temporal.Instant) => void;
|
|
58
|
-
};
|
|
59
|
-
/**
|
|
60
|
-
* Component-bound handle returned by `useResource`. The handle is itself
|
|
61
|
-
* the fetch callable — `await user(signal?, params?)` triggers a
|
|
62
|
-
* request — with attached read accessors and methods:
|
|
63
|
-
*
|
|
64
|
-
* - `.if({ over }, signal?, params?)` — fetch only if the cached
|
|
65
|
-
* payload is older than the supplied freshness window; otherwise
|
|
66
|
-
* return the cached payload synchronously.
|
|
67
|
-
* - `.else(fallback)` — synchronous read of the cached payload,
|
|
68
|
-
* falling back to the supplied default when nothing has resolved
|
|
69
|
-
* successfully yet. Accepts either a value (terminal, returns
|
|
70
|
-
* `T | U`) or a {@link Stored} (chainable, seeds the cache from the
|
|
71
|
-
* Stored's data/at when the cache is empty and returns the same bound
|
|
72
|
-
* handle for further chaining).
|
|
73
|
-
* - `.snapshot()` — returns a {@link Stored} wrapping the current
|
|
74
|
-
* cache state, symmetric with `Store.get(key)`. Pass straight to
|
|
75
|
-
* `Store.set(key, ...)` to persist the latest successful payload.
|
|
76
|
-
* - `.data` and `.at` — the underlying cache fields. Reading
|
|
77
|
-
* `.snapshot()` is usually clearer.
|
|
78
|
-
*
|
|
79
|
-
* Call signature: `(signal?)` for resources with no params, or
|
|
80
|
-
* `(signal: AbortSignal | null, params: P)` for parameterised
|
|
81
|
-
* resources — pass `null` as the first argument when you have
|
|
82
|
-
* params but no signal to thread.
|
|
83
|
-
*/
|
|
84
|
-
export type BoundResourceHandle<T, P extends object> = [keyof P] extends [never] ? {
|
|
85
|
-
(signal?: AbortSignal): Promise<T>;
|
|
86
|
-
/**
|
|
87
|
-
* Calls the underlying fetcher if the most recent successful run
|
|
88
|
-
* resolved longer ago than `options.over`. Otherwise returns the
|
|
89
|
-
* cached data without hitting the network.
|
|
90
|
-
*/
|
|
91
|
-
readonly if: (options: IfOptions, signal?: AbortSignal) => Promise<T>;
|
|
92
|
-
/**
|
|
93
|
-
* Overloaded fallback accessor.
|
|
94
|
-
*
|
|
95
|
-
* - `(fallback: U)` terminal — returns the cached payload, or
|
|
96
|
-
* `fallback` when nothing has resolved yet. Cached `null` values
|
|
97
|
-
* are returned verbatim.
|
|
98
|
-
* - `(stored: Stored<T>)` chainable — if the cache is empty
|
|
99
|
-
* and the Stored carries data and a timestamp, seeds the cache
|
|
100
|
-
* from it before returning the same bound handle so further
|
|
101
|
-
* `.else(...)` calls compose. Used to hydrate the cache on first
|
|
102
|
-
* render after a page reload, allowing `.if({ over })` to
|
|
103
|
-
* short-circuit on the persisted timestamp.
|
|
104
|
-
*/
|
|
105
|
-
readonly else: {
|
|
106
|
-
(stored: Stored<T>): BoundResourceHandle<T, P>;
|
|
107
|
-
<U>(fallback: U): T | U;
|
|
108
|
-
};
|
|
109
|
-
/**
|
|
110
|
-
* Snapshot of the current cache state in the shared {@link Stored}
|
|
111
|
-
* shape. Empty (`data === unset`, `at === null`) until a fetcher
|
|
112
|
-
* resolves; otherwise carries the most recent payload and the
|
|
113
|
-
* instant it resolved. Pass directly to `Store.set(key, ...)`.
|
|
114
|
-
*/
|
|
115
|
-
readonly snapshot: () => Stored<T>;
|
|
116
|
-
/** Direct read of the cache payload. Prefer `.snapshot()`. */
|
|
117
|
-
readonly data: T | Unset;
|
|
118
|
-
/** Instant the cache was last populated, or `null` if empty. */
|
|
119
|
-
readonly at: Temporal.Instant | null;
|
|
120
|
-
} : {
|
|
121
|
-
(signal: AbortSignal | null, params: P): Promise<T>;
|
|
122
|
-
/**
|
|
123
|
-
* Calls the underlying fetcher if the most recent successful run
|
|
124
|
-
* resolved longer ago than `options.over`. Otherwise returns the
|
|
125
|
-
* cached data without hitting the network.
|
|
126
|
-
*/
|
|
127
|
-
readonly if: (options: IfOptions, signal: AbortSignal | null, params: P) => Promise<T>;
|
|
128
|
-
/**
|
|
129
|
-
* Overloaded fallback accessor.
|
|
130
|
-
*
|
|
131
|
-
* - `(fallback: U)` terminal — returns the cached payload, or
|
|
132
|
-
* `fallback` when nothing has resolved yet.
|
|
133
|
-
* - `(stored: Stored<T>)` chainable — if the cache is empty
|
|
134
|
-
* and the Stored carries data and a timestamp, seeds the cache
|
|
135
|
-
* from it before returning the same bound handle.
|
|
136
|
-
*/
|
|
137
|
-
readonly else: {
|
|
138
|
-
(stored: Stored<T>): BoundResourceHandle<T, P>;
|
|
139
|
-
<U>(fallback: U): T | U;
|
|
140
|
-
};
|
|
141
|
-
/**
|
|
142
|
-
* Snapshot of the current cache state in the shared {@link Stored}
|
|
143
|
-
* shape. Pass directly to `Store.set(key, ...)`.
|
|
144
|
-
*/
|
|
145
|
-
readonly snapshot: () => Stored<T>;
|
|
146
|
-
/** Direct read of the cache payload. Prefer `.snapshot()`. */
|
|
147
|
-
readonly data: T | Unset;
|
|
148
|
-
/** Instant the cache was last populated, or `null` if empty. */
|
|
149
|
-
readonly at: Temporal.Instant | null;
|
|
150
|
-
};
|
|
27
|
+
export type Fetcher<T, P extends object = Record<never, never>> = (args: Args<P>) => Promise<T>;
|
|
@@ -1,20 +1,34 @@
|
|
|
1
|
+
import { Cache } from '../cache/index.ts';
|
|
1
2
|
import { unset } from '../utils/index.ts';
|
|
3
|
+
export { Cache } from '../cache/index.ts';
|
|
2
4
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Default in-memory `Cache` used when {@link Resource} is constructed
|
|
6
|
+
* without an explicit one. Each fetcher gets its own slot via the
|
|
7
|
+
* outer `WeakMap` so unrelated Resources don't share a string-key
|
|
8
|
+
* namespace.
|
|
7
9
|
*
|
|
8
10
|
* @internal
|
|
9
11
|
*/
|
|
10
|
-
export declare const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
export declare const defaults: WeakMap<object, Cache>;
|
|
13
|
+
/**
|
|
14
|
+
* Returns the {@link Cache} bound to `fetcher`, allocating a fresh
|
|
15
|
+
* in-memory Cache on first access.
|
|
16
|
+
*
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export declare function defaultCache(fetcher: object): Cache;
|
|
20
|
+
/**
|
|
21
|
+
* Stable string key derived from the call-site `params`. Two calls with
|
|
22
|
+
* the same logical params (same key order, same primitive values) hit
|
|
23
|
+
* the same slot. JSON.stringify is sufficient for the Chizu params
|
|
24
|
+
* convention (primitive-leaf objects); callers who need order-stable
|
|
25
|
+
* keying should normalise the object before passing it in.
|
|
26
|
+
*
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
export declare function key(params: object): string;
|
|
14
30
|
/**
|
|
15
31
|
* Re-export of the shared `unset` sentinel from {@link "../utils/index.ts"}.
|
|
16
|
-
* Kept under `config` for back-compatibility with existing imports in this
|
|
17
|
-
* module's siblings; new code should import `unset` directly from `utils`.
|
|
18
32
|
*
|
|
19
33
|
* @internal
|
|
20
34
|
*/
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Operation, Process, Inspect, Box } from 'immertation';
|
|
2
2
|
import { ActionId, Task, Tasks } from '../boundary/components/tasks/types.ts';
|
|
3
3
|
import { Fault } from '../error/types.ts';
|
|
4
|
+
import { Store } from '../boundary/components/store/index.tsx';
|
|
4
5
|
import * as React from "react";
|
|
5
6
|
export type { ActionId, Box, Task, Tasks };
|
|
6
7
|
/**
|
|
@@ -31,6 +32,20 @@ export type BrandedMulticast = {
|
|
|
31
32
|
export type BrandedObject = {
|
|
32
33
|
readonly [x: symbol]: unknown;
|
|
33
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* Recursive readonly. Locks every nested property so that read-only
|
|
37
|
+
* projections on `context` (model, data, store) reject direct assignment
|
|
38
|
+
* — mutation must go through `context.actions.produce(...)`.
|
|
39
|
+
*
|
|
40
|
+
* Function types pass through untouched so method calls (e.g.
|
|
41
|
+
* `AbortController#abort`) remain callable. Built-in mutable containers
|
|
42
|
+
* are mapped to their readonly counterparts.
|
|
43
|
+
*
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
export type DeepReadonly<T> = T extends (...args: never) => unknown ? T : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepReadonly<U>> : T extends ReadonlyMap<infer K, infer V> ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>> : T extends ReadonlySet<infer U> ? ReadonlySet<DeepReadonly<U>> : T extends object ? {
|
|
47
|
+
readonly [K in keyof T]: DeepReadonly<T[K]>;
|
|
48
|
+
} : T;
|
|
34
49
|
/**
|
|
35
50
|
* Union type representing any valid action that can be passed to action utilities.
|
|
36
51
|
* This includes raw ActionIds (symbol/string), and any branded object.
|
|
@@ -53,6 +68,13 @@ export declare class Brand {
|
|
|
53
68
|
static readonly Action: unique symbol;
|
|
54
69
|
/** Identifies channeled actions (result of calling Action(channel)) */
|
|
55
70
|
static readonly Channel: unique symbol;
|
|
71
|
+
/**
|
|
72
|
+
* Phantom brand carrying the action's literal name. Used purely at the
|
|
73
|
+
* type level to make `Action("X")` and `Action("Y")` produce
|
|
74
|
+
* structurally-distinct types so `dispatch`/`useAction` can reject
|
|
75
|
+
* symbols imported from a class outside `AC`.
|
|
76
|
+
*/
|
|
77
|
+
static readonly Name: unique symbol;
|
|
56
78
|
}
|
|
57
79
|
/**
|
|
58
80
|
* Internal symbol for the global `Lifecycle.Fault` broadcast. Exposed so the
|
|
@@ -86,13 +108,13 @@ export declare const FaultSymbol: unique symbol;
|
|
|
86
108
|
*/
|
|
87
109
|
export declare class Lifecycle {
|
|
88
110
|
/** Creates a Mount lifecycle action. Triggered once on component mount (`useLayoutEffect`). */
|
|
89
|
-
static Mount(): HandlerPayload<never>;
|
|
111
|
+
static Mount(): HandlerPayload<never, never, "Mount">;
|
|
90
112
|
/** Creates an Unmount lifecycle action. Triggered when the component unmounts. */
|
|
91
|
-
static Unmount(): HandlerPayload<never>;
|
|
113
|
+
static Unmount(): HandlerPayload<never, never, "Unmount">;
|
|
92
114
|
/** Creates an Error lifecycle action. Triggered when an action throws. Receives `Fault` as payload. */
|
|
93
|
-
static Error(): HandlerPayload<Fault>;
|
|
115
|
+
static Error(): HandlerPayload<Fault, never, "Error">;
|
|
94
116
|
/** Creates an Update lifecycle action. Triggered when `context.data` changes (not on initial mount). */
|
|
95
|
-
static Update(): HandlerPayload<Record<string, unknown
|
|
117
|
+
static Update(): HandlerPayload<Record<string, unknown>, never, "Update">;
|
|
96
118
|
/**
|
|
97
119
|
* Global fault broadcast. Receives a `Fault` whenever any action in the
|
|
98
120
|
* `<Boundary>` errors, times out, or is supplanted. Subscribe via
|
|
@@ -112,7 +134,7 @@ export declare class Lifecycle {
|
|
|
112
134
|
* });
|
|
113
135
|
* ```
|
|
114
136
|
*/
|
|
115
|
-
static Fault: BroadcastPayload<Fault>;
|
|
137
|
+
static Fault: BroadcastPayload<Fault, never, "Fault">;
|
|
116
138
|
}
|
|
117
139
|
/**
|
|
118
140
|
* Distribution modes for actions.
|
|
@@ -209,12 +231,13 @@ export type Model<M = Record<string, unknown>> = M;
|
|
|
209
231
|
* dispatch(UserUpdated({ UserId: 5 }), user); // channeled dispatch
|
|
210
232
|
* ```
|
|
211
233
|
*/
|
|
212
|
-
export type HandlerPayload<P = unknown, C extends Filter = never> = {
|
|
234
|
+
export type HandlerPayload<P = unknown, C extends Filter = never, Name extends string = string> = {
|
|
213
235
|
readonly [Brand.Action]: symbol;
|
|
214
236
|
readonly [Brand.Payload]: P;
|
|
237
|
+
readonly [Brand.Name]: Name;
|
|
215
238
|
readonly [Brand.Broadcast]?: boolean;
|
|
216
239
|
} & ([C] extends [never] ? unknown : {
|
|
217
|
-
(channel: C): ChanneledAction<P, C>;
|
|
240
|
+
(channel: C): ChanneledAction<P, C, Name>;
|
|
218
241
|
});
|
|
219
242
|
/**
|
|
220
243
|
* Result of calling an action with a channel argument.
|
|
@@ -231,10 +254,11 @@ export type HandlerPayload<P = unknown, C extends Filter = never> = {
|
|
|
231
254
|
* dispatch(UserUpdated({ UserId: 5 }), user);
|
|
232
255
|
* ```
|
|
233
256
|
*/
|
|
234
|
-
export type ChanneledAction<P = unknown, C = unknown> = {
|
|
257
|
+
export type ChanneledAction<P = unknown, C = unknown, Name extends string = string> = {
|
|
235
258
|
readonly [Brand.Action]: symbol;
|
|
236
259
|
readonly [Brand.Payload]: P;
|
|
237
260
|
readonly [Brand.Channel]: C;
|
|
261
|
+
readonly [Brand.Name]: Name;
|
|
238
262
|
readonly channel: C;
|
|
239
263
|
};
|
|
240
264
|
/**
|
|
@@ -260,7 +284,7 @@ export type ChanneledAction<P = unknown, C = unknown> = {
|
|
|
260
284
|
* const user = await context.actions.resolution(SignedOut);
|
|
261
285
|
* ```
|
|
262
286
|
*/
|
|
263
|
-
export type BroadcastPayload<P = unknown, C extends Filter = never> = HandlerPayload<P, C> & {
|
|
287
|
+
export type BroadcastPayload<P = unknown, C extends Filter = never, Name extends string = string> = HandlerPayload<P, C, Name> & {
|
|
264
288
|
readonly [Brand.Broadcast]: true;
|
|
265
289
|
};
|
|
266
290
|
/**
|
|
@@ -306,7 +330,7 @@ export type BroadcastPayload<P = unknown, C extends Filter = never> = HandlerPay
|
|
|
306
330
|
* actions.dispatch(Actions.Multicast.Update, 42);
|
|
307
331
|
* ```
|
|
308
332
|
*/
|
|
309
|
-
export type MulticastPayload<P = unknown, C extends Filter = never> = HandlerPayload<P, C> & {
|
|
333
|
+
export type MulticastPayload<P = unknown, C extends Filter = never, Name extends string = string> = HandlerPayload<P, C, Name> & {
|
|
310
334
|
readonly [Brand.Multicast]: true;
|
|
311
335
|
};
|
|
312
336
|
/**
|
|
@@ -401,8 +425,8 @@ export type Actions = object;
|
|
|
401
425
|
export type Result = {
|
|
402
426
|
processes: Set<Process>;
|
|
403
427
|
};
|
|
404
|
-
export type HandlerContext<M extends Model | void,
|
|
405
|
-
readonly model:
|
|
428
|
+
export type HandlerContext<M extends Model | void, AC extends Actions | void, D extends Props = Props> = {
|
|
429
|
+
readonly model: DeepReadonly<M>;
|
|
406
430
|
/**
|
|
407
431
|
* The current lifecycle phase of the component.
|
|
408
432
|
* Useful for determining if the handler was called during mount (e.g., from a cached
|
|
@@ -458,7 +482,7 @@ export type HandlerContext<M extends Model | void, _AC extends Actions | void, D
|
|
|
458
482
|
* });
|
|
459
483
|
* ```
|
|
460
484
|
*/
|
|
461
|
-
readonly data: D
|
|
485
|
+
readonly data: DeepReadonly<D>;
|
|
462
486
|
/**
|
|
463
487
|
* Set of all running tasks across all components in the context.
|
|
464
488
|
* Tasks are ordered by creation time (oldest first).
|
|
@@ -490,13 +514,87 @@ export type HandlerContext<M extends Model | void, _AC extends Actions | void, D
|
|
|
490
514
|
* ```
|
|
491
515
|
*/
|
|
492
516
|
readonly tasks: ReadonlySet<Task>;
|
|
517
|
+
/**
|
|
518
|
+
* Read-only view of the per-`<Boundary>` Store — ambient,
|
|
519
|
+
* cross-cutting state (session, locale, feature flags, etc.) typed
|
|
520
|
+
* via module augmentation on the library's `Store` interface.
|
|
521
|
+
* Identical to the value returned by `useStore()` at the hook level.
|
|
522
|
+
*
|
|
523
|
+
* Reads use plain dot notation and always reflect the latest value,
|
|
524
|
+
* even after `await` boundaries. Writes go through
|
|
525
|
+
* `context.actions.produce(({ store }) => { store.x = ... })`
|
|
526
|
+
* — the same Immer-style recipe used for the model.
|
|
527
|
+
*
|
|
528
|
+
* @example
|
|
529
|
+
* ```ts
|
|
530
|
+
* actions.useAction(Actions.SignIn, async (context, credentials) => {
|
|
531
|
+
* const result = await context.actions.resource(signIn(credentials));
|
|
532
|
+
* context.actions.produce(({ store }) => {
|
|
533
|
+
* store.session = result;
|
|
534
|
+
* });
|
|
535
|
+
* });
|
|
536
|
+
*
|
|
537
|
+
* actions.useAction(Actions.Refresh, async (context) => {
|
|
538
|
+
* if (context.store.session === null) return;
|
|
539
|
+
* // ...
|
|
540
|
+
* });
|
|
541
|
+
* ```
|
|
542
|
+
*/
|
|
543
|
+
readonly store: DeepReadonly<Store>;
|
|
493
544
|
readonly actions: {
|
|
494
545
|
produce<F extends (draft: {
|
|
495
546
|
model: M;
|
|
547
|
+
store: Store;
|
|
496
548
|
readonly inspect: Readonly<Inspect<M>>;
|
|
497
549
|
}) => void>(ƒ: F & AssertSync<F>): void;
|
|
498
|
-
dispatch(action:
|
|
550
|
+
dispatch(action: NoPayloadActions<Dispatchable<AC>>): Promise<void>;
|
|
551
|
+
dispatch<A extends WithPayloadActions<Dispatchable<AC>>>(action: A, payload: Payload<A>): Promise<void>;
|
|
499
552
|
annotate<T>(value: T, operation?: Operation): T;
|
|
553
|
+
/**
|
|
554
|
+
* Fetches a {@link Resource} with the abort controller and Store
|
|
555
|
+
* snapshot auto-threaded from the current handler context. The
|
|
556
|
+
* argument is a resource invocation (`cat({ id: 5 })`) — the
|
|
557
|
+
* call primes a slot with the resource and params, and
|
|
558
|
+
* `.resource(...)` reads it. The return value is a thenable —
|
|
559
|
+
* `await` it to fire the fetch unconditionally, or use
|
|
560
|
+
* `.exceeds(duration)` to short-circuit when the per-params cache
|
|
561
|
+
* slot is still within the supplied freshness window (i.e. fetch
|
|
562
|
+
* only when the cache age *exceeds* the duration).
|
|
563
|
+
*
|
|
564
|
+
* @example
|
|
565
|
+
* ```ts
|
|
566
|
+
* actions.useAction(Actions.Mount, async (context) => {
|
|
567
|
+
* // Always fetch.
|
|
568
|
+
* const fresh = await context.actions.resource(user({ id: 5 }));
|
|
569
|
+
*
|
|
570
|
+
* // Reuse cache when < 5 minutes old.
|
|
571
|
+
* const maybe = await context.actions
|
|
572
|
+
* .resource(user({ id: 5 }))
|
|
573
|
+
* .exceeds({ minutes: 5 });
|
|
574
|
+
*
|
|
575
|
+
* context.actions.produce(({ model }) => void (model.user = fresh));
|
|
576
|
+
* });
|
|
577
|
+
* ```
|
|
578
|
+
*/
|
|
579
|
+
resource: (<T>(invocation: T | null) => PromiseLike<T> & {
|
|
580
|
+
readonly exceeds: (duration: Temporal.DurationLike) => Promise<T>;
|
|
581
|
+
}) & {
|
|
582
|
+
/**
|
|
583
|
+
* Writes `data` into the per-params cache slot of the resource
|
|
584
|
+
* invocation passed as the first argument, with a fresh timestamp.
|
|
585
|
+
* Use this when payloads arrive out-of-band (SSE, WebSocket,
|
|
586
|
+
* postMessage) and need to be reflected in the Resource cache
|
|
587
|
+
* without a fetcher round-trip.
|
|
588
|
+
*
|
|
589
|
+
* @example
|
|
590
|
+
* ```ts
|
|
591
|
+
* actions.useAction(Actions.Broadcast.UserSSE, (context, payload) => {
|
|
592
|
+
* context.actions.resource.set(user({ id: payload.id }), payload);
|
|
593
|
+
* });
|
|
594
|
+
* ```
|
|
595
|
+
*/
|
|
596
|
+
set<T>(invocation: T | null, data: T): void;
|
|
597
|
+
};
|
|
500
598
|
/**
|
|
501
599
|
* Returns the resolved broadcast or multicast value, waiting for any
|
|
502
600
|
* pending annotations to settle before resolving.
|
|
@@ -586,6 +684,50 @@ export type Handler<M extends Model | void, AC extends Actions | void, K extends
|
|
|
586
684
|
* as a handler key and avoids recursion into Function internals.
|
|
587
685
|
*/
|
|
588
686
|
type OwnKeys<AC> = Exclude<keyof AC & string, "prototype">;
|
|
687
|
+
/**
|
|
688
|
+
* Recursively flattens an actions class into the union of its leaf action
|
|
689
|
+
* types. A "leaf" is any property whose own string keys are empty — the
|
|
690
|
+
* branded `HandlerPayload` / `BroadcastPayload` / `MulticastPayload` values
|
|
691
|
+
* produced by `Action(...)` and `Lifecycle.*()`. Nested namespace classes
|
|
692
|
+
* (e.g. `static Broadcast = BroadcastActions`) are descended into.
|
|
693
|
+
*
|
|
694
|
+
* Used to constrain `dispatch` and `useAction` so that only actions owned by
|
|
695
|
+
* the component's `AC` (plus the global `Lifecycle.Fault`) can be referenced.
|
|
696
|
+
*/
|
|
697
|
+
export type LeafActions<AC> = AC extends void ? never : {
|
|
698
|
+
[K in OwnKeys<AC>]: OwnKeys<AC[K]> extends never ? AC[K] : LeafActions<AC[K]>;
|
|
699
|
+
}[OwnKeys<AC>];
|
|
700
|
+
/**
|
|
701
|
+
* Maps each action in a union to its channeled-call variant, when one exists.
|
|
702
|
+
* Distributes over unions so a mixed bag of leaf actions produces the union
|
|
703
|
+
* of their `ChanneledAction<P, C>` results.
|
|
704
|
+
*/
|
|
705
|
+
export type ChanneledOf<A> = A extends HandlerPayload<infer P, infer C> ? [C] extends [never] ? never : ChanneledAction<P, C> : never;
|
|
706
|
+
/**
|
|
707
|
+
* Everything `dispatch` accepts for a given `AC`: leaf actions on the class
|
|
708
|
+
* and their channeled-call variants. The shared `Lifecycle.Fault` broadcast
|
|
709
|
+
* is excluded — it's library-internal and not user-dispatchable.
|
|
710
|
+
*/
|
|
711
|
+
export type Dispatchable<AC> = LeafActions<AC> | ChanneledOf<LeafActions<AC>>;
|
|
712
|
+
/**
|
|
713
|
+
* Everything `useAction` will subscribe to for a given `AC`: same as
|
|
714
|
+
* `Dispatchable<AC>` plus the shared `Lifecycle.Fault` broadcast which lives
|
|
715
|
+
* outside `AC` but is subscribable by any component.
|
|
716
|
+
*/
|
|
717
|
+
export type Subscribable<AC> = Dispatchable<AC> | typeof Lifecycle.Fault;
|
|
718
|
+
/**
|
|
719
|
+
* Subset of a union of actions whose payload type is `never`. Used to split
|
|
720
|
+
* `dispatch`/`useAction` into a no-payload and a with-payload overload so
|
|
721
|
+
* TypeScript reports a clear "no overload matches" error instead of widening
|
|
722
|
+
* the inferred action type when constraints don't match.
|
|
723
|
+
*/
|
|
724
|
+
export type NoPayloadActions<U> = Extract<U, {
|
|
725
|
+
readonly [Brand.Payload]: never;
|
|
726
|
+
}>;
|
|
727
|
+
/** Subset of a union of actions whose payload type is non-`never`. */
|
|
728
|
+
export type WithPayloadActions<U> = Exclude<U, {
|
|
729
|
+
readonly [Brand.Payload]: never;
|
|
730
|
+
}>;
|
|
589
731
|
/**
|
|
590
732
|
* Recursive mapped type for action handlers that mirrors the action class hierarchy.
|
|
591
733
|
*
|
|
@@ -633,10 +775,8 @@ export type UseActions<M extends Model | void, AC extends Actions | void, D exte
|
|
|
633
775
|
* their scope from the action declaration, so no extra options are
|
|
634
776
|
* required at the call site.
|
|
635
777
|
*/
|
|
636
|
-
dispatch
|
|
637
|
-
dispatch<
|
|
638
|
-
dispatch<P>(action: MulticastPayload<P>, payload?: P): Promise<void>;
|
|
639
|
-
dispatch<P, C extends Filter>(action: ChanneledAction<P, C>, payload?: P): Promise<void>;
|
|
778
|
+
dispatch(action: NoPayloadActions<Dispatchable<AC>>): Promise<void>;
|
|
779
|
+
dispatch<A extends WithPayloadActions<Dispatchable<AC>>>(action: A, payload: Payload<A>): Promise<void>;
|
|
640
780
|
inspect: Inspect<M>;
|
|
641
781
|
/**
|
|
642
782
|
* Streams broadcast values declaratively in JSX using a render-prop pattern.
|
|
@@ -689,5 +829,6 @@ export type UseActions<M extends Model | void, AC extends Actions | void, D exte
|
|
|
689
829
|
* });
|
|
690
830
|
* ```
|
|
691
831
|
*/
|
|
692
|
-
useAction
|
|
832
|
+
useAction(action: NoPayloadActions<Subscribable<AC>>, handler: (context: HandlerContext<M, AC, D>) => void | Promise<void> | AsyncGenerator | Generator): void;
|
|
833
|
+
useAction<A extends WithPayloadActions<Subscribable<AC>>>(action: A, handler: (context: HandlerContext<M, AC, D>, payload: Payload<A>) => void | Promise<void> | AsyncGenerator | Generator): void;
|
|
693
834
|
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Pk } from '../types/index.ts';
|
|
2
|
-
import { Adapter, Store } from './types.ts';
|
|
3
2
|
export { unset } from './utils.ts';
|
|
4
|
-
export type {
|
|
3
|
+
export type { Stored, Unset } from './types.ts';
|
|
5
4
|
/**
|
|
6
5
|
* Returns a promise that resolves after the specified number of
|
|
7
6
|
* milliseconds, or rejects with an {@link AbortError} when the signal is
|
|
@@ -45,48 +44,9 @@ export declare function pk(): symbol;
|
|
|
45
44
|
* @returns `true` if `id` is a non-symbol value, `false` otherwise.
|
|
46
45
|
*/
|
|
47
46
|
export declare function pk<T>(id: Pk<T>): boolean;
|
|
48
|
-
/**
|
|
49
|
-
* Wraps a synchronous {@link Adapter} into a {@link Store} that traffics
|
|
50
|
-
* in {@link Stored} values. Storage entries serialise as
|
|
51
|
-
* {@link Encoded}`<T>` so the `Temporal.Instant` timestamp survives the
|
|
52
|
-
* string round-trip and `BoundResourceHandle.if({ over })` can
|
|
53
|
-
* short-circuit on the persisted timestamp after a reload.
|
|
54
|
-
*
|
|
55
|
-
* @param adapter Backend implementation providing raw string `get`/`set`/
|
|
56
|
-
* `remove`/`clear`. The Store layers JSON encoding and
|
|
57
|
-
* timestamp serialisation on top.
|
|
58
|
-
* @returns A {@link Store} bound to `adapter`. Reads return {@link Stored}
|
|
59
|
-
* envelopes; writes accept Stored envelopes and return `true`
|
|
60
|
-
* when the entry landed in the adapter.
|
|
61
|
-
*
|
|
62
|
-
* @example
|
|
63
|
-
* ```ts
|
|
64
|
-
* const store = utils.store({
|
|
65
|
-
* get: (key) => localStorage.getItem(key),
|
|
66
|
-
* set: (key, value) => localStorage.setItem(key, value),
|
|
67
|
-
* remove: (key) => localStorage.removeItem(key),
|
|
68
|
-
* clear: () => localStorage.clear(),
|
|
69
|
-
* });
|
|
70
|
-
*
|
|
71
|
-
* // Read into a Resource fallback chain.
|
|
72
|
-
* { cat: get.cat.else(store.get(Snapshots.Cat)).else(null) }
|
|
73
|
-
*
|
|
74
|
-
* // Write the latest cached value back to storage.
|
|
75
|
-
* store.set(Snapshots.Cat, get.cat.snapshot());
|
|
76
|
-
*
|
|
77
|
-
* // Drop a snapshot on sign-out, cache invalidation, etc.
|
|
78
|
-
* store.remove(Snapshots.Cat);
|
|
79
|
-
*
|
|
80
|
-
* // Or wipe the whole backing store (scope is the adapter's call).
|
|
81
|
-
* store.clear();
|
|
82
|
-
* ```
|
|
83
|
-
*/
|
|
84
|
-
export declare function store(adapter: Adapter): Store;
|
|
85
47
|
/** Shorthand alias for {@link sleep}. */
|
|
86
48
|
export declare const ζ: typeof sleep;
|
|
87
49
|
/** Shorthand alias for {@link poll}. */
|
|
88
50
|
export declare const π: typeof poll;
|
|
89
51
|
/** Shorthand alias for {@link pk}. */
|
|
90
52
|
export declare const κ: typeof pk;
|
|
91
|
-
/** Shorthand alias for {@link store}. */
|
|
92
|
-
export declare const σ: typeof store;
|
|
@@ -1,25 +1,10 @@
|
|
|
1
1
|
import { unset } from './utils.ts';
|
|
2
2
|
/** Nominal type of the {@link unset} sentinel. */
|
|
3
3
|
export type Unset = typeof unset;
|
|
4
|
-
/**
|
|
5
|
-
* On-disk JSON shape of a {@link Stored} envelope. The Store wrapper
|
|
6
|
-
* encodes a populated Stored as `{ data, at: at.toString() }` so the
|
|
7
|
-
* `Temporal.Instant` survives the string round-trip, and decodes via
|
|
8
|
-
* `Temporal.Instant.from(...)` on read. Adapters never see this shape
|
|
9
|
-
* directly — they shuttle the already-stringified JSON.
|
|
10
|
-
*
|
|
11
|
-
* @template T The payload type carried by the matching {@link Stored}.
|
|
12
|
-
*/
|
|
13
|
-
export type Encoded<T> = {
|
|
14
|
-
readonly data: T;
|
|
15
|
-
readonly at: string;
|
|
16
|
-
};
|
|
17
4
|
/**
|
|
18
5
|
* Common shape for a possibly-present value with a timestamp. Produced by
|
|
19
|
-
* `
|
|
20
|
-
*
|
|
21
|
-
* handle's overloaded `.else(...)`, which seeds the cache when given a
|
|
22
|
-
* Stored that carries data and a timestamp.
|
|
6
|
+
* `Cache.get(key)` (from persistent storage or in-memory) and consumed
|
|
7
|
+
* internally by Resource's per-params cache slots.
|
|
23
8
|
*
|
|
24
9
|
* @template T The payload type when present.
|
|
25
10
|
*/
|
|
@@ -28,74 +13,6 @@ export type Stored<T> = {
|
|
|
28
13
|
readonly data: T | Unset;
|
|
29
14
|
/** When the payload was recorded, or `null` when nothing is recorded. */
|
|
30
15
|
readonly at: Temporal.Instant | null;
|
|
31
|
-
/**
|
|
32
|
-
* Returns {@link data} when present, otherwise the supplied fallback.
|
|
33
|
-
* Symmetric with `BoundResourceHandle.else(...)`'s terminal form.
|
|
34
|
-
*/
|
|
16
|
+
/** Returns {@link data} when present, otherwise the supplied fallback. */
|
|
35
17
|
readonly else: <U>(fallback: U) => T | U;
|
|
36
18
|
};
|
|
37
|
-
/**
|
|
38
|
-
* Adapter contract for synchronous key/value storage. Implement once per
|
|
39
|
-
* backend (localStorage, MMKV on React Native, chrome.storage with a sync
|
|
40
|
-
* facade, etc.) and pass to `store`. The adapter shuttles raw strings;
|
|
41
|
-
* JSON encoding and `Temporal.Instant` round-tripping happen inside the
|
|
42
|
-
* Store wrapper, so adapters stay trivial.
|
|
43
|
-
*/
|
|
44
|
-
export type Adapter = {
|
|
45
|
-
/**
|
|
46
|
-
* Return the raw string stored under `key`, or `null` when no entry
|
|
47
|
-
* exists. The Store wrapper handles JSON parsing and `Temporal.Instant`
|
|
48
|
-
* round-tripping, so this stays a plain string getter. Treat any
|
|
49
|
-
* read-time error (decryption, IPC, etc.) as "not found" and return
|
|
50
|
-
* `null` — the Store falls through to its next fallback rather than
|
|
51
|
-
* crashing the render.
|
|
52
|
-
*/
|
|
53
|
-
readonly get: (key: string) => string | null;
|
|
54
|
-
/**
|
|
55
|
-
* Persist the raw string `value` under `key`. The Store guarantees
|
|
56
|
-
* `value` is a JSON-encoded `{ data, at }` envelope produced by a
|
|
57
|
-
* resolved snapshot — never a placeholder. Throwing is fine on quota,
|
|
58
|
-
* private mode, sandboxed iframes, etc.; the Store catches and
|
|
59
|
-
* swallows so a write failure can't poison an already-resolved fetch.
|
|
60
|
-
*/
|
|
61
|
-
readonly set: (key: string, value: string) => void;
|
|
62
|
-
/**
|
|
63
|
-
* Drop the entry at `key`. Idempotent — calling `remove` for a key
|
|
64
|
-
* that isn't present must not throw.
|
|
65
|
-
*/
|
|
66
|
-
readonly remove: (key: string) => void;
|
|
67
|
-
/**
|
|
68
|
-
* Wipe every entry this adapter can see. On a shared backend such as
|
|
69
|
-
* `localStorage` this means the whole origin — third-party SDK state,
|
|
70
|
-
* dismissed banners, route hints, etc. all go with it. Adapter authors
|
|
71
|
-
* should either delegate to the backend's native clear (accepting that
|
|
72
|
-
* scope) or namespace by key prefix and remove only their own.
|
|
73
|
-
*/
|
|
74
|
-
readonly clear: () => void;
|
|
75
|
-
};
|
|
76
|
-
/**
|
|
77
|
-
* Bound storage instance returned by `store`. Reads return a
|
|
78
|
-
* {@link Stored} handle so the result composes with the Resource bound
|
|
79
|
-
* handle's `.else(...)`; writes accept a Stored and short-circuit on the
|
|
80
|
-
* empty case to avoid persisting placeholder snapshots.
|
|
81
|
-
*/
|
|
82
|
-
export type Store = Pick<Adapter, "remove" | "clear"> & {
|
|
83
|
-
/**
|
|
84
|
-
* Read the entry at `key` as a {@link Stored} envelope. Returns an
|
|
85
|
-
* empty Stored (`data: unset`, `at: null`) when nothing is recorded
|
|
86
|
-
* or when the persisted payload fails to parse — corrupted entries
|
|
87
|
-
* never reach the caller. The result composes directly with a
|
|
88
|
-
* Resource bound handle's `.else(...)` for seeding the cache after
|
|
89
|
-
* a reload.
|
|
90
|
-
*/
|
|
91
|
-
readonly get: <T>(key: string) => Stored<T>;
|
|
92
|
-
/**
|
|
93
|
-
* Persist `value` under `key`. Returns `true` when the entry landed
|
|
94
|
-
* in the adapter, `false` otherwise. A `false` covers two distinct
|
|
95
|
-
* cases: the Stored had no payload yet (`data === unset` or
|
|
96
|
-
* `at === null`), or the adapter threw (quota, private mode, etc.).
|
|
97
|
-
* Callers that care about quota failures should branch on the
|
|
98
|
-
* return; callers writing on every dispatch can safely ignore it.
|
|
99
|
-
*/
|
|
100
|
-
readonly set: <T>(key: string, value: Stored<T>) => boolean;
|
|
101
|
-
};
|