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.
- package/README.md +453 -0
- package/dist/march-hare.js +6 -0
- package/dist/march-hare.umd.cjs +1 -0
- package/dist/src/library/action/index.d.ts +66 -0
- package/dist/src/library/action/utils.d.ts +89 -0
- package/dist/src/library/annotate/index.d.ts +25 -0
- package/dist/src/library/boundary/components/broadcast/index.d.ts +12 -0
- package/dist/src/library/boundary/components/broadcast/types.d.ts +19 -0
- package/dist/src/library/boundary/components/broadcast/utils.d.ts +39 -0
- package/dist/src/library/boundary/components/consumer/components/partition/index.d.ts +27 -0
- package/dist/src/library/boundary/components/consumer/components/partition/types.d.ts +9 -0
- package/dist/src/library/boundary/components/consumer/index.d.ts +19 -0
- package/dist/src/library/boundary/components/consumer/types.d.ts +37 -0
- package/dist/src/library/boundary/components/consumer/utils.d.ts +13 -0
- package/dist/src/library/boundary/components/mode/index.d.ts +15 -0
- package/dist/src/library/boundary/components/mode/types.d.ts +7 -0
- package/dist/src/library/boundary/components/mode/utils.d.ts +55 -0
- package/dist/src/library/boundary/components/scope/index.d.ts +40 -0
- package/dist/src/library/boundary/components/scope/types.d.ts +20 -0
- package/dist/src/library/boundary/components/scope/utils.d.ts +19 -0
- package/dist/src/library/boundary/components/tasks/index.d.ts +14 -0
- package/dist/src/library/boundary/components/tasks/types.d.ts +43 -0
- package/dist/src/library/boundary/components/tasks/utils.d.ts +26 -0
- package/dist/src/library/boundary/index.d.ts +20 -0
- package/dist/src/library/boundary/types.d.ts +4 -0
- package/dist/src/library/error/index.d.ts +2 -0
- package/dist/src/library/error/types.d.ts +75 -0
- package/dist/src/library/error/utils.d.ts +15 -0
- package/dist/src/library/hooks/index.d.ts +43 -0
- package/dist/src/library/hooks/types.d.ts +72 -0
- package/dist/src/library/hooks/utils.d.ts +198 -0
- package/dist/src/library/index.d.ts +16 -0
- package/dist/src/library/resource/index.d.ts +99 -0
- package/dist/src/library/types/index.d.ts +718 -0
- package/dist/src/library/utils/index.d.ts +42 -0
- package/dist/src/library/utils/utils.d.ts +5 -0
- package/dist/src/library/utils.d.ts +37 -0
- 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 — 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` – 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 — `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 — 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
|
+
* – 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 – 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>;
|