march-hare 0.6.0 → 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.
@@ -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, _AC extends Actions | void, D extends Props = Props> = {
405
- readonly model: Readonly<M>;
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 &mdash; 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
+ * &mdash; 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: ActionOrChanneled, payload?: unknown): Promise<void>;
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 })`) &mdash; the
557
+ * call primes a slot with the resource and params, and
558
+ * `.resource(...)` reads it. The return value is a thenable &mdash;
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 &mdash; 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<P>(action: HandlerPayload<P>, payload?: P): Promise<void>;
637
- dispatch<P>(action: BroadcastPayload<P>, payload?: P): Promise<void>;
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,30 +829,6 @@ export type UseActions<M extends Model | void, AC extends Actions | void, D exte
689
829
  * });
690
830
  * ```
691
831
  */
692
- useAction<A extends ActionId | HandlerPayload | ChanneledAction>(action: A, handler: (context: HandlerContext<M, AC, D>, ...args: [Payload<A>] extends [never] ? [] : [payload: Payload<A>]) => void | Promise<void> | AsyncGenerator | Generator): void;
693
- /**
694
- * Connects a {@link Resource} declared at module scope to this component.
695
- * Returns a frozen `{ run, data, at }` object &ndash; `run` triggers a
696
- * fresh network call (concurrent calls share the in-flight promise),
697
- * while `data` and `at` are read-only snapshots of the most recent
698
- * successful payload and the instant it resolved.
699
- *
700
- * `data` and `at` are non-reactive &mdash; reading them does not
701
- * subscribe the component to updates. Drive UI from the model.
702
- *
703
- * @example
704
- * ```ts
705
- * const user = actions.useResource(resources.user);
706
- *
707
- * actions.useAction(Actions.Mount, async (context) => {
708
- * const data = await user.run();
709
- * context.actions.produce(({ model }) => { model.user = data; });
710
- * });
711
- * ```
712
- */
713
- useResource<T, P extends object>(resource: import('../resource/index.ts').ResourceHandle<T, P>): Readonly<{
714
- run: import('../resource/index.ts').BoundRun<T, P>;
715
- data: T | null;
716
- at: Temporal.Instant | null;
717
- }>;
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;
718
834
  };
@@ -1,42 +1,52 @@
1
1
  import { Pk } from '../types/index.ts';
2
+ export { unset } from './utils.ts';
3
+ export type { Stored, Unset } from './types.ts';
2
4
  /**
3
- * Returns a promise that resolves after the specified number of milliseconds.
4
- * The sleep will reject with an AbortError when the signal is aborted,
5
- * allowing cleanup of pending operations.
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
8
  *
7
- * @param ms The number of milliseconds to sleep.
8
- * @param signal AbortSignal to cancel the sleep early.
9
- * @returns A promise that resolves after the delay or rejects if aborted.
9
+ * @param ms How long to wait before resolving.
10
+ * @param signal Optional {@link AbortSignal} that cancels the sleep early.
11
+ * Pass `context.task.controller.signal` to tie the wait to
12
+ * the lifetime of the current action.
13
+ * @returns A promise that resolves after `ms` milliseconds or rejects with
14
+ * an {@link AbortError} if `signal` aborts first.
10
15
  */
11
16
  export declare function sleep(ms: number, signal: AbortSignal | undefined): Promise<void>;
12
- /** Shorthand alias for {@link sleep}. */
13
- export declare const ζ: typeof sleep;
14
17
  /**
15
- * Repeatedly calls a function at a fixed interval until it returns `true` or
16
- * the signal is aborted. The function is invoked immediately on the first
17
- * iteration, then after each interval.
18
+ * Repeatedly calls a function at a fixed interval until it returns `true`
19
+ * or the signal is aborted. The function is invoked immediately on the
20
+ * first iteration, then after each interval.
18
21
  *
19
- * @param ms The interval in milliseconds between invocations.
20
- * @param signal Optional AbortSignal to cancel polling early.
21
- * @param fn Callback invoked each iteration. Return `true` to stop polling.
22
- * @returns A promise that resolves when `fn` returns `true`, or rejects with
23
- * an AbortError if the signal is aborted.
22
+ * @param ms Interval in milliseconds between invocations of `fn`.
23
+ * @param signal Optional {@link AbortSignal} that cancels polling early.
24
+ * Aborts propagate as an {@link AbortError} rejection.
25
+ * @param fn Predicate invoked each iteration. Return `true` to stop
26
+ * polling, `false` to schedule another invocation after `ms`.
27
+ * May be sync or async.
28
+ * @returns A promise that resolves when `fn` returns `true`, or rejects
29
+ * with an {@link AbortError} if `signal` aborts first.
24
30
  */
25
31
  export declare function poll(ms: number, signal: AbortSignal | undefined, fn: () => boolean | Promise<boolean>): Promise<void>;
26
- /** Shorthand alias for {@link poll}. */
27
- export declare const π: typeof poll;
28
32
  /**
29
33
  * Generates a unique primary key.
34
+ *
30
35
  * @returns A new unique symbol representing the primary key.
31
36
  */
32
37
  export declare function pk(): symbol;
33
38
  /**
34
- * Checks if the provided ID is a valid primary key.
35
- * A valid primary key is considered any value that is not a symbol.
36
- * @template T The type of the object.
39
+ * Checks if the provided ID is a valid primary key. A valid primary key
40
+ * is any value that is not a symbol.
41
+ *
42
+ * @template T The model type the key identifies.
37
43
  * @param id The primary key to validate.
38
- * @returns `true` if the ID is valid, `false` otherwise.
44
+ * @returns `true` if `id` is a non-symbol value, `false` otherwise.
39
45
  */
40
46
  export declare function pk<T>(id: Pk<T>): boolean;
47
+ /** Shorthand alias for {@link sleep}. */
48
+ export declare const ζ: typeof sleep;
49
+ /** Shorthand alias for {@link poll}. */
50
+ export declare const π: typeof poll;
41
51
  /** Shorthand alias for {@link pk}. */
42
52
  export declare const κ: typeof pk;
@@ -0,0 +1,18 @@
1
+ import { unset } from './utils.ts';
2
+ /** Nominal type of the {@link unset} sentinel. */
3
+ export type Unset = typeof unset;
4
+ /**
5
+ * Common shape for a possibly-present value with a timestamp. Produced by
6
+ * `Cache.get(key)` (from persistent storage or in-memory) and consumed
7
+ * internally by Resource's per-params cache slots.
8
+ *
9
+ * @template T The payload type when present.
10
+ */
11
+ export type Stored<T> = {
12
+ /** The payload, or {@link unset} when nothing is recorded. */
13
+ readonly data: T | Unset;
14
+ /** When the payload was recorded, or `null` when nothing is recorded. */
15
+ readonly at: Temporal.Instant | null;
16
+ /** Returns {@link data} when present, otherwise the supplied fallback. */
17
+ readonly else: <U>(fallback: U) => T | U;
18
+ };
@@ -1,5 +1,36 @@
1
+ import { Stored } from './types.ts';
1
2
  /**
2
- * Returns a function to force a component re-render.
3
- * Useful when state is managed externally (e.g., refs) but the UI needs updating.
3
+ * Sentinel symbol marking "no value present yet". Shared by the Resource
4
+ * cache and by storage handles so callers can distinguish "nothing has been
5
+ * recorded" from "a legitimately stored null".
6
+ */
7
+ export declare const unset: unique symbol;
8
+ /**
9
+ * Returns a function to force a component re-render. Useful when state is
10
+ * managed externally (e.g., refs) but the UI needs updating.
11
+ *
12
+ * @returns A zero-arg callback that schedules a re-render of the host
13
+ * component when invoked.
4
14
  */
5
15
  export declare function useRerender(): () => void;
16
+ /**
17
+ * Constructs a {@link Stored} in the empty state. Internal helper shared
18
+ * by the storage layer and the Resource snapshot accessor.
19
+ *
20
+ * @template T The payload type the resulting Stored would carry if populated.
21
+ * @returns A Stored with `data` set to {@link unset} and `at` set to `null`.
22
+ * Its `.else(fallback)` returns the fallback unchanged.
23
+ * @internal
24
+ */
25
+ export declare function empty<T>(): Stored<T>;
26
+ /**
27
+ * Constructs a {@link Stored} wrapping a present payload and timestamp.
28
+ *
29
+ * @template T The payload type carried by the Stored.
30
+ * @param data The payload value to wrap.
31
+ * @param at The instant the payload was recorded — flows through to the
32
+ * Resource cache as the entry's `at` timestamp.
33
+ * @returns A Stored whose `.else(fallback)` returns `data` unchanged.
34
+ * @internal
35
+ */
36
+ export declare function present<T>(data: T, at: Temporal.Instant): Stored<T>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "march-hare",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "packageManager": "yarn@1.22.22",
@@ -30,7 +30,7 @@
30
30
  ],
31
31
  "scripts": {
32
32
  "build": "vite build",
33
- "build:example": "vite build --mode example --outDir dist-example --base /march-hare/",
33
+ "build:example": "vite build --mode example --outDir dist-example --base /MarchHare/ && mv dist-example/src/example/index.html dist-example/index.html && rm -rf dist-example/src",
34
34
  "dev": "vite",
35
35
  "preview": "vite preview",
36
36
  "release": "make checks",
@@ -88,7 +88,7 @@
88
88
  "react-flip-numbers": "^3.0.9",
89
89
  "react-router-dom": "^7.13.0",
90
90
  "react-test-renderer": "^19.2.4",
91
- "react-wayfinder": "^0.1.3",
91
+ "react-wayfinder": "^0.1.4",
92
92
  "rollup-plugin-visualizer": "^6.0.5",
93
93
  "terser": "^5.46.0",
94
94
  "traverse": "^0.6.11",
@@ -1,15 +0,0 @@
1
- import { Props } from './types.ts';
2
- import * as React from "react";
3
- export { useMode } from './utils.ts';
4
- export type { ModeHandle } from './utils.ts';
5
- /**
6
- * Provides a single mutable mode handle to every component inside the
7
- * boundary. Components opt in by calling {@link useMode} and threading the
8
- * returned handle through {@link useActions}'s `data` callback.
9
- *
10
- * Mode is **not** reactive &mdash; mutating it does not trigger a re-render.
11
- * Use it for cross-handler coordination (e.g. flagging an in-progress
12
- * sign-out) when you do not want the value showing up as render-time UI
13
- * state.
14
- */
15
- export declare function Mode({ children }: Props): React.ReactNode;
@@ -1,7 +0,0 @@
1
- import { ReactNode } from 'react';
2
- /**
3
- * Props for the Mode provider component.
4
- */
5
- export type Props = {
6
- children: ReactNode;
7
- };
@@ -1,55 +0,0 @@
1
- import * as React from "react";
2
- /**
3
- * React context exposing the per-Boundary mode handle. The handle itself
4
- * is stable across renders &mdash; readers grab `.current` at call time.
5
- *
6
- * @internal
7
- */
8
- export declare const Context: React.Context<React.RefObject<unknown>>;
9
- /**
10
- * Handle returned by {@link useMode}. Reads always reflect the latest
11
- * write, even after `await` boundaries.
12
- */
13
- export type ModeHandle<T> = {
14
- read(): T | null;
15
- update(value: T | null): void;
16
- };
17
- /**
18
- * Hook that returns a `{ read, update }` handle to the per-Boundary mode
19
- * value.
20
- *
21
- * Mode is a single mutable value shared across every component inside the
22
- * surrounding `<Boundary>`. It is **not** reactive &mdash; mutating it does
23
- * not trigger a re-render. Use it for coordinating between async action
24
- * handlers (e.g. short-circuiting refreshes during sign-out).
25
- *
26
- * Pass the handle through the {@link useActions} `data` callback so it
27
- * shows up under `context.data` inside handlers, fully typed.
28
- *
29
- * @example
30
- * ```ts
31
- * enum Mode {
32
- * Idle,
33
- * SigningOut,
34
- * }
35
- *
36
- * function useSignOutActions() {
37
- * const mode = useMode<Mode>();
38
- * const actions = useActions<Model, typeof Actions>(model, () => ({ mode }));
39
- *
40
- * actions.useAction(Actions.SignOut, async (context) => {
41
- * context.data.mode.update(Mode.SigningOut);
42
- * await api.signOut();
43
- * context.data.mode.update(Mode.Idle);
44
- * });
45
- *
46
- * actions.useAction(Actions.Refresh, async (context) => {
47
- * if (context.data.mode.read() === Mode.SigningOut) return;
48
- * // ...
49
- * });
50
- *
51
- * return actions;
52
- * }
53
- * ```
54
- */
55
- export declare function useMode<T>(): ModeHandle<T>;