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.
Files changed (36) hide show
  1. package/README.md +480 -209
  2. package/dist/{hooks → actions}/index.d.ts +2 -39
  3. package/dist/{hooks → actions}/utils.d.ts +0 -39
  4. package/dist/app/index.d.ts +112 -0
  5. package/dist/app/types.d.ts +49 -0
  6. package/dist/boundary/components/broadcast/utils.d.ts +1 -1
  7. package/dist/boundary/components/env/index.d.ts +26 -0
  8. package/dist/boundary/components/env/types.d.ts +11 -0
  9. package/dist/boundary/components/env/utils.d.ts +36 -0
  10. package/dist/boundary/components/scope/index.d.ts +1 -39
  11. package/dist/boundary/components/scope/types.d.ts +17 -13
  12. package/dist/boundary/components/scope/utils.d.ts +12 -8
  13. package/dist/boundary/components/sharing/index.d.ts +43 -0
  14. package/dist/boundary/index.d.ts +10 -10
  15. package/dist/boundary/types.d.ts +6 -16
  16. package/dist/cache/index.d.ts +4 -4
  17. package/dist/coalesce/index.d.ts +57 -0
  18. package/dist/context/index.d.ts +39 -0
  19. package/dist/context/types.d.ts +14 -0
  20. package/dist/error/index.d.ts +1 -1
  21. package/dist/error/types.d.ts +8 -19
  22. package/dist/index.d.ts +8 -13
  23. package/dist/march-hare.js +7 -5
  24. package/dist/march-hare.umd.cjs +1 -1
  25. package/dist/resource/index.d.ts +52 -78
  26. package/dist/resource/types.d.ts +83 -10
  27. package/dist/scope/index.d.ts +63 -0
  28. package/dist/scope/types.d.ts +55 -0
  29. package/dist/types/index.d.ts +77 -39
  30. package/dist/utils/index.d.ts +6 -5
  31. package/dist/with/index.d.ts +40 -0
  32. package/package.json +1 -1
  33. package/dist/boundary/components/store/index.d.ts +0 -41
  34. package/dist/boundary/components/store/types.d.ts +0 -11
  35. package/dist/boundary/components/store/utils.d.ts +0 -64
  36. /package/dist/{hooks → actions}/types.d.ts +0 -0
@@ -1,27 +1,100 @@
1
- import { Store } from '../boundary/components/store/index';
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
- * Args object passed to every {@link Fetcher}. The fetcher destructures
4
- * whatever it needs; unused fields can be omitted.
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
- * - `store` &mdash; snapshot of the per-`<Boundary>` Store at the
16
+ * - `env` &mdash; snapshot of the per-`<Boundary>` Env at the
7
17
  * moment the fetcher is invoked.
8
18
  * - `controller` &mdash; 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` &mdash; the call-site params object. Defaults to `{}`.
23
+ * - `dispatch` &mdash; 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 store: Store;
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 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.
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` &mdash; the fetcher.
53
+ * - `cache` &mdash; 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 Fetcher<T, P extends object = Record<never, never>> = (args: Args<P>) => Promise<T>;
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 &mdash; just render two `<scope.Boundary>`s
12
+ * &mdash; 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
+ * &mdash; 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` &mdash; 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` &mdash; 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 &mdash; just render two `<scope.Boundary>`s
12
+ * &mdash; 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
+ * &mdash; 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` &mdash;
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` &mdash; 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
+ };
@@ -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 { Store } from '../boundary/components/store/index';
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 &mdash; 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, store) reject direct assignment
69
+ * projections on `context` (model, data, env) reject direct assignment
38
70
  * &mdash; 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.Store` broadcast. The store
120
+ * Internal symbol for the global `Lifecycle.Env` broadcast. The env
89
121
  * mutation path in `useActions` fires this symbol whenever a
90
- * `produce({ store })` call changes the slot reference.
122
+ * `produce({ env })` call changes the slot reference.
91
123
  *
92
124
  * @internal
93
125
  */
94
- export declare const StoreSymbol: unique symbol;
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.Store` are singleton broadcasts (not
146
+ * `Lifecycle.Fault` and `Lifecycle.Env` are singleton broadcasts (not
115
147
  * factories). All components subscribe to the same shared symbol &mdash;
116
- * `Fault` delivers global fault notifications, `Store` delivers per-`Boundary`
117
- * store-change notifications.
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 store-change broadcast. Receives the latest {@link Store}
150
- * snapshot whenever a `context.actions.produce(({ store }) => ...)` call
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.Store, handler)` &mdash; or render against
153
- * it directly with `actions.stream(Lifecycle.Store, (store) => ...)`.
184
+ * `actions.useAction(Lifecycle.Env, handler)` &mdash; 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 store on mount.
190
+ * streams receive the current env on mount.
159
191
  *
160
192
  * @example
161
193
  * ```tsx
162
- * actions.useAction(Lifecycle.Store, (context, store) => {
163
- * console.log("store changed", store);
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.Store, (store) => (
168
- * <span>{store.locale}</span>
199
+ * {actions.stream(Lifecycle.Env, (env) => (
200
+ * <span>{env.locale}</span>
169
201
  * ))}
170
202
  * ```
171
203
  */
172
- static Store: BroadcastPayload<Store, never, "Store">;
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** &ndash; 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** &ndash; Action defines its own scope. Components reach it by
182
- * wrapping a subtree in `withScope(<theMulticastAction>, Component)`.
214
+ * rendering inside a `<scope.Boundary>` produced by `app.Scope<MulticastActions>()`.
183
215
  *
184
216
  * @example
185
217
  * ```ts
186
- * export class Scope {
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 withScope(Scope.Mood, Component);
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(Scope.Mood, mood);
196
- * actions.useAction(Scope.Mood, (context, mood) => { ... });
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 `withScope` boundary. */
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
- * Reactive field type &mdash; a value that may be a concrete `T`, or
242
- * `null` / `undefined` while loading, awaiting a fetch, or before
281
+ * Maybe-present field type &mdash; 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 Reactive<T> = T | null | undefined;
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.resolution()`.
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.resolution(SignedOut);
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 store: DeepReadonly<Store>;
518
+ readonly env: DeepReadonly<Env>;
479
519
  readonly actions: {
480
520
  produce<F extends (draft: {
481
521
  model: M;
482
- store: Store;
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) => PromiseLike<T> & {
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
- resolution<T>(action: BroadcastPayload<T> | MulticastPayload<T>): Promise<T | null>;
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.Store`) can be referenced.
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.Store`
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.Store;
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
@@ -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 AbortError} when the signal is
7
- * aborted. Use to inject a cancellable delay into an action handler.
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 AbortError} if `signal` aborts first.
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 AbortError} rejection.
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 an {@link AbortError} if `signal` aborts first.
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,6 +1,6 @@
1
1
  {
2
2
  "name": "march-hare",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "packageManager": "yarn@1.22.22",
@@ -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()` &mdash; 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
- };