march-hare 0.8.0 → 0.10.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 (41) hide show
  1. package/README.md +491 -211
  2. package/dist/actions/index.d.ts +46 -0
  3. package/dist/{hooks → actions}/utils.d.ts +0 -39
  4. package/dist/app/index.d.ts +132 -0
  5. package/dist/app/types.d.ts +82 -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/components/tap/index.d.ts +36 -0
  15. package/dist/boundary/components/tap/types.d.ts +150 -0
  16. package/dist/boundary/components/tap/utils.d.ts +14 -0
  17. package/dist/boundary/index.d.ts +10 -10
  18. package/dist/boundary/types.d.ts +46 -14
  19. package/dist/cache/index.d.ts +4 -4
  20. package/dist/coalesce/index.d.ts +57 -0
  21. package/dist/context/index.d.ts +41 -0
  22. package/dist/context/types.d.ts +14 -0
  23. package/dist/error/index.d.ts +1 -1
  24. package/dist/error/types.d.ts +8 -19
  25. package/dist/index.d.ts +9 -13
  26. package/dist/march-hare.js +8 -5
  27. package/dist/march-hare.umd.cjs +1 -1
  28. package/dist/resource/index.d.ts +55 -78
  29. package/dist/resource/types.d.ts +87 -11
  30. package/dist/resource/utils.d.ts +1 -1
  31. package/dist/scope/index.d.ts +63 -0
  32. package/dist/scope/types.d.ts +55 -0
  33. package/dist/types/index.d.ts +108 -58
  34. package/dist/utils/index.d.ts +6 -5
  35. package/dist/with/index.d.ts +111 -0
  36. package/package.json +1 -1
  37. package/dist/boundary/components/store/index.d.ts +0 -41
  38. package/dist/boundary/components/store/types.d.ts +0 -11
  39. package/dist/boundary/components/store/utils.d.ts +0 -64
  40. package/dist/hooks/index.d.ts +0 -83
  41. /package/dist/{hooks → actions}/types.d.ts +0 -0
@@ -1,8 +1,41 @@
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';
6
+ import { WithHandle } from '../with/index';
5
7
  import * as React from "react";
8
+ /**
9
+ * Chainable handle returned from `context.actions.resource(invocation)`.
10
+ *
11
+ * - `.exceeds(duration)` short-circuits the fetch when the per-params
12
+ * cache age is within the supplied freshness window.
13
+ * - `.coalesce(token)` opts the call into in-flight sharing: any other
14
+ * caller with the same Resource, same structural params, and equal
15
+ * `token` joins the same promise.
16
+ *
17
+ * Awaiting the handle (`await context.actions.resource(...)`) triggers
18
+ * the fetch with whichever options have been set on the chain.
19
+ */
20
+ export type ResourceCall<T> = PromiseLike<T> & {
21
+ /**
22
+ * Skip the fetch when the cached payload is within `duration`.
23
+ * Accepts a `Temporal.Duration`, a `DurationLike` object
24
+ * (`{ minutes: 5 }`), or an ISO 8601 string (`"PT5M"`).
25
+ */
26
+ readonly exceeds: (duration: Temporal.DurationLike) => ResourceCall<T>;
27
+ /**
28
+ * Join an in-flight fetch for the same `(resource, params, token)`
29
+ * tuple. The shared fetch runs against a detached `AbortController`
30
+ * so a single caller's abort never cancels work other callers are
31
+ * waiting on; each caller still sees its own `context.task.controller`
32
+ * abort as a rejection of its personal await.
33
+ *
34
+ * `token` is optional &mdash; omit it to share with every other
35
+ * untokened caller for the same `(resource, params)` slot.
36
+ */
37
+ readonly coalesce: (token?: Coalesce) => ResourceCall<T>;
38
+ };
6
39
  export type { ActionId, Box, Task, Tasks };
7
40
  /**
8
41
  * Type for objects with a Brand.Action symbol property.
@@ -34,7 +67,7 @@ export type BrandedObject = {
34
67
  };
35
68
  /**
36
69
  * Recursive readonly. Locks every nested property so that read-only
37
- * projections on `context` (model, data, store) reject direct assignment
70
+ * projections on `context` (model, data, env) reject direct assignment
38
71
  * &mdash; mutation must go through `context.actions.produce(...)`.
39
72
  *
40
73
  * Function types pass through untouched so method calls (e.g.
@@ -85,13 +118,13 @@ export declare class Brand {
85
118
  */
86
119
  export declare const FaultSymbol: unique symbol;
87
120
  /**
88
- * Internal symbol for the global `Lifecycle.Store` broadcast. The store
121
+ * Internal symbol for the global `Lifecycle.Env` broadcast. The env
89
122
  * mutation path in `useActions` fires this symbol whenever a
90
- * `produce({ store })` call changes the slot reference.
123
+ * `produce({ env })` call changes the slot reference.
91
124
  *
92
125
  * @internal
93
126
  */
94
- export declare const StoreSymbol: unique symbol;
127
+ export declare const EnvSymbol: unique symbol;
95
128
  /**
96
129
  * Factory functions for lifecycle actions.
97
130
  *
@@ -111,10 +144,10 @@ export declare const StoreSymbol: unique symbol;
111
144
  * }
112
145
  * ```
113
146
  *
114
- * `Lifecycle.Fault` and `Lifecycle.Store` are singleton broadcasts (not
147
+ * `Lifecycle.Fault` and `Lifecycle.Env` are singleton broadcasts (not
115
148
  * factories). All components subscribe to the same shared symbol &mdash;
116
- * `Fault` delivers global fault notifications, `Store` delivers per-`Boundary`
117
- * store-change notifications.
149
+ * `Fault` delivers global fault notifications, `Env` delivers per-`Boundary`
150
+ * env-change notifications.
118
151
  */
119
152
  export declare class Lifecycle {
120
153
  /** Creates a Mount lifecycle action. Triggered once on component mount (`useLayoutEffect`). */
@@ -146,30 +179,30 @@ export declare class Lifecycle {
146
179
  */
147
180
  static Fault: BroadcastPayload<Fault, never, "Fault">;
148
181
  /**
149
- * Global store-change broadcast. Receives the latest {@link Store}
150
- * snapshot whenever a `context.actions.produce(({ store }) => ...)` call
182
+ * Global env-change broadcast. Receives the latest {@link Env}
183
+ * snapshot whenever a `context.actions.produce(({ env }) => ...)` call
151
184
  * mutates the slot. Subscribe via
152
- * `actions.useAction(Lifecycle.Store, handler)` &mdash; or render against
153
- * it directly with `actions.stream(Lifecycle.Store, (store) => ...)`.
185
+ * `actions.useAction(Lifecycle.Env, handler)` &mdash; or render against
186
+ * it directly with `actions.stream(Lifecycle.Env, (env) => ...)`.
154
187
  *
155
188
  * Like `Lifecycle.Fault`, this is a singleton broadcast (not a factory):
156
189
  * every subscriber points at the same shared symbol. The latest value is
157
190
  * cached on the broadcast emitter so that late-mounting handlers and
158
- * streams receive the current store on mount.
191
+ * streams receive the current env on mount.
159
192
  *
160
193
  * @example
161
194
  * ```tsx
162
- * actions.useAction(Lifecycle.Store, (context, store) => {
163
- * console.log("store changed", store);
195
+ * actions.useAction(Lifecycle.Env, (context, env) => {
196
+ * console.log("env changed", env);
164
197
  * });
165
198
  *
166
199
  * // In JSX:
167
- * {actions.stream(Lifecycle.Store, (store) => (
168
- * <span>{store.locale}</span>
200
+ * {actions.stream(Lifecycle.Env, (env) => (
201
+ * <span>{env.locale}</span>
169
202
  * ))}
170
203
  * ```
171
204
  */
172
- static Store: BroadcastPayload<Store, never, "Store">;
205
+ static Env: BroadcastPayload<Env, never, "Env">;
173
206
  }
174
207
  /**
175
208
  * Distribution modes for actions.
@@ -179,21 +212,29 @@ export declare class Lifecycle {
179
212
  * - **Broadcast** &ndash; Action is distributed to all mounted components that have
180
213
  * defined a handler for it. Values are cached for late-mounting components.
181
214
  * - **Multicast** &ndash; Action defines its own scope. Components reach it by
182
- * wrapping a subtree in `withScope(<theMulticastAction>, Component)`.
215
+ * rendering inside a `<scope.Boundary>` produced by `app.Scope<MulticastActions>()`.
183
216
  *
184
217
  * @example
185
218
  * ```ts
186
- * export class Scope {
187
- * // The action itself acts as the scope identifier.
219
+ * export class MulticastActions {
188
220
  * static Mood = Action<Mood>("Mood", Distribution.Multicast);
189
221
  * }
190
222
  *
223
+ * export const scope = app.Scope<typeof MulticastActions>();
224
+ *
191
225
  * // Wrap the subtree where the scope applies.
192
- * export default withScope(Scope.Mood, Component);
226
+ * export default function Mood() {
227
+ * return (
228
+ * <scope.Boundary>
229
+ * <Happy />
230
+ * <Sad />
231
+ * </scope.Boundary>
232
+ * );
233
+ * }
193
234
  *
194
235
  * // Dispatch / subscribe — no extra options.
195
- * actions.dispatch(Scope.Mood, mood);
196
- * actions.useAction(Scope.Mood, (context, mood) => { ... });
236
+ * actions.dispatch(MulticastActions.Mood, mood);
237
+ * actions.useAction(MulticastActions.Mood, (context, mood) => { ... });
197
238
  * ```
198
239
  */
199
240
  export declare enum Distribution {
@@ -201,7 +242,7 @@ export declare enum Distribution {
201
242
  Unicast = "unicast",
202
243
  /** Action is broadcast to all mounted components and can be consumed. */
203
244
  Broadcast = "broadcast",
204
- /** Action is multicast to every component inside its `withScope` boundary. */
245
+ /** Action is multicast to every component inside its `<scope.Boundary>`. */
205
246
  Multicast = "multicast"
206
247
  }
207
248
  /**
@@ -238,14 +279,14 @@ export declare enum Phase {
238
279
  */
239
280
  export type Pk<T> = undefined | symbol | T;
240
281
  /**
241
- * Reactive field type &mdash; a value that may be a concrete `T`, or
242
- * `null` / `undefined` while loading, awaiting a fetch, or before
282
+ * Maybe-present field type &mdash; a value that may be a concrete `T`,
283
+ * or `null` / `undefined` while loading, awaiting a fetch, or before
243
284
  * upstream data has arrived. Use this for model fields whose presence
244
285
  * is determined by async or external state.
245
286
  *
246
287
  * @template T - The concrete value type
247
288
  */
248
- export type Reactive<T> = T | null | undefined;
289
+ export type Maybe<T> = T | null | undefined;
249
290
  /**
250
291
  * Base constraint type for model state objects.
251
292
  * Models must be plain objects with string keys.
@@ -315,7 +356,7 @@ export type ChanneledAction<P = unknown, C = unknown, Name extends string = stri
315
356
  * to check whether a cached value exists before performing default fetches.
316
357
  *
317
358
  * 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()`.
359
+ * that only broadcast actions can be passed to `context.actions.final()`.
319
360
  *
320
361
  * @template P - The payload type for the action
321
362
  * @template C - The channel type for channeled dispatches (defaults to never)
@@ -325,7 +366,7 @@ export type ChanneledAction<P = unknown, C = unknown, Name extends string = stri
325
366
  * const SignedOut = Action<User>("SignedOut", Distribution.Broadcast);
326
367
  *
327
368
  * // Resolve the latest value inside a handler
328
- * const user = await context.actions.resolution(SignedOut);
369
+ * const user = await context.actions.final(SignedOut);
329
370
  * ```
330
371
  */
331
372
  export type BroadcastPayload<P = unknown, C extends Filter = never, Name extends string = string> = HandlerPayload<P, C, Name> & {
@@ -442,16 +483,19 @@ export type Filter = Record<string, string | number | bigint | boolean | symbol>
442
483
  */
443
484
  export type ActionOrChanneled<A extends HandlerPayload = HandlerPayload> = A | ChanneledAction;
444
485
  /**
445
- * Checks if a function type returns a Promise.
446
- * @internal
447
- */
448
- type IsAsync<F> = F extends (...args: unknown[]) => Promise<unknown> ? true : false;
449
- /**
450
- * Type guard that produces a compile-time error if an async function is passed.
451
- * Used to enforce synchronous callbacks in `produce()`.
486
+ * Type guard that produces a compile-time error if an async function is
487
+ * passed. Used to enforce synchronous callbacks in `produce()`.
488
+ *
489
+ * The `[F]` tuple wrapping prevents distribution over function unions, and
490
+ * checking the actual signature (`(...args: never[]) => Promise<unknown>`)
491
+ * sidesteps TypeScript's lenient `Promise<void>`→`void` assignability that
492
+ * would otherwise let an async recipe satisfy a `(draft) => void`
493
+ * constraint. Async F collapses the argument type to `never`, which no
494
+ * function value can satisfy.
495
+ *
452
496
  * @internal
453
497
  */
454
- type AssertSync<F> = IsAsync<F> extends true ? "Error: async functions are not allowed in produce" : F;
498
+ type AssertSync<F> = [F] extends [(...args: never[]) => Promise<unknown>] ? never : F;
455
499
  /**
456
500
  * Base type for data props passed to useActions.
457
501
  * Represents any object that can be captured as reactive data.
@@ -469,29 +513,27 @@ export type Actions = object;
469
513
  export type Result = {
470
514
  processes: Set<Process>;
471
515
  };
472
- export type HandlerContext<M extends Model | void, AC extends Actions | void, D extends Props = Props> = {
516
+ export type HandlerContext<M extends Model | void, AC extends Actions | void, D extends Props = Props, S extends Env = Env> = {
473
517
  readonly model: DeepReadonly<M>;
474
518
  readonly phase: Phase;
475
519
  readonly task: Task;
476
520
  readonly data: DeepReadonly<D>;
477
521
  readonly tasks: ReadonlySet<Task>;
478
- readonly store: DeepReadonly<Store>;
522
+ readonly env: Readonly<S>;
479
523
  readonly actions: {
480
524
  produce<F extends (draft: {
481
525
  model: M;
482
- store: Store;
526
+ env: S;
483
527
  readonly inspect: Readonly<Inspect<M>>;
484
528
  }) => void>(ƒ: F & AssertSync<F>): void;
485
529
  dispatch(action: NoPayloadActions<Dispatchable<AC>>): Promise<void>;
486
530
  dispatch<A extends WithPayloadActions<Dispatchable<AC>>>(action: A, payload: Payload<A>): Promise<void>;
487
531
  annotate<T>(value: T, operation?: Operation): T;
488
532
  readonly inspect: Readonly<Inspect<M>>;
489
- resource: (<T>(invocation: T | null) => PromiseLike<T> & {
490
- readonly exceeds: (duration: Temporal.DurationLike) => Promise<T>;
491
- }) & {
533
+ resource: (<T>(invocation: T | null) => ResourceCall<T>) & {
492
534
  set<T>(invocation: T | null, data: T): void;
493
535
  };
494
- resolution<T>(action: BroadcastPayload<T> | MulticastPayload<T>): Promise<T | null>;
536
+ final<T>(action: BroadcastPayload<T> | MulticastPayload<T>): Promise<T | null>;
495
537
  peek<T>(action: BroadcastPayload<T> | MulticastPayload<T>): T | null;
496
538
  };
497
539
  };
@@ -512,7 +554,7 @@ export type HandlerContext<M extends Model | void, AC extends Actions | void, D
512
554
  * @example
513
555
  * ```tsx
514
556
  * const [model, actions, data] = useActions<Model, typeof Actions, Data>(
515
- * initialModel,
557
+ * model,
516
558
  * () => ({ user, theme }),
517
559
  * );
518
560
  *
@@ -540,7 +582,7 @@ export type HandlerContext<M extends Model | void, AC extends Actions | void, D
540
582
  *
541
583
  * @see {@link Handlers} for the recommended HKT pattern
542
584
  */
543
- export type Handler<M extends Model | void, AC extends Actions | void, K extends keyof AC & string, D extends Props = Props> = (context: HandlerContext<M, AC, D>, ...args: [Payload<AC[K] & HandlerPayload<unknown>>] extends [never] ? [] : [payload: Payload<AC[K] & HandlerPayload<unknown>>]) => void | Promise<void> | AsyncGenerator | Generator;
585
+ export type Handler<M extends Model | void, AC extends Actions | void, K extends keyof AC & string, D extends Props = Props, S extends Env = Env> = (context: HandlerContext<M, AC, D, S>, ...args: [Payload<AC[K] & HandlerPayload<unknown>>] extends [never] ? [] : [payload: Payload<AC[K] & HandlerPayload<unknown>>]) => void | Promise<void> | AsyncGenerator | Generator;
544
586
  /**
545
587
  * String keys of `AC` excluding inherited `prototype` from class constructors.
546
588
  * When action containers are classes (`typeof MyActions`), TypeScript includes
@@ -557,7 +599,7 @@ type OwnKeys<AC> = Exclude<keyof AC & string, "prototype">;
557
599
  *
558
600
  * Used to constrain `dispatch` and `useAction` so that only actions owned by
559
601
  * the component's `AC` (plus the global `Lifecycle.Fault` /
560
- * `Lifecycle.Store`) can be referenced.
602
+ * `Lifecycle.Env`) can be referenced.
561
603
  */
562
604
  export type LeafActions<AC> = AC extends void ? never : {
563
605
  [K in OwnKeys<AC>]: OwnKeys<AC[K]> extends never ? AC[K] : LeafActions<AC[K]>;
@@ -576,10 +618,10 @@ export type ChanneledOf<A> = A extends HandlerPayload<infer P, infer C> ? [C] ex
576
618
  export type Dispatchable<AC> = LeafActions<AC> | ChanneledOf<LeafActions<AC>>;
577
619
  /**
578
620
  * Everything `useAction` will subscribe to for a given `AC`: same as
579
- * `Dispatchable<AC>` plus the shared `Lifecycle.Fault` and `Lifecycle.Store`
621
+ * `Dispatchable<AC>` plus the shared `Lifecycle.Fault` and `Lifecycle.Env`
580
622
  * broadcasts which live outside `AC` but are subscribable by any component.
581
623
  */
582
- export type Subscribable<AC> = Dispatchable<AC> | typeof Lifecycle.Fault | typeof Lifecycle.Store;
624
+ export type Subscribable<AC> = Dispatchable<AC> | typeof Lifecycle.Fault | typeof Lifecycle.Env;
583
625
  /**
584
626
  * Subset of a union of actions whose payload type is `never`. Used to split
585
627
  * `dispatch`/`useAction` into a no-payload and a with-payload overload so
@@ -629,10 +671,10 @@ export type WithPayloadActions<U> = Exclude<U, {
629
671
  * export const handlePaymentSent: H["Broadcast"]["PaymentSent"] = (context) => { ... };
630
672
  * ```
631
673
  */
632
- export type Handlers<M extends Model | void, AC extends Actions | void, D extends Props = Props, RootAC extends Actions | void = AC> = {
633
- [K in OwnKeys<AC>]: OwnKeys<AC[K]> extends never ? (context: HandlerContext<M, RootAC, D>, ...args: [Payload<AC[K] & HandlerPayload<unknown>>] extends [never] ? [] : [payload: Payload<AC[K] & HandlerPayload<unknown>>]) => void | Promise<void> | AsyncGenerator | Generator : Handlers<M, AC[K] & Actions, D, RootAC>;
674
+ export type Handlers<M extends Model | void, AC extends Actions | void, D extends Props = Props, RootAC extends Actions | void = AC, S extends Env = Env> = {
675
+ [K in OwnKeys<AC>]: OwnKeys<AC[K]> extends never ? (context: HandlerContext<M, RootAC, D, S>, ...args: [Payload<AC[K] & HandlerPayload<unknown>>] extends [never] ? [] : [payload: Payload<AC[K] & HandlerPayload<unknown>>]) => void | Promise<void> | AsyncGenerator | Generator : Handlers<M, AC[K] & Actions, D, RootAC, S>;
634
676
  };
635
- export type UseActions<M extends Model | void, AC extends Actions | void, D extends Props = Props> = [
677
+ export type UseActions<M extends Model | void, AC extends Actions | void, D extends Props = Props, S extends Env = Env> = [
636
678
  Readonly<M>,
637
679
  {
638
680
  /**
@@ -665,6 +707,7 @@ export type UseActions<M extends Model | void, AC extends Actions | void, D exte
665
707
  * );
666
708
  * ```
667
709
  */
710
+ stream(action: typeof Lifecycle.Env, renderer: (value: Readonly<S>, inspect: Inspect<S>) => React.ReactNode): React.ReactNode;
668
711
  stream<T extends object>(action: BroadcastPayload<T>, renderer: (value: T, inspect: Inspect<T>) => React.ReactNode): React.ReactNode;
669
712
  },
670
713
  DeepReadonly<D>
@@ -705,8 +748,9 @@ export type UseActions<M extends Model | void, AC extends Actions | void, D exte
705
748
  * });
706
749
  * ```
707
750
  */
708
- useAction(action: NoPayloadActions<Subscribable<AC>>, handler: (context: HandlerContext<M, AC, D>) => void | Promise<void> | AsyncGenerator | Generator): void;
709
- useAction<A extends WithPayloadActions<Subscribable<AC>>>(action: A, handler: (context: HandlerContext<M, AC, D>, payload: Payload<A>) => void | Promise<void> | AsyncGenerator | Generator): void;
751
+ useAction(action: NoPayloadActions<Subscribable<AC>>, handler: (context: HandlerContext<M, AC, D, S>) => void | Promise<void> | AsyncGenerator | Generator): void;
752
+ useAction(action: typeof Lifecycle.Env, handler: (context: HandlerContext<M, AC, D, S>, env: Readonly<S>) => void | Promise<void> | AsyncGenerator | Generator): void;
753
+ useAction<A extends WithPayloadActions<Subscribable<AC>>>(action: A, handler: (context: HandlerContext<M, AC, D, S>, payload: Payload<A>) => void | Promise<void> | AsyncGenerator | Generator): void;
710
754
  };
711
755
  /**
712
756
  * Stable, typed dispatch function for the actions class `AC`. Same call
@@ -729,10 +773,16 @@ export type Dispatch<AC extends Actions | void> = {
729
773
  * `React.Context` &mdash; it's the March Hare action surface returned by
730
774
  * the `useContext` hook of this library.
731
775
  */
732
- export type Context<M extends Model | void, AC extends Actions | void, D extends Props = Props> = {
776
+ export type Context<M extends Model | void, AC extends Actions | void, D extends Props = Props, S extends Env = Env> = {
733
777
  readonly actions: {
734
778
  dispatch: Dispatch<AC>;
735
779
  };
736
- useActions(getData?: () => D): UseActions<M, AC, D>;
737
- useActions(initialModel: M extends void ? never : M, getData?: () => D): UseActions<M, AC, D>;
780
+ /**
781
+ * Typed bag of handler factories bound to `M`. Methods accept lodash-style
782
+ * dotted paths with array indices (e.g. `"a.b.c"`, `"items.0.id"`) and
783
+ * autocomplete from the model. See {@link WithHandle}.
784
+ */
785
+ readonly with: WithHandle<M>;
786
+ useActions(getData?: () => D): UseActions<M, AC, D, S>;
787
+ useActions(model: M extends void ? never : M, getData?: () => D): UseActions<M, AC, D, S>;
738
788
  };
@@ -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,111 @@
1
+ import { Actions, HandlerContext, Model, Props } from '../types/index';
2
+ import { Env } from '../boundary/components/env/index';
3
+ type Primitive = string | number | bigint | boolean | symbol | null | undefined;
4
+ type Depth = [never, 0, 1, 2, 3, 4, 5];
5
+ /**
6
+ * Lodash-style dotted paths reachable from `T`. Yields `"a"`, `"a.b"`,
7
+ * `"items.0"`, `"items.0.name"`, etc. Recursion is capped at depth 5 to
8
+ * keep the type-checker tractable on deeply nested models.
9
+ *
10
+ * @template T The object type to enumerate paths from.
11
+ * @template D Recursion budget (internal).
12
+ */
13
+ export type Paths<T, D extends number = 5> = [D] extends [never] ? never : T extends Primitive ? never : T extends ReadonlyArray<infer U> ? `${number}` | (U extends Primitive ? never : `${number}.${Paths<U, Depth[D]>}`) : T extends object ? {
14
+ [K in Extract<keyof T, string>]: T[K] extends Primitive ? K : K | `${K}.${Paths<T[K], Depth[D]>}`;
15
+ }[Extract<keyof T, string>] : never;
16
+ /**
17
+ * Subset of {@link Paths} whose leaf type is `boolean`. Used by
18
+ * `context.with.invert` (and the legacy {@link With.Invert}) to restrict the
19
+ * key to togglable fields only.
20
+ *
21
+ * @template T The object type to enumerate boolean leaves from.
22
+ * @template D Recursion budget (internal).
23
+ */
24
+ export type BooleanPaths<T, D extends number = 5> = [D] extends [never] ? never : T extends Primitive ? never : T extends ReadonlyArray<infer U> ? (U extends boolean ? `${number}` : never) | (U extends Primitive ? never : `${number}.${BooleanPaths<U, Depth[D]>}`) : T extends object ? {
25
+ [K in Extract<keyof T, string>]: T[K] extends boolean ? K : T[K] extends Primitive ? never : `${K}.${BooleanPaths<T[K], Depth[D]>}`;
26
+ }[Extract<keyof T, string>] : never;
27
+ /**
28
+ * Resolves the leaf type at a dotted path on `T`. `Get<{a:{b:number}},"a.b">`
29
+ * is `number`; `Get<{items: string[]},"items.0">` is `string`.
30
+ *
31
+ * @template T The object type to walk.
32
+ * @template P The dotted path string.
33
+ */
34
+ export type Get<T, P extends string> = P extends `${infer Head}.${infer Tail}` ? T extends ReadonlyArray<infer U> ? Head extends `${number}` ? Get<U, Tail> : never : Head extends keyof T ? Get<T[Head], Tail> : never : T extends ReadonlyArray<infer U> ? P extends `${number}` ? U : never : P extends keyof T ? T[P] : never;
35
+ /**
36
+ * Returned by `context.with` &mdash; a typed bag of handler factories
37
+ * bound to the model `M` declared in `useContext<M, …>()`. Methods accept
38
+ * lodash-style dotted paths (`"a.b.c"`) with array indices (`"items.0.id"`).
39
+ *
40
+ * - `update(key)` &mdash; assigns the dispatched payload to `model[key]`.
41
+ * - `invert(key)` &mdash; flips a boolean leaf at `model[key]`.
42
+ * - `always(key, value)` &mdash; assigns a fixed `value` to `model[key]`,
43
+ * ignoring any dispatched payload.
44
+ *
45
+ * @template M The model type to bind keys against.
46
+ */
47
+ export type WithHandle<M> = M extends Model ? {
48
+ update<K extends Paths<M>>(key: K): <A extends Actions | void, D extends Props, S extends Env = Env>(context: HandlerContext<M, A, D, S>, payload: Get<M, K>) => void;
49
+ invert<K extends BooleanPaths<M>>(key: K): <A extends Actions | void, D extends Props, S extends Env = Env>(context: HandlerContext<M, A, D, S>) => void;
50
+ always<K extends Paths<M>>(key: K, value: Get<M, K>): <A extends Actions | void, D extends Props, S extends Env = Env>(context: HandlerContext<M, A, D, S>) => void;
51
+ } : Record<string, never>;
52
+ /**
53
+ * Builds the {@link WithHandle} object returned via `context.with`. The
54
+ * runtime is identical for any model &mdash; only the call-site types differ.
55
+ *
56
+ * @internal
57
+ */
58
+ export declare function bindWith<M extends Model | void>(): WithHandle<M>;
59
+ /**
60
+ * Handler factories that wire an action directly to a model field. Prefer
61
+ * `context.with` from `useContext<Model>()` for first-class autocompletion
62
+ * over dotted paths; this top-level form is kept for callers that don't have
63
+ * a typed `context` in scope.
64
+ *
65
+ * - {@link With.Update} assigns the dispatched payload to a model path.
66
+ * - {@link With.Invert} flips a boolean leaf at a model path.
67
+ * - {@link With.Always} assigns a fixed value to a model path, ignoring any
68
+ * dispatched payload.
69
+ *
70
+ * Keys may be lodash-style dotted paths (`"a.b.c"`) and support array
71
+ * indices (`"items.0.name"`). The model type is inferred at handler-bind
72
+ * time; an invalid path or mismatched payload fails to compile when the
73
+ * handler is registered with `useAction`.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * import { With } from "march-hare";
78
+ *
79
+ * type Model = {
80
+ * name: string;
81
+ * sidebar: boolean;
82
+ * nested: { open: boolean };
83
+ * items: { id: number }[];
84
+ * };
85
+ *
86
+ * actions.useAction(Actions.SetName, With.Update("name"));
87
+ * actions.useAction(Actions.SetFirstId, With.Update("items.0.id"));
88
+ * actions.useAction(Actions.ToggleSidebar, With.Invert("sidebar"));
89
+ * actions.useAction(Actions.ToggleNested, With.Invert("nested.open"));
90
+ * actions.useAction(Actions.Open, With.Always("nested.open", true));
91
+ * ```
92
+ */
93
+ export declare const With: {
94
+ /**
95
+ * Returns a handler that assigns the action payload to the model leaf at
96
+ * the given lodash-style path. The payload type must match `Get<M, K>`,
97
+ * and the path must exist on the model.
98
+ */
99
+ Update<K extends string>(key: K): <M extends Model, A extends Actions | void, D extends Props, P extends K extends Paths<M> ? Get<M, K> : never, S extends Env = Env>(context: HandlerContext<M, A, D, S>, payload: P) => void;
100
+ /**
101
+ * Returns a handler that inverts a boolean leaf at the given lodash-style
102
+ * path. The leaf must be a `boolean` on the model.
103
+ */
104
+ Invert<K extends string>(key: K): <M extends Model, A extends Actions | void, D extends Props, S extends Env = Env>(context: K extends BooleanPaths<M> ? HandlerContext<M, A, D, S> : never) => void;
105
+ /**
106
+ * Returns a handler that assigns a fixed `value` to the model leaf at the
107
+ * given lodash-style path. The dispatched payload (if any) is ignored.
108
+ */
109
+ Always<K extends string, V>(key: K, value: V): <M extends Model, A extends Actions | void, D extends Props, S extends Env = Env>(context: K extends Paths<M> ? V extends Get<M, K> ? HandlerContext<M, A, D, S> : never : never) => void;
110
+ };
111
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "march-hare",
3
- "version": "0.8.0",
3
+ "version": "0.10.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
- };
@@ -1,64 +0,0 @@
1
- import { RefObject } from 'react';
2
- import { Store } from './index';
3
- import * as React from "react";
4
- /**
5
- * React context exposing the per-Boundary Store ref. The ref itself is
6
- * stable across renders &mdash; readers grab `.current` at call time.
7
- *
8
- * @internal
9
- */
10
- export declare const Context: React.Context<React.RefObject<Store>>;
11
- /**
12
- * Hook that returns a read-only handle to the per-Boundary {@link Store}.
13
- * Reads use plain dot notation (`store.session`) and always reflect the
14
- * latest value, even after `await` boundaries &mdash; the handle is a
15
- * `Proxy` that delegates property access to the live ref.
16
- *
17
- * Writes are not exposed here. Mutate the Store inside an action handler
18
- * via `context.actions.produce(({ model, store }) => { store.x = ... })`
19
- * &mdash; the same Immer-style recipe used for the model. Mutations do
20
- * **not** trigger a re-render; drive view state through the model.
21
- *
22
- * The Store's shape is declared via module augmentation on the library's
23
- * {@link Store} interface, so dot reads are fully typed at every call
24
- * site.
25
- *
26
- * @example
27
- * ```ts
28
- * declare module "march-hare" {
29
- * interface Store {
30
- * session: Session | null;
31
- * locale: string;
32
- * }
33
- * }
34
- *
35
- * function useAuthActions() {
36
- * const store = useStore();
37
- * const actions = useActions<void, typeof Actions>();
38
- *
39
- * actions.useAction(Actions.SignIn, async (context, credentials) => {
40
- * const result = await context.actions.resource(signIn(credentials));
41
- * context.actions.produce(({ store }) => {
42
- * store.session = result;
43
- * });
44
- * });
45
- *
46
- * actions.useAction(Actions.Refresh, async (context) => {
47
- * if (store.session === null) return;
48
- * // ...
49
- * });
50
- *
51
- * return actions;
52
- * }
53
- * ```
54
- */
55
- export declare function useStore(): Store;
56
- /**
57
- * Internal accessor for the per-Boundary Store ref &mdash; used by the
58
- * Resource layer to pass a fresh snapshot to each fetcher invocation
59
- * and by the action layer to write through `context.actions.produce`.
60
- * Not exported from the library.
61
- *
62
- * @internal
63
- */
64
- export declare function useStoreRef(): RefObject<Store>;