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.
- package/README.md +148 -59
- package/dist/march-hare.js +6 -6
- package/dist/march-hare.umd.cjs +1 -1
- package/dist/src/library/action/index.d.ts +17 -13
- package/dist/src/library/boundary/components/store/index.d.ts +41 -0
- package/dist/src/library/boundary/components/store/types.d.ts +11 -0
- package/dist/src/library/boundary/components/store/utils.d.ts +64 -0
- package/dist/src/library/boundary/components/tasks/types.d.ts +3 -3
- package/dist/src/library/boundary/index.d.ts +8 -7
- package/dist/src/library/boundary/types.d.ts +18 -0
- package/dist/src/library/cache/index.d.ts +44 -0
- package/dist/src/library/cache/types.d.ts +54 -0
- package/dist/src/library/index.d.ts +7 -4
- package/dist/src/library/resource/index.d.ts +82 -79
- package/dist/src/library/resource/types.d.ts +27 -0
- package/dist/src/library/resource/utils.d.ts +37 -0
- package/dist/src/library/types/index.d.ts +160 -44
- package/dist/src/library/utils/index.d.ts +32 -22
- package/dist/src/library/utils/types.d.ts +18 -0
- package/dist/src/library/utils/utils.d.ts +33 -2
- package/package.json +3 -3
- package/dist/src/library/boundary/components/mode/index.d.ts +0 -15
- package/dist/src/library/boundary/components/mode/types.d.ts +0 -7
- package/dist/src/library/boundary/components/mode/utils.d.ts +0 -55
|
@@ -6,24 +6,28 @@ export { getActionSymbol, getLifecycleType, isBroadcastAction, isMulticastAction
|
|
|
6
6
|
type ActionFactory = {
|
|
7
7
|
/**
|
|
8
8
|
* Creates a new unicast action with the given name.
|
|
9
|
+
*
|
|
10
|
+
* `K` is the literal type of the action name and is captured as a phantom
|
|
11
|
+
* brand so `Action("A")` and `Action("B")` produce structurally-distinct
|
|
12
|
+
* types. **Note:** when the caller supplies any explicit generic
|
|
13
|
+
* (`Action<P>("Name")`), TypeScript fills `K` from its default and the
|
|
14
|
+
* literal is lost. The Name brand still helps for `Action("Name")` calls
|
|
15
|
+
* (e.g. lifecycle / no-payload actions) which is the most common source of
|
|
16
|
+
* foreign-class collisions.
|
|
17
|
+
*
|
|
9
18
|
* @template P The payload type for the action.
|
|
10
|
-
* @template C The channel type for channeled dispatches
|
|
11
|
-
* @
|
|
12
|
-
*
|
|
19
|
+
* @template C The channel type for channeled dispatches.
|
|
20
|
+
* @template K The literal type of the action name (inferred when no other
|
|
21
|
+
* generics are supplied; defaults to `string` otherwise).
|
|
13
22
|
*/
|
|
14
|
-
<P = never, C extends Filter = never>(name:
|
|
23
|
+
<P = never, C extends Filter = never, K extends string = string>(name: K): HandlerPayload<P, C, K>;
|
|
15
24
|
/**
|
|
16
25
|
* Creates a new action with the specified distribution mode.
|
|
17
|
-
* @template P The payload type for the action.
|
|
18
|
-
* @template C The channel type for channeled dispatches (defaults to never).
|
|
19
|
-
* @param name The action name, used for debugging purposes.
|
|
20
|
-
* @param distribution The distribution mode (Unicast, Broadcast, or Multicast).
|
|
21
|
-
* @returns A typed action object (BroadcastPayload if Broadcast, MulticastPayload if Multicast).
|
|
22
26
|
*/
|
|
23
|
-
<P = never, C extends Filter = never>(name:
|
|
24
|
-
<P = never, C extends Filter = never>(name:
|
|
25
|
-
<P = never, C extends Filter = never>(name:
|
|
26
|
-
<P = never, C extends Filter = never>(name:
|
|
27
|
+
<P = never, C extends Filter = never, K extends string = string>(name: K, distribution: Distribution.Broadcast): BroadcastPayload<P, C, K>;
|
|
28
|
+
<P = never, C extends Filter = never, K extends string = string>(name: K, distribution: Distribution.Multicast): MulticastPayload<P, C, K>;
|
|
29
|
+
<P = never, C extends Filter = never, K extends string = string>(name: K, distribution: Distribution.Unicast): HandlerPayload<P, C, K>;
|
|
30
|
+
<P = never, C extends Filter = never, K extends string = string>(name: K, distribution: Distribution): HandlerPayload<P, C, K> | BroadcastPayload<P, C, K> | MulticastPayload<P, C, K>;
|
|
27
31
|
};
|
|
28
32
|
/**
|
|
29
33
|
* Creates a new action with a given payload type, optional channel type, and optional distribution mode.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Props } from './types.ts';
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
export { useStore } from './utils.ts';
|
|
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()` — 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;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { Store } from './index.tsx';
|
|
3
|
+
export type { Store } from './index.tsx';
|
|
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
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
import { Store } from './index.tsx';
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
/**
|
|
5
|
+
* React context exposing the per-Boundary Store ref. The ref itself is
|
|
6
|
+
* stable across renders — 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 — 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
|
+
* — 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 — 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>;
|
|
@@ -27,9 +27,9 @@ export type ActionId = symbol | string;
|
|
|
27
27
|
* ```
|
|
28
28
|
*/
|
|
29
29
|
export type Task<P = unknown> = {
|
|
30
|
-
controller: AbortController;
|
|
31
|
-
action: ActionId;
|
|
32
|
-
payload: P;
|
|
30
|
+
readonly controller: AbortController;
|
|
31
|
+
readonly action: ActionId;
|
|
32
|
+
readonly payload: P;
|
|
33
33
|
};
|
|
34
34
|
/**
|
|
35
35
|
* A set of running tasks ordered by creation time (oldest first).
|
|
@@ -2,19 +2,20 @@ import { Props } from './types.ts';
|
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
/**
|
|
4
4
|
* Creates a unified context boundary for all March Hare features.
|
|
5
|
-
* Wraps children with Broadcaster,
|
|
5
|
+
* Wraps children with Broadcaster, Store, and Tasks providers.
|
|
6
6
|
*
|
|
7
|
-
* Use this at the root of your application or to create isolated context
|
|
8
|
-
* for libraries that need their own March Hare context.
|
|
7
|
+
* Use this at the root of your application or to create isolated context
|
|
8
|
+
* boundaries for libraries that need their own March Hare context.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Pass the `store` prop with the initial Store value (session, locale,
|
|
11
|
+
* feature flags, etc.) — the shape is determined by module
|
|
12
|
+
* augmentation on the library's `Store` interface.
|
|
12
13
|
*
|
|
13
14
|
* @example
|
|
14
15
|
* ```tsx
|
|
15
|
-
* <Boundary>
|
|
16
|
+
* <Boundary store={{ session: null, locale: "en-GB" }}>
|
|
16
17
|
* <App />
|
|
17
18
|
* </Boundary>
|
|
18
19
|
* ```
|
|
19
20
|
*/
|
|
20
|
-
export declare function Boundary({ children }: Props): React.ReactNode;
|
|
21
|
+
export declare function Boundary({ store, children }: Props): React.ReactNode;
|
|
@@ -1,4 +1,22 @@
|
|
|
1
|
+
import { Store } from './components/store/types.ts';
|
|
1
2
|
import type * as React from "react";
|
|
2
3
|
export type Props = {
|
|
4
|
+
/**
|
|
5
|
+
* Initial value of the per-Boundary {@link Store}. The shape is
|
|
6
|
+
* derived from module augmentation — declare the keys your
|
|
7
|
+
* application needs once via:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* declare module "march-hare" {
|
|
11
|
+
* interface Store {
|
|
12
|
+
* session: Session | null;
|
|
13
|
+
* locale: string;
|
|
14
|
+
* }
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Optional only when the augmented Store has no required keys.
|
|
19
|
+
*/
|
|
20
|
+
store?: Store;
|
|
3
21
|
children: React.ReactNode;
|
|
4
22
|
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Adapter, Stored } from './types.ts';
|
|
2
|
+
export type { Adapter, Encoded } from './types.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Persistence-aware cache for a single {@link Resource}. Wraps a
|
|
5
|
+
* synchronous {@link Adapter} (localStorage, MMKV, chrome.storage with a
|
|
6
|
+
* sync facade, etc.) and traffics in {@link Stored} envelopes —
|
|
7
|
+
* storage entries serialise as {@link Encoded}`<T>` so the
|
|
8
|
+
* `Temporal.Instant` timestamp survives the string round-trip and
|
|
9
|
+
* `.exceeds({...})` can short-circuit on the persisted timestamp after
|
|
10
|
+
* a reload.
|
|
11
|
+
*
|
|
12
|
+
* Call with no arguments for an in-memory cache scoped to this
|
|
13
|
+
* instance — useful for tests, ephemeral state, or when you want a
|
|
14
|
+
* first-class cache object to share between Resources without
|
|
15
|
+
* persistence. Pass an {@link Adapter} to back the cache with a
|
|
16
|
+
* persistent store.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* // In-memory, scoped to this instance.
|
|
21
|
+
* const cache = Cache();
|
|
22
|
+
*
|
|
23
|
+
* // Persisted via localStorage.
|
|
24
|
+
* const cache = Cache({
|
|
25
|
+
* get: (key) => localStorage.getItem(key),
|
|
26
|
+
* set: (key, value) => localStorage.setItem(key, value),
|
|
27
|
+
* remove: (key) => localStorage.removeItem(key),
|
|
28
|
+
* clear: () => localStorage.clear(),
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* // Wired into a Resource — successful runs write through automatically.
|
|
32
|
+
* export const cat = Resource(
|
|
33
|
+
* async ({ controller }) => fetchCat(controller.signal),
|
|
34
|
+
* cache,
|
|
35
|
+
* );
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export type Cache = {
|
|
39
|
+
get<T>(key: string): Stored<T>;
|
|
40
|
+
set<T>(key: string, value: Stored<T>): boolean;
|
|
41
|
+
remove(key: string): void;
|
|
42
|
+
clear(): void;
|
|
43
|
+
};
|
|
44
|
+
export declare function Cache(adapter?: Adapter): Cache;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export type { Stored } from '../utils/types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* On-disk JSON shape of a `Stored` envelope. The Cache wrapper
|
|
4
|
+
* encodes a populated Stored as `{ data, at: at.toString() }` so the
|
|
5
|
+
* `Temporal.Instant` survives the string round-trip, and decodes via
|
|
6
|
+
* `Temporal.Instant.from(...)` on read. Adapters never see this shape
|
|
7
|
+
* directly — they shuttle the already-stringified JSON.
|
|
8
|
+
*
|
|
9
|
+
* @template T The payload type carried by the matching {@link Stored}.
|
|
10
|
+
*/
|
|
11
|
+
export type Encoded<T> = {
|
|
12
|
+
readonly data: T;
|
|
13
|
+
readonly at: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Adapter contract for synchronous key/value storage. Implement once per
|
|
17
|
+
* backend (localStorage, MMKV on React Native, chrome.storage with a sync
|
|
18
|
+
* facade, etc.) and pass to {@link Cache}. The adapter shuttles raw
|
|
19
|
+
* strings; JSON encoding and `Temporal.Instant` round-tripping happen
|
|
20
|
+
* inside the Cache wrapper, so adapters stay trivial.
|
|
21
|
+
*/
|
|
22
|
+
export type Adapter = {
|
|
23
|
+
/**
|
|
24
|
+
* Return the raw string stored under `key`, or `null` when no entry
|
|
25
|
+
* exists. The Cache wrapper handles JSON parsing and `Temporal.Instant`
|
|
26
|
+
* round-tripping, so this stays a plain string getter. Treat any
|
|
27
|
+
* read-time error (decryption, IPC, etc.) as "not found" and return
|
|
28
|
+
* `null` — the Cache falls through to its next fallback rather
|
|
29
|
+
* than crashing the render.
|
|
30
|
+
*/
|
|
31
|
+
readonly get: (key: string) => string | null;
|
|
32
|
+
/**
|
|
33
|
+
* Persist the raw string `value` under `key`. The Cache guarantees
|
|
34
|
+
* `value` is a JSON-encoded `{ data, at }` envelope produced by a
|
|
35
|
+
* resolved snapshot — never a placeholder. Throwing is fine on
|
|
36
|
+
* quota, private mode, sandboxed iframes, etc.; the Cache catches and
|
|
37
|
+
* swallows so a write failure can't poison an already-resolved fetch.
|
|
38
|
+
*/
|
|
39
|
+
readonly set: (key: string, value: string) => void;
|
|
40
|
+
/**
|
|
41
|
+
* Drop the entry at `key`. Idempotent — calling `remove` for a
|
|
42
|
+
* key that isn't present must not throw.
|
|
43
|
+
*/
|
|
44
|
+
readonly remove: (key: string) => void;
|
|
45
|
+
/**
|
|
46
|
+
* Wipe every entry this adapter can see. On a shared backend such as
|
|
47
|
+
* `localStorage` this means the whole origin — third-party SDK
|
|
48
|
+
* state, dismissed banners, route hints, etc. all go with it. Adapter
|
|
49
|
+
* authors should either delegate to the backend's native clear
|
|
50
|
+
* (accepting that scope) or namespace by key prefix and remove only
|
|
51
|
+
* their own.
|
|
52
|
+
*/
|
|
53
|
+
readonly clear: () => void;
|
|
54
|
+
};
|
|
@@ -5,12 +5,15 @@ export { Operation, Op, State } from 'immertation';
|
|
|
5
5
|
export { annotate } from './annotate/index.ts';
|
|
6
6
|
export { Boundary } from './boundary/index.tsx';
|
|
7
7
|
export { withScope } from './boundary/components/scope/index.tsx';
|
|
8
|
-
export {
|
|
9
|
-
export type {
|
|
8
|
+
export { useStore } from './boundary/components/store/index.tsx';
|
|
9
|
+
export type { Store } from './boundary/components/store/index.tsx';
|
|
10
10
|
export { useActions, With } from './hooks/index.ts';
|
|
11
11
|
export { Resource } from './resource/index.ts';
|
|
12
|
-
export type {
|
|
12
|
+
export type { Fetcher } from './resource/index.ts';
|
|
13
|
+
export { Cache } from './cache/index.ts';
|
|
14
|
+
export type { Adapter, Encoded } from './cache/index.ts';
|
|
13
15
|
export * as utils from './utils/index.ts';
|
|
16
|
+
export type { Stored, Unset } from './utils/index.ts';
|
|
14
17
|
export type { Box } from 'immertation';
|
|
15
18
|
export type { Fault } from './error/index.ts';
|
|
16
|
-
export type { Pk, Task, Tasks, Handlers } from './types/index.ts';
|
|
19
|
+
export type { Pk, Task, Tasks, Handlers, Handler, LeafActions, Dispatchable, Subscribable, } from './types/index.ts';
|
|
@@ -1,99 +1,102 @@
|
|
|
1
|
+
import { Fetcher } from './types.ts';
|
|
2
|
+
import { Cache } from './utils.ts';
|
|
3
|
+
import { Store } from '../boundary/components/store/index.tsx';
|
|
4
|
+
export type { Fetcher } from './types.ts';
|
|
1
5
|
/**
|
|
2
|
-
*
|
|
6
|
+
* Snapshot of the most recent resource invocation. `cat(params)` writes
|
|
7
|
+
* one of these into a module-scope slot; the next
|
|
8
|
+
* `context.actions.resource(...)` / `.set(...)` call consumes it via
|
|
9
|
+
* {@link consumePending}.
|
|
3
10
|
*
|
|
4
|
-
*
|
|
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
|
-
* ```
|
|
11
|
+
* @internal
|
|
16
12
|
*/
|
|
17
|
-
export type
|
|
18
|
-
readonly
|
|
13
|
+
export type PendingCall = {
|
|
14
|
+
readonly run: (store: Store, controller: AbortController, params: object) => Promise<unknown>;
|
|
15
|
+
readonly read: (params: object) => {
|
|
16
|
+
data: unknown;
|
|
17
|
+
at: Temporal.Instant | null;
|
|
18
|
+
};
|
|
19
|
+
readonly seed: (params: object, data: unknown, at: Temporal.Instant) => void;
|
|
20
|
+
readonly params: object;
|
|
19
21
|
};
|
|
20
22
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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.
|
|
23
|
+
* Reads and clears the slot populated by the most recent resource
|
|
24
|
+
* invocation. Throws when the slot is empty — the public
|
|
25
|
+
* `.resource(...)` shape requires a fresh `cat(params)` call as its
|
|
26
|
+
* argument.
|
|
32
27
|
*
|
|
33
|
-
*
|
|
34
|
-
* `P` is empty — `run()` instead of `run({})`.
|
|
28
|
+
* @internal
|
|
35
29
|
*/
|
|
36
|
-
export
|
|
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
|
-
};
|
|
30
|
+
export declare function consumePending(): PendingCall;
|
|
51
31
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* `
|
|
32
|
+
* Resource handle returned by `Resource(...)`. Call it with `params` to
|
|
33
|
+
* read the per-params cache slot synchronously and prime the slot
|
|
34
|
+
* consumed by `context.actions.resource(...)` for a follow-up fetch or
|
|
35
|
+
* `context.actions.resource.set(...)` for an out-of-band write.
|
|
36
|
+
*
|
|
37
|
+
* ```ts
|
|
38
|
+
* // Sync cache read in a model literal.
|
|
39
|
+
* { cat: cat({ id: 5 }) }
|
|
40
|
+
*
|
|
41
|
+
* // Fetch with `.exceeds(...)` for cache-aware refresh.
|
|
42
|
+
* await context.actions.resource(cat({ id: 5 })).exceeds({ minutes: 5 });
|
|
43
|
+
*
|
|
44
|
+
* // Write through to the per-params cache slot.
|
|
45
|
+
* context.actions.resource.set(cat({ id: 5 }), data);
|
|
46
|
+
* ```
|
|
55
47
|
*/
|
|
56
|
-
export type
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
};
|
|
48
|
+
export type Resource<T, P extends object = Record<never, never>> = [
|
|
49
|
+
keyof P
|
|
50
|
+
] extends [never] ? (params?: P) => T | null : (params: P) => T | null;
|
|
65
51
|
/**
|
|
66
|
-
* Defines a remote resource —
|
|
67
|
-
*
|
|
68
|
-
*
|
|
52
|
+
* Defines a remote resource — declared at module scope and used
|
|
53
|
+
* directly. Calling the returned handle with `params` returns the sync
|
|
54
|
+
* cache value (`T | null`) and primes the slot consumed by
|
|
55
|
+
* `context.actions.resource(...)` / `.set(...)` for fetch and write
|
|
56
|
+
* paths.
|
|
69
57
|
*
|
|
70
|
-
* The fetcher
|
|
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(...)`.
|
|
58
|
+
* The fetcher receives a single args object `{ store, controller, params }`:
|
|
75
59
|
*
|
|
76
|
-
* `
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
60
|
+
* - `store` – snapshot of the per-`<Boundary>` Store (session,
|
|
61
|
+
* locale, feature flags, etc.). Reads only; writes go through
|
|
62
|
+
* `context.actions.produce(({ store }) => ...)` in handlers.
|
|
63
|
+
* - `controller` – the `AbortController` auto-threaded from the
|
|
64
|
+
* calling handler's `context.task.controller`. Pass `controller.signal`
|
|
65
|
+
* to `fetch`/`ky`, or call `controller.abort()` to fail fast.
|
|
66
|
+
* - `params` – the call-site params object (defaults to `{}`).
|
|
81
67
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
68
|
+
* Resources do **not** carry any callbacks – side-effects
|
|
69
|
+
* (broadcasting, logging, model updates) belong in the `useAction`
|
|
70
|
+
* handler that awaited `context.actions.resource(...)`.
|
|
71
|
+
*
|
|
72
|
+
* Every successful fetch writes through to the per-fetcher {@link Cache}
|
|
73
|
+
* (in-memory by default, persistent when an adapter is supplied via the
|
|
74
|
+
* second argument).
|
|
85
75
|
*
|
|
86
76
|
* @example
|
|
87
77
|
* ```ts
|
|
88
|
-
* import { Resource } from "march-hare";
|
|
78
|
+
* import { Resource, Cache } from "march-hare";
|
|
89
79
|
*
|
|
90
|
-
* export const
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
80
|
+
* export const user = Resource<User, { id: number }>(
|
|
81
|
+
* ({ store, controller, params }) =>
|
|
82
|
+
* ky.get(`users/${params.id}`, {
|
|
83
|
+
* headers: store.session
|
|
84
|
+
* ? { Authorization: `Bearer ${store.session.accessToken}` }
|
|
85
|
+
* : {},
|
|
86
|
+
* signal: controller.signal,
|
|
87
|
+
* }).json<User>(),
|
|
96
88
|
* );
|
|
89
|
+
*
|
|
90
|
+
* // Sync cache read at module scope or in the model literal.
|
|
91
|
+
* const cached: User | null = user({ id: 5 });
|
|
92
|
+
*
|
|
93
|
+
* // Fetch inside a handler — controller and Store auto-threaded.
|
|
94
|
+
* actions.useAction(Actions.Mount, async (context) => {
|
|
95
|
+
* const data = await context.actions
|
|
96
|
+
* .resource(user({ id: 5 }))
|
|
97
|
+
* .exceeds({ minutes: 5 });
|
|
98
|
+
* context.actions.produce(({ model }) => void (model.user = data));
|
|
99
|
+
* });
|
|
97
100
|
* ```
|
|
98
101
|
*/
|
|
99
|
-
export declare function Resource<T, P extends object = Record<never, never>>(
|
|
102
|
+
export declare function Resource<T, P extends object = Record<never, never>>(fetcher: Fetcher<T, P>, cache?: Cache): Resource<T, P>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Store } from '../boundary/components/store/index.tsx';
|
|
2
|
+
/**
|
|
3
|
+
* Args object passed to every {@link Fetcher}. The fetcher destructures
|
|
4
|
+
* whatever it needs; unused fields can be omitted.
|
|
5
|
+
*
|
|
6
|
+
* - `store` — snapshot of the per-`<Boundary>` Store at the
|
|
7
|
+
* moment the fetcher is invoked.
|
|
8
|
+
* - `controller` — the `AbortController` auto-threaded from the
|
|
9
|
+
* calling handler's `context.task.controller`. Pass
|
|
10
|
+
* `controller.signal` to `fetch`/`ky`/`EventSource`, or call
|
|
11
|
+
* `controller.abort()` to fail fast.
|
|
12
|
+
* - `params` — the call-site params object. Defaults to `{}`.
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
export type Args<P extends object = Record<never, never>> = {
|
|
17
|
+
readonly store: Store;
|
|
18
|
+
readonly controller: AbortController;
|
|
19
|
+
readonly params: P;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Fetcher signature accepted by `Resource`. Receives the args object
|
|
23
|
+
* `{ store, controller, params }`. Side-effects (dispatching broadcasts,
|
|
24
|
+
* analytics, etc.) belong in the calling `useAction` handler, not
|
|
25
|
+
* inside the fetcher.
|
|
26
|
+
*/
|
|
27
|
+
export type Fetcher<T, P extends object = Record<never, never>> = (args: Args<P>) => Promise<T>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Cache } from '../cache/index.ts';
|
|
2
|
+
import { unset } from '../utils/index.ts';
|
|
3
|
+
export { Cache } from '../cache/index.ts';
|
|
4
|
+
/**
|
|
5
|
+
* Default in-memory `Cache` used when {@link Resource} is constructed
|
|
6
|
+
* without an explicit one. Each fetcher gets its own slot via the
|
|
7
|
+
* outer `WeakMap` so unrelated Resources don't share a string-key
|
|
8
|
+
* namespace.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare const defaults: WeakMap<object, Cache>;
|
|
13
|
+
/**
|
|
14
|
+
* Returns the {@link Cache} bound to `fetcher`, allocating a fresh
|
|
15
|
+
* in-memory Cache on first access.
|
|
16
|
+
*
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export declare function defaultCache(fetcher: object): Cache;
|
|
20
|
+
/**
|
|
21
|
+
* Stable string key derived from the call-site `params`. Two calls with
|
|
22
|
+
* the same logical params (same key order, same primitive values) hit
|
|
23
|
+
* the same slot. JSON.stringify is sufficient for the Chizu params
|
|
24
|
+
* convention (primitive-leaf objects); callers who need order-stable
|
|
25
|
+
* keying should normalise the object before passing it in.
|
|
26
|
+
*
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
export declare function key(params: object): string;
|
|
30
|
+
/**
|
|
31
|
+
* Re-export of the shared `unset` sentinel from {@link "../utils/index.ts"}.
|
|
32
|
+
*
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
export declare const config: {
|
|
36
|
+
readonly unset: typeof unset;
|
|
37
|
+
};
|