march-hare 0.6.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 (38) hide show
  1. package/README.md +453 -0
  2. package/dist/march-hare.js +6 -0
  3. package/dist/march-hare.umd.cjs +1 -0
  4. package/dist/src/library/action/index.d.ts +66 -0
  5. package/dist/src/library/action/utils.d.ts +89 -0
  6. package/dist/src/library/annotate/index.d.ts +25 -0
  7. package/dist/src/library/boundary/components/broadcast/index.d.ts +12 -0
  8. package/dist/src/library/boundary/components/broadcast/types.d.ts +19 -0
  9. package/dist/src/library/boundary/components/broadcast/utils.d.ts +39 -0
  10. package/dist/src/library/boundary/components/consumer/components/partition/index.d.ts +27 -0
  11. package/dist/src/library/boundary/components/consumer/components/partition/types.d.ts +9 -0
  12. package/dist/src/library/boundary/components/consumer/index.d.ts +19 -0
  13. package/dist/src/library/boundary/components/consumer/types.d.ts +37 -0
  14. package/dist/src/library/boundary/components/consumer/utils.d.ts +13 -0
  15. package/dist/src/library/boundary/components/mode/index.d.ts +15 -0
  16. package/dist/src/library/boundary/components/mode/types.d.ts +7 -0
  17. package/dist/src/library/boundary/components/mode/utils.d.ts +55 -0
  18. package/dist/src/library/boundary/components/scope/index.d.ts +40 -0
  19. package/dist/src/library/boundary/components/scope/types.d.ts +20 -0
  20. package/dist/src/library/boundary/components/scope/utils.d.ts +19 -0
  21. package/dist/src/library/boundary/components/tasks/index.d.ts +14 -0
  22. package/dist/src/library/boundary/components/tasks/types.d.ts +43 -0
  23. package/dist/src/library/boundary/components/tasks/utils.d.ts +26 -0
  24. package/dist/src/library/boundary/index.d.ts +20 -0
  25. package/dist/src/library/boundary/types.d.ts +4 -0
  26. package/dist/src/library/error/index.d.ts +2 -0
  27. package/dist/src/library/error/types.d.ts +75 -0
  28. package/dist/src/library/error/utils.d.ts +15 -0
  29. package/dist/src/library/hooks/index.d.ts +43 -0
  30. package/dist/src/library/hooks/types.d.ts +72 -0
  31. package/dist/src/library/hooks/utils.d.ts +198 -0
  32. package/dist/src/library/index.d.ts +16 -0
  33. package/dist/src/library/resource/index.d.ts +99 -0
  34. package/dist/src/library/types/index.d.ts +718 -0
  35. package/dist/src/library/utils/index.d.ts +42 -0
  36. package/dist/src/library/utils/utils.d.ts +5 -0
  37. package/dist/src/library/utils.d.ts +37 -0
  38. package/package.json +104 -0
@@ -0,0 +1,15 @@
1
+ import { Reason } from './types.ts';
2
+ /**
3
+ * Determines the error reason based on what was thrown.
4
+ *
5
+ * @param error - The value that was thrown.
6
+ * @returns The appropriate Reason enum value.
7
+ */
8
+ export declare function getReason(error: unknown): Reason;
9
+ /**
10
+ * Gets an Error instance from a thrown value.
11
+ *
12
+ * @param error - The value that was thrown.
13
+ * @returns An Error instance (original if already Error, wrapped otherwise).
14
+ */
15
+ export declare function getError(error: unknown): Error;
@@ -0,0 +1,43 @@
1
+ import { Data } from './types.ts';
2
+ import { Model, Props, Actions, UseActions } from '../types/index.ts';
3
+ export { With } from './utils.ts';
4
+ /**
5
+ * A hook for managing state with actions.
6
+ *
7
+ * Call `useActions` first, then use `actions.useAction` to bind handlers
8
+ * to action symbols. Types are pre-baked from the generic parameters, so
9
+ * no additional type annotations are needed on handler calls.
10
+ *
11
+ * The hook returns a result containing:
12
+ * 1. The current model state
13
+ * 2. An actions object with `dispatch`, `inspect`, and `useAction`
14
+ *
15
+ * The `inspect` property provides access to Immertation's annotation system,
16
+ * allowing you to check for pending operations on model properties.
17
+ *
18
+ * @template M The model type representing the component's state.
19
+ * @template AC The actions class containing action definitions.
20
+ * @template D The data type for reactive external values.
21
+ * @param initialModel The initial model state.
22
+ * @param getData Optional function that returns reactive values as data.
23
+ * Values returned are accessible via `context.data` in action handlers,
24
+ * always reflecting the latest values even after await operations.
25
+ * @returns A result `[model, actions]` with pre-typed `useAction` method.
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * // Basic usage
30
+ * const actions = useActions<Model, typeof Actions>(model);
31
+ *
32
+ * // Without a model (actions-only)
33
+ * const actions = useActions<void, typeof Actions>();
34
+ *
35
+ * // With reactive data
36
+ * const actions = useActions<Model, typeof Actions, { query: string }>(
37
+ * model,
38
+ * () => ({ query: props.query }),
39
+ * );
40
+ * ```
41
+ */
42
+ export declare function useActions<_M extends void = void, A extends Actions | void = void, D extends Props = Props>(getData?: Data<D>): UseActions<void, A, D>;
43
+ export declare function useActions<M extends Model, A extends Actions | void = void, D extends Props = Props>(initialModel: M, getData?: Data<D>): UseActions<M, A, D>;
@@ -0,0 +1,72 @@
1
+ import { default as EventEmitter } from 'eventemitter3';
2
+ import { RefObject } from 'react';
3
+ import { Model, HandlerContext, Actions, Props, Tasks, ActionId, Phase, Filter } from '../types/index.ts';
4
+ import { BroadcastEmitter } from '../boundary/components/broadcast/utils.ts';
5
+ import { ScopeContext } from '../boundary/components/scope/types.ts';
6
+ /**
7
+ * Function signature for action handlers registered via `useAction`.
8
+ * Receives the reactive context and payload, returning void or a promise/generator.
9
+ *
10
+ * @template M - The model type
11
+ * @template A - The actions class type
12
+ * @template D - The data props type
13
+ */
14
+ export type Handler<M extends Model | void = Model, A extends Actions | void = Actions, D extends Props = Props> = (context: HandlerContext<M, A, D>, payload: unknown) => void | Promise<void> | AsyncGenerator | Generator;
15
+ /**
16
+ * Entry for an action handler with a reactive channel getter.
17
+ * When getChannel returns undefined, the handler fires for all dispatches.
18
+ * When getChannel returns a channel, dispatches must match.
19
+ */
20
+ export type HandlerEntry<M extends Model | void = Model, A extends Actions | void = Actions, D extends Props = Props> = {
21
+ handler: Handler<M, A, D>;
22
+ getChannel: () => Filter | undefined;
23
+ };
24
+ /**
25
+ * Internal scope for tracking registered action handlers.
26
+ * Maps action IDs to sets of handler entries (with optional channels).
27
+ *
28
+ * @template M - The model type
29
+ * @template A - The actions class type
30
+ * @template D - The data props type
31
+ */
32
+ export type Scope<M extends Model | void = Model, A extends Actions | void = Actions, D extends Props = Props> = {
33
+ /** All handlers for each action, with optional channels */
34
+ handlers: Map<ActionId, Set<HandlerEntry<M, A, D>>>;
35
+ };
36
+ /**
37
+ * Function type for the data snapshot passed to useActions.
38
+ * Returns the current reactive values to be captured in the context.
39
+ *
40
+ * @template D - The data props type
41
+ */
42
+ export type Data<D extends Props = Props> = () => D;
43
+ /**
44
+ * Return type for useDispatchers hook.
45
+ */
46
+ export type Dispatchers = {
47
+ /** Set of registered broadcast action IDs */
48
+ broadcast: Set<ActionId>;
49
+ /** Set of registered multicast action IDs */
50
+ multicast: Set<ActionId>;
51
+ };
52
+ /**
53
+ * Configuration for {@link useLifecycles}.
54
+ */
55
+ export type LifecycleConfig = {
56
+ /** Component-local event emitter for unicast action dispatch */
57
+ unicast: EventEmitter;
58
+ /** Shared broadcast emitter with cached values for cross-component events */
59
+ broadcast: BroadcastEmitter;
60
+ /** Global set of all in-flight tasks across components */
61
+ tasks: Tasks;
62
+ /** Tracked broadcast and multicast action sets for cached replay on mount */
63
+ dispatchers: Dispatchers;
64
+ /** Scope context for multicast cached replay (null when outside any scope) */
65
+ scope: ScopeContext;
66
+ /** Mutable ref tracking the component's current lifecycle phase */
67
+ phase: RefObject<Phase>;
68
+ /** Current snapshot of reactive data props for change detection */
69
+ data: Props;
70
+ /** Handler registry for lifecycle action discovery */
71
+ handlers: Map<ActionId, Set<unknown>>;
72
+ };
@@ -0,0 +1,198 @@
1
+ import { RefObject } from 'react';
2
+ import { Props, Model, Actions, Filter, ActionId, HandlerPayload, ChanneledAction, HandlerContext } from '../types/index.ts';
3
+ import { default as EventEmitter } from 'eventemitter3';
4
+ import { Dispatchers, LifecycleConfig, Scope } from './types.ts';
5
+ import { isChanneledAction, getActionSymbol } from '../action/index.ts';
6
+ import * as React from "react";
7
+ /**
8
+ * Creates a new object with getters for each property of the input object.
9
+ * The getters retrieve the current value from a ref, ensuring that the latest value is always accessed.
10
+ */
11
+ export declare function withGetters<P extends Props>(a: P, b: RefObject<P>): P;
12
+ /**
13
+ * Checks if the given result is a generator or async generator object.
14
+ * Uses `Object.prototype.toString` which reliably returns
15
+ * `"[object Generator]"` or `"[object AsyncGenerator]"` regardless of
16
+ * the generator function's name.
17
+ */
18
+ export declare function isGenerator(result: unknown): result is Generator | AsyncGenerator;
19
+ /**
20
+ * Sentinel passed as the dispatch channel during mount replay. Channeled
21
+ * handlers check for this to skip replay &mdash; they require specific
22
+ * channel context and cannot meaningfully process a replay without it.
23
+ *
24
+ * @internal
25
+ */
26
+ export declare const replay: unique symbol;
27
+ /**
28
+ * Invokes all listeners for an event and returns a promise that resolves
29
+ * when every handler has settled. For {@link BroadcastEmitter} instances the
30
+ * payload is cached before listeners fire so late-mounting components still
31
+ * see the latest value.
32
+ *
33
+ * @internal
34
+ */
35
+ export declare function emitAsync(emitter: EventEmitter, event: string | symbol, ...args: unknown[]): Promise<void>;
36
+ /**
37
+ * Emits lifecycle events for component mount and DOM attachment.
38
+ * Also invokes broadcast action handlers with cached values on mount.
39
+ * Updates the phase ref to track the component's current lifecycle state.
40
+ *
41
+ * The Mount effect skips when `phase` is already `Mounted` — this catches
42
+ * Strict Mode's dev-only double-invocation. It accepts both `Mounting` (first
43
+ * mount) and `Unmounted` (re-mount after `<Activity>` show) as entry states
44
+ * so that hidden-then-shown subtrees correctly re-emit Mount.
45
+ *
46
+ * Phase transitions:
47
+ * - First mount: Mounting → Mounted
48
+ * - Activity hide / show: Mounted → Unmounting → Unmounted → Mounting → Mounted
49
+ */
50
+ export declare function useLifecycles({ unicast, broadcast, dispatchers, scope, phase, data, handlers, }: LifecycleConfig): void;
51
+ /**
52
+ * Creates a data proxy for a given object, returning a memoized version.
53
+ * The proxy provides stable access to the object's properties,
54
+ * even as the original object changes across renders.
55
+ *
56
+ * This is an internal utility used by useActions to provide stable
57
+ * access to reactive values in async action handlers via `context.data`.
58
+ *
59
+ * @template P The type of the object.
60
+ * @param props The object to create a data proxy for.
61
+ * @returns A memoized data proxy of the object.
62
+ */
63
+ export declare function useData<P extends Props>(props: P): P;
64
+ /**
65
+ * Handler factories that wire an action directly to a model field.
66
+ *
67
+ * - {@link With.Update} assigns the dispatched payload to `model[key]`.
68
+ * - {@link With.Invert} flips a boolean field on `model[key]`.
69
+ *
70
+ * Both are typed so the call site fails to compile when `key` is missing or
71
+ * has an incompatible type.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * import { With } from "march-hare";
76
+ *
77
+ * type Model = { name: string; sidebar: boolean };
78
+ *
79
+ * class Actions {
80
+ * static SetName = Action<string>("SetName");
81
+ * static ToggleSidebar = Action("ToggleSidebar");
82
+ * }
83
+ *
84
+ * actions.useAction(Actions.SetName, With.Update("name"));
85
+ * actions.useAction(Actions.ToggleSidebar, With.Invert("sidebar"));
86
+ * ```
87
+ */
88
+ export declare const With: {
89
+ /**
90
+ * Returns a handler that assigns the action payload to `model[key]`.
91
+ *
92
+ * Type-checks at the call site: the payload type must be assignable to
93
+ * the model property's type, and the key must exist on the model.
94
+ */
95
+ 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;
96
+ /**
97
+ * Returns a handler that inverts a boolean field on the model.
98
+ *
99
+ * Type-checks at the call site: `model[key]` must be a boolean.
100
+ */
101
+ 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;
102
+ };
103
+ /**
104
+ * Scans a handler registry for a lifecycle action of the given type.
105
+ *
106
+ * When lifecycle actions become per-class instances (via `Lifecycle.Mount()`),
107
+ * each Actions class has its own unique symbol. Emission sites can no longer
108
+ * emit to a shared singleton — they must discover the component's lifecycle
109
+ * action by scanning the registry keys for matching lifecycle prefixes.
110
+ *
111
+ * Handler maps typically contain 5–15 entries, so the O(n) scan is trivial.
112
+ *
113
+ * @param handlers The handler map from a component's scope.
114
+ * @param type The lifecycle type to find (e.g. `"Mount"`, `"Unmount"`, `"Error"`).
115
+ * @returns The matching ActionId, or `null` if no lifecycle action of that type is registered.
116
+ *
117
+ * @internal
118
+ */
119
+ export declare function findLifecycleAction(handlers: Map<ActionId, Set<unknown>>, type: string): ActionId | null;
120
+ export { isChanneledAction, getActionSymbol };
121
+ /**
122
+ * Manages sets of broadcast and multicast action IDs.
123
+ *
124
+ * This hook creates stable refs for tracking which actions have been registered
125
+ * as broadcast or multicast within a component. These sets are used to:
126
+ * - Replay cached broadcast values on mount
127
+ * - Track multicast subscriptions for scope-based dispatch
128
+ *
129
+ * @returns Object with `broadcast` and `multicast` Sets for action tracking
130
+ *
131
+ * @example
132
+ * ```ts
133
+ * const actions = useDispatchers();
134
+ *
135
+ * // Register a broadcast action
136
+ * actions.broadcast.add(getActionSymbol(MyBroadcastAction));
137
+ *
138
+ * // Check if an action is registered as multicast
139
+ * if (actions.multicast.has(actionId)) {
140
+ * // Handle multicast dispatch
141
+ * }
142
+ * ```
143
+ *
144
+ * @internal
145
+ */
146
+ export declare function useDispatchers(): Dispatchers;
147
+ /**
148
+ * Registers an action handler within a component's scope.
149
+ *
150
+ * This hook binds a handler function to an action, supporting both regular and channeled
151
+ * actions. The handler is wrapped with `useEffectEvent` to ensure it always has access
152
+ * to the latest closure values while maintaining a stable reference.
153
+ *
154
+ * For generator handlers (sync or async), the hook automatically iterates through
155
+ * all yielded values to completion.
156
+ *
157
+ * @template M - The model type representing the component's state
158
+ * @template AC - The actions class containing action definitions
159
+ * @template D - The data type for reactive external values
160
+ *
161
+ * @param scope - Ref to the component's handler scope containing registered handlers
162
+ * @param action - The action to register (ActionId, HandlerPayload, or ChanneledAction)
163
+ * @param handler - The handler function to invoke when the action is dispatched
164
+ *
165
+ * @example
166
+ * ```ts
167
+ * useRegisterHandler(scope, Actions.Increment, async (context, payload) => {
168
+ * context.actions.produce((draft) => {
169
+ * draft.model.count += payload;
170
+ * });
171
+ * });
172
+ *
173
+ * // With channeled action
174
+ * useRegisterHandler(scope, Actions.UserUpdated({ UserId: 5 }), (context, user) => {
175
+ * // Only called when UserId matches 5
176
+ * });
177
+ * ```
178
+ *
179
+ * @internal
180
+ */
181
+ export declare function useRegisterHandler<M extends Model | void, A extends Actions | void, D extends Props>(scope: React.RefObject<Scope<M, A, D>>, action: ActionId | HandlerPayload | ChanneledAction, handler: (context: HandlerContext<M, A, D>, payload: unknown) => void | Promise<void> | AsyncGenerator | Generator): void;
182
+ /**
183
+ * Checks if a dispatch channel matches a registered handler channel.
184
+ * All properties in the dispatch channel must match the corresponding properties in the registered channel.
185
+ *
186
+ * @param dispatchChannel - The channel from the dispatch call (from ChanneledAction)
187
+ * @param registeredChannel - The channel registered with useAction
188
+ * @returns `true` if all dispatch channel properties match the registered channel
189
+ *
190
+ * @example
191
+ * ```ts
192
+ * matchesChannel({ UserId: 1 }, { UserId: 1 }); // true
193
+ * matchesChannel({ UserId: 1 }, { UserId: 2 }); // false
194
+ * matchesChannel({ UserId: 1 }, { UserId: 1, Role: "admin" }); // true (subset match)
195
+ * matchesChannel({ UserId: 1, Role: "admin" }, { UserId: 1 }); // false (missing Role)
196
+ * ```
197
+ */
198
+ export declare function matchesChannel(dispatchChannel: Filter, registeredChannel: Filter): boolean;
@@ -0,0 +1,16 @@
1
+ export { Action } from './action/index.ts';
2
+ export { Distribution, Lifecycle } from './types/index.ts';
3
+ export { Reason, AbortError, TimeoutError } from './error/index.ts';
4
+ export { Operation, Op, State } from 'immertation';
5
+ export { annotate } from './annotate/index.ts';
6
+ export { Boundary } from './boundary/index.tsx';
7
+ export { withScope } from './boundary/components/scope/index.tsx';
8
+ export { useMode } from './boundary/components/mode/index.tsx';
9
+ export type { ModeHandle } from './boundary/components/mode/index.tsx';
10
+ export { useActions, With } from './hooks/index.ts';
11
+ export { Resource } from './resource/index.ts';
12
+ export type { ResourceHandle, ResourceFetcher, BoundRun, IfOptions, } from './resource/index.ts';
13
+ export * as utils from './utils/index.ts';
14
+ export type { Box } from 'immertation';
15
+ export type { Fault } from './error/index.ts';
16
+ export type { Pk, Task, Tasks, Handlers } from './types/index.ts';
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Options accepted by `run.if(...)`.
3
+ *
4
+ * - `over` &ndash; a `Temporal.Duration`, a `DurationLike` object
5
+ * (e.g. `{ minutes: 5 }`), or an ISO 8601 duration string (`"PT5M"`).
6
+ * If the most recent successful run resolved longer ago than this
7
+ * window, `run(...)` is called. Otherwise the cached data is returned
8
+ * without hitting the network.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * await user.run.if({ over: { minutes: 5 } });
13
+ * await user.run.if({ over: "PT5M" });
14
+ * await user.run.if({ over: Temporal.Duration.from({ minutes: 5 }) });
15
+ * ```
16
+ */
17
+ export type IfOptions = {
18
+ readonly over: Temporal.DurationLike;
19
+ };
20
+ /**
21
+ * Fetcher signature accepted by {@link Resource}. Receives the
22
+ * call-site `params` object and returns a `Promise` of the response.
23
+ * Side-effects (dispatching broadcasts, analytics, etc.) belong in
24
+ * the calling `useAction` handler, not inside the fetcher.
25
+ */
26
+ export type ResourceFetcher<T, P extends object = Record<never, never>> = (params: P) => Promise<T>;
27
+ /**
28
+ * Component-bound `run` callable returned by `actions.useResource`. It
29
+ * is invokable like the underlying fetcher (`run(params)`) and also
30
+ * carries an `if` method that triggers the network call only when the
31
+ * cached data is older than the supplied freshness window.
32
+ *
33
+ * The conditional specialisation collapses the call signature when
34
+ * `P` is empty &mdash; `run()` instead of `run({})`.
35
+ */
36
+ export type BoundRun<T, P extends object> = [keyof P] extends [never] ? {
37
+ (): Promise<T>;
38
+ /**
39
+ * Calls `run()` if the most recent successful run resolved longer
40
+ * ago than `options.over`. Otherwise returns the cached data.
41
+ */
42
+ readonly if: (options: IfOptions) => Promise<T>;
43
+ } : {
44
+ (params: P): Promise<T>;
45
+ /**
46
+ * Calls `run(params)` if the most recent successful run resolved
47
+ * longer ago than `options.over`. Otherwise returns the cached data.
48
+ */
49
+ readonly if: (options: IfOptions, params: P) => Promise<T>;
50
+ };
51
+ /**
52
+ * Module-scope handle returned by {@link Resource}. Pass to
53
+ * `actions.useResource(handle)` inside a component to obtain a
54
+ * `{ run, data, at }` object.
55
+ */
56
+ export type ResourceHandle<T, P extends object = Record<never, never>> = {
57
+ readonly key: string;
58
+ /** @internal */
59
+ readonly run: (params: P) => Promise<T>;
60
+ /** Most recent successful data across all param-sets, or `null`. */
61
+ readonly data: T | null;
62
+ /** Instant of the most recent successful run, or `null`. */
63
+ readonly at: Temporal.Instant | null;
64
+ };
65
+ /**
66
+ * Defines a remote resource &mdash; declare at module scope and consume
67
+ * via `actions.useResource(handle)`. Mirrors the {@link Action} factory
68
+ * pattern: the declaration is a value, not a hook.
69
+ *
70
+ * The fetcher takes a single `params` argument (defaults to `{}`) and
71
+ * returns a `Promise<T>`. Resources do **not** carry any callbacks
72
+ * &ndash; any side-effects the caller wants on success or failure
73
+ * (broadcasting, logging, model updates) belong in the `useAction`
74
+ * handler that called `await user.run(...)`.
75
+ *
76
+ * `params` are typed via the second generic and forwarded to every
77
+ * `run(params)` call site. In-flight dedup keys per params shape, so
78
+ * `feed.run({ cursor: null })` and `feed.run({ cursor: "abc" })` execute
79
+ * independently while two concurrent `feed.run({ cursor: "abc" })` calls
80
+ * share one network request.
81
+ *
82
+ * Each call to `run()` always hits the network; `data` and `at`
83
+ * are read-only snapshots of the most recent successful payload and
84
+ * the instant it resolved &ndash; not a memoised result.
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * import { Resource } from "march-hare";
89
+ *
90
+ * export const feed = Resource<Page<Item>, { cursor: string | null }>(
91
+ * "feed",
92
+ * ({ cursor }) =>
93
+ * http
94
+ * .get("feed", { searchParams: { cursor: cursor ?? "" } })
95
+ * .json<Page<Item>>(),
96
+ * );
97
+ * ```
98
+ */
99
+ export declare function Resource<T, P extends object = Record<never, never>>(key: string, fetcher: ResourceFetcher<T, P>): ResourceHandle<T, P>;