march-hare 0.11.1 → 0.13.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,39 +1,64 @@
1
- import { Fetcher, PendingCall, ResourceHandle } from './types';
1
+ import { ResourceHandle } from './types';
2
2
  import { Cache } from './utils';
3
- export type { Coalesce, Fetcher, PendingCall, ResourceHandle, } from './types';
3
+ import { AppFetcher } from '../app/types';
4
+ export type { Coalesce, Fetcher, Invocation, ResourceHandle } from './types';
4
5
  /**
5
- * Reads and clears the slot populated by the most recent resource
6
- * invocation. Throws when the slot is empty — the public
7
- * `.resource(...)` shape requires a fresh `resource.cat(params)` call
8
- * as its argument.
6
+ * Evicts cache entries across every Resource constructed in the
7
+ * current process. Resources register themselves on declaration, so
8
+ * `nuke` covers both `app.Resource` and `shared.Resource`. Pass a
9
+ * `where` pattern to drop only slots whose stored params satisfy the
10
+ * pattern's keys (partial match — extra keys in the stored
11
+ * params are ignored). Pass nothing to clear every known slot.
9
12
  *
10
- * @internal
13
+ * @internal Public surface lives on `context.actions.resource.nuke(...)`.
11
14
  */
12
- export declare function consumePending(): PendingCall;
15
+ export declare function nuke(where?: object): void;
13
16
  /**
14
17
  * Defines a remote resource — declared at module scope and used
15
- * directly. Calling the returned handle with `params` returns the sync
16
- * cache value (`T | null`) and primes the slot consumed by
17
- * `context.actions.resource(...)` / `.set(...)` for fetch and write
18
- * paths.
18
+ * directly. Exported as `shared.Resource` and (via the app factory) as
19
+ * `app.Resource`. Calling the returned handle with `params` produces an
20
+ * {@link Invocation} suitable for `context.actions.resource(...)` (fetch
21
+ * path) or `context.actions.resource(...).evict(where?)` (partial-match
22
+ * invalidation). Use `.get(params)` on the handle for a synchronous
23
+ * cache read returning `T | null`. Persistence happens automatically
24
+ * when the App is declared with `App({ cache })`.
25
+ *
26
+ * Takes the **Env shape `E` as a mandatory first generic** —
27
+ * `context.env` inside the fetcher is typed as `E`. Pass a union of
28
+ * every App's Env if the resource is shared across reusable
29
+ * components. For single-app resources, prefer `app.Resource` —
30
+ * the Env is captured from `app` automatically and you only need the
31
+ * payload generic.
19
32
  *
20
33
  * The fetcher receives a single `context` argument carrying `env`,
21
34
  * `controller`, `params`, and a broadcast/multicast-only `dispatch`.
22
- * `env` is a live handle — dot reads inside the fetcher
23
- * always see the latest per-`<Boundary>` Env, even after `await`
24
- * boundaries. Every successful fetch writes through to a per-resource
25
- * in-memory cache; pair with {@link Resource.Cachable} to persist
26
- * across reloads.
35
+ * `env` is a live handle &mdash; dot reads inside the fetcher always
36
+ * see the latest per-`<Boundary>` Env, even after `await` boundaries.
37
+ *
38
+ * Cache behaviour is decided at the App level: when `App({ cache })`
39
+ * is supplied, every `app.Resource` declaration on that App writes
40
+ * through to (and seeds from) the shared cache, isolated per resource
41
+ * by a stable module-order namespace. When the App is constructed
42
+ * without a `cache`, every resource keeps its own in-memory slot.
43
+ * Standalone `shared.Resource` declarations always use an in-memory
44
+ * cache &mdash; reach for `app.Resource` when persistence is required.
27
45
  *
28
46
  * Concurrent calls fire fresh requests by default. Opt in to in-flight
29
47
  * sharing per call via `.coalesce(key)` on the thenable returned from
30
48
  * `context.actions.resource(...)`.
31
49
  *
50
+ * @template E The Env shape (or union) the fetcher's `context.env` is
51
+ * typed against.
52
+ * @template T The payload type the fetcher resolves to.
53
+ * @template P The call-time params type.
54
+ *
32
55
  * @example
33
56
  * ```ts
34
- * import { Resource } from "march-hare";
57
+ * import { shared } from "march-hare";
35
58
  *
36
- * export const user = Resource<User, { id: number }>((context) =>
59
+ * type WebEnv = { session: Session | null };
60
+ *
61
+ * export const user = shared.Resource<WebEnv, User, { id: number }>((context) =>
37
62
  * ky
38
63
  * .get(`users/${context.params.id}`, {
39
64
  * headers: context.env.session
@@ -44,36 +69,9 @@ export declare function consumePending(): PendingCall;
44
69
  * .json<User>(),
45
70
  * );
46
71
  * ```
72
+ *
73
+ * @internal The optional `cache` argument is reserved for `app.Resource`
74
+ * &mdash; consumers should use `App({ cache })` instead of passing it
75
+ * directly.
47
76
  */
48
- export declare function Resource<T, P extends object = Record<never, never>>(ƒ: Fetcher<T, P>): ResourceHandle<T, P>;
49
- export declare namespace Resource {
50
- /**
51
- * Cache-aware variant of {@link Resource}. The supplied {@link Cache}
52
- * is the **first** argument &mdash; persistence is the headline of
53
- * this form, the fetcher is the operation. Every successful fetch
54
- * writes through to the cache; first reads via the call form
55
- * auto-seed from the cache's adapter.
56
- *
57
- * @example
58
- * ```ts
59
- * import { Cache, Resource } from "march-hare";
60
- *
61
- * const cache = Cache({
62
- * get: (key) => localStorage.getItem(key),
63
- * set: (key, value) => localStorage.setItem(key, value),
64
- * remove: (key) => localStorage.removeItem(key),
65
- * clear: () => localStorage.clear(),
66
- * });
67
- *
68
- * export const cat = Resource.Cachable(cache, async (context) =>
69
- * ky
70
- * .get("https://api.thecatapi.com/v1/images/search", {
71
- * signal: context.controller.signal,
72
- * })
73
- * .json<Cat[]>()
74
- * .then((cats) => cats[0]),
75
- * );
76
- * ```
77
- */
78
- function Cachable<T, P extends object = Record<never, never>>(cache: Cache, ƒ: Fetcher<T, P>): ResourceHandle<T, P>;
79
- }
77
+ export declare function Resource<E extends object, T, P extends object = Record<never, never>>(ƒ: AppFetcher<E, T, P>, cache?: Cache): ResourceHandle<T, P>;
@@ -62,42 +62,65 @@ export type Config<T, P extends object = Record<never, never>> = {
62
62
  readonly cache?: Cache;
63
63
  };
64
64
  /**
65
- * Snapshot of the most recent resource invocation. `resource.cat(params)`
66
- * writes one of these into a module-scope slot; the next
67
- * `context.actions.resource(...)` / `.set(...)` call consumes it via
68
- * `consumePending` and then clears the slot.
65
+ * Descriptor produced by calling a Resource handle. Carries the per-call
66
+ * `params` together with the closures `context.actions.resource(...)`
67
+ * needs to run, read, or evict the slot. Pass it straight to
68
+ * `context.actions.resource(invocation)` &mdash; no module-level state
69
+ * sits between the producer and the consumer, so two synchronous calls
70
+ * to the same Resource are independent values that can be stored,
71
+ * deferred, or passed across `await` boundaries safely.
69
72
  *
70
73
  * @internal
71
74
  */
72
- export type PendingCall = {
73
- readonly run: (env: Env, controller: AbortController, params: object, dispatch: Dispatch) => Promise<unknown>;
75
+ export type Invocation<T, P extends object = Record<never, never>> = {
76
+ readonly run: (env: Env, controller: AbortController, params: object, dispatch: Dispatch) => Promise<T>;
74
77
  readonly read: (params: object) => {
75
- data: unknown;
78
+ data: T | symbol;
76
79
  at: Temporal.Instant | null;
77
80
  };
78
- readonly seed: (params: object, data: unknown, at: Temporal.Instant) => void;
79
- readonly params: object;
81
+ readonly evict: (where: object) => void;
82
+ readonly params: P;
80
83
  };
81
84
  /**
82
- * Resource handle returned by `Resource(...)` or `Resource.Cachable(...)`.
83
- * Call it with `params` to read the per-params cache slot synchronously
84
- * and prime the slot consumed by `context.actions.resource(...)` for a
85
- * follow-up fetch or `context.actions.resource.set(...)` for an
86
- * out-of-band write.
85
+ * Resource handle returned by `Resource(...)` (or its `app.Resource` /
86
+ * `shared.Resource` counterparts). Call it with `params` to produce an
87
+ * {@link Invocation} suitable for `context.actions.resource(...)`. Use
88
+ * `.get(params)` for a synchronous cache read.
89
+ *
90
+ * - `resource.cat.get({id: 5})` &mdash; sync read, returns `T | null`.
91
+ * - `context.actions.resource(resource.cat({id: 5}))` &mdash; fetch.
92
+ * - `context.actions.resource(resource.cat({id: 5})).evict()` &mdash;
93
+ * drop the `{id: 5}` slot.
94
+ * - `context.actions.resource(resource.cat()).evict({name: "Adam"})`
95
+ * &mdash; evict every cached `cat` entry whose stored params include
96
+ * `name: "Adam"`, regardless of other keys.
97
+ * - `context.actions.resource.nuke({id: 5})` &mdash; partial-match
98
+ * eviction across every resource on the App; nuke with no argument
99
+ * clears every known slot.
87
100
  *
88
101
  * ```ts
89
102
  * // Sync cache read in a model literal.
90
- * { cat: resource.cat({ id: 5 }) }
103
+ * { cat: resource.cat.get({ id: 5 }) }
91
104
  *
92
105
  * // Fetch with `.exceeds(...)` for cache-aware refresh.
93
106
  * await context.actions
94
107
  * .resource(resource.cat({ id: 5 }))
95
108
  * .exceeds({ minutes: 5 });
96
109
  *
97
- * // Write through to the per-params cache slot.
98
- * context.actions.resource.set(resource.cat({ id: 5 }), data);
110
+ * // Evict the {id: 5} slot.
111
+ * context.actions.resource(resource.cat({ id: 5 })).evict();
99
112
  * ```
100
113
  */
101
- export type ResourceHandle<T, P extends object = Record<never, never>> = [
114
+ export type ResourceHandle<T, P extends object = Record<never, never>> = ([
102
115
  keyof P
103
- ] extends [never] ? (params?: P) => T | null : (params: P) => T | null;
116
+ ] extends [never] ? (params?: P) => Invocation<T, P> : (params: P) => Invocation<T, P>) & {
117
+ readonly get: [keyof P] extends [never] ? (params?: P) => T | null : (params: P) => T | null;
118
+ };
119
+ /**
120
+ * Drops cache slots whose stored params match the supplied `where`
121
+ * pattern. Each Resource registers one of these on declaration so
122
+ * `nuke(where)` can iterate them.
123
+ *
124
+ * @internal
125
+ */
126
+ export type ResourceEvictor = (where: object) => void;
@@ -1,5 +1,6 @@
1
1
  import { Cache } from '../cache/index';
2
- import { unset } from '../utils/index';
2
+ import { unset } from '../utils/utils';
3
+ import { Fetcher, ResourceEvictor, ResourceHandle } from './types';
3
4
  export { Cache } from '../cache/index';
4
5
  /**
5
6
  * Default in-memory `Cache` used when {@link Resource} is constructed
@@ -35,3 +36,30 @@ export declare function key(params: object): string;
35
36
  export declare const config: {
36
37
  readonly unset: typeof unset;
37
38
  };
39
+ /**
40
+ * Per-Resource eviction callbacks. Each `Resource` declaration registers
41
+ * one entry on construction; the public `nuke(...)` (defined in
42
+ * {@link "./index.ts"}) iterates them to drop cache slots across every
43
+ * Resource in the process.
44
+ *
45
+ * @internal
46
+ */
47
+ export declare const evictors: Array<ResourceEvictor>;
48
+ /**
49
+ * Mints the next namespace id for an app-shared cache. Each `app.Resource`
50
+ * declaration consumes one id so the shared {@link Cache} can keep
51
+ * resource-specific slots from colliding on shared params keys.
52
+ *
53
+ * @internal
54
+ */
55
+ export declare function nextResourceId(fetcher: object): string;
56
+ /**
57
+ * Allocates the per-Resource closure shared by `app.Resource` and
58
+ * `shared.Resource`. The returned callable produces an
59
+ * {@link Invocation} on every call &mdash; pass it to
60
+ * `context.actions.resource(...)` for fetch/evict. `.get(params)` reads
61
+ * the per-params cache slot synchronously.
62
+ *
63
+ * @internal
64
+ */
65
+ export declare function build<T, P extends object>(ƒ: Fetcher<T, P>, backing: Cache, namespace: string | null): ResourceHandle<T, P>;
@@ -1,63 +1,38 @@
1
- import { AppContextHandle, AppResource } from '../app/types';
2
- import { Actions, Model, Props } from '../types/index';
3
- import * as React from "react";
1
+ import { ScopeHandle } from './types';
2
+ export type { ScopeHandle } from './types';
3
+ export { createScope } from './utils';
4
4
  /**
5
- * Handle returned by `app.Scope<MulticastActions>()`. Mirrors the {@link App}
6
- * surface (`Boundary`, `useContext`, `useEnv`, `Resource`) but typed
7
- * against a specific multicast action surface `MulticastActions` and the
8
- * enclosing App's Env shape `S`.
5
+ * Standalone counterpart to `app.Scope<MulticastActions>()`, exported
6
+ * as `shared.Scope` &mdash; opens a typed multicast scope without
7
+ * going through an `App` handle. Takes the **Env shape `E` as a
8
+ * mandatory first generic**, mirroring the other standalone exports
9
+ * (`shared.useContext`, `shared.useEnv`, `shared.Resource`). The Env
10
+ * carried by `scope.useContext()` is typed as `E`.
9
11
  *
10
- * Notably absent: a nested `Scope` method. Nesting scopes is supported
11
- * at the React-tree level &mdash; just render two `<scope.Boundary>`s
12
- * &mdash; but each scope must come from a distinct
13
- * `app.Scope<MulticastActions>()` call so that its multicast surface is
14
- * declared up-front.
12
+ * Use this in reusable feature modules that need to open their own
13
+ * multicast scope without binding to one App's `app.Scope` factory.
14
+ * For single-app code, prefer `app.Scope<MulticastActions>()` &mdash;
15
+ * the Env is captured from `app` automatically.
15
16
  *
16
- * @template S The enclosing App's Env shape.
17
- * @template MulticastActions The multicast Actions class (or union of
18
- * classes) this scope's `useContext().actions.dispatch` is allowed
19
- * to fire.
20
- */
21
- export type Scope<S extends object, MulticastActions> = {
22
- /**
23
- * Boundary component. Wrap a subtree to open a fresh multicast scope
24
- * &mdash; every `Distribution.Multicast` action dispatched inside this
25
- * subtree routes through this boundary's emitter, and every handler
26
- * subscribed via `scope.useContext().useActions(...)` on that subtree
27
- * receives the event.
28
- *
29
- * Each render of `<scope.Boundary>` opens a distinct scope instance;
30
- * unmounting tears the emitter down.
31
- */
32
- readonly Boundary: React.FC<{
33
- children: React.ReactNode;
34
- }>;
35
- /**
36
- * Hook returning a stable `Context` handle. Identical to
37
- * `app.useContext` except `actions.dispatch` accepts the multicast
38
- * surface `MulticastActions` in addition to the local `AC` &mdash; mirroring
39
- * the way `Actions.Broadcast = BroadcastActions` already widens the
40
- * dispatch surface for broadcasts.
41
- */
42
- readonly useContext: <LocalModel extends Model | void = void, AC extends Actions | void = void, D extends Props = Props>() => AppContextHandle<LocalModel, MulticastActions extends Actions ? AC extends Actions ? AC & MulticastActions : MulticastActions : AC, D, S>;
43
- /**
44
- * Read-only Proxy over the enclosing App's Env. Identical to
45
- * `app.useEnv` &mdash; the Scope does not introduce its own Env;
46
- * scopes are about multicast routing, not ambient state.
47
- */
48
- readonly useEnv: () => Readonly<S>;
49
- /**
50
- * Resource factory bound to the enclosing App's Env. Identical to
51
- * `app.Resource`; provided on the scope handle for convenience so a
52
- * scoped feature can keep all its primitives in one place.
53
- */
54
- readonly Resource: AppResource<S>;
55
- };
56
- /**
57
- * Internal constructor for a {@link Scope} handle. Called from inside
58
- * `App<S>()` so the enclosing Env shape `S` is captured at the type
59
- * level.
17
+ * @template E The Env shape (or union) the scope's `useContext` types
18
+ * `context.env` against.
19
+ * @template A The multicast Actions class (or union of classes) the
20
+ * scope's dispatch surface is widened to include.
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * import { Action, Distribution, shared } from "march-hare";
25
+ *
26
+ * class MulticastActions {
27
+ * static Mood = Action<"happy" | "sad">(
28
+ * "Mood",
29
+ * Distribution.Multicast,
30
+ * );
31
+ * }
32
+ *
33
+ * type MoodEnv = { tracker: string };
60
34
  *
61
- * @internal
35
+ * export const scope = shared.Scope<MoodEnv, typeof MulticastActions>();
36
+ * ```
62
37
  */
63
- export declare function createScope<S extends object, MulticastActions>(): Scope<S, MulticastActions>;
38
+ export declare function Scope<E extends object, A>(): ScopeHandle<E, A>;
@@ -5,7 +5,7 @@ import type * as React from "react";
5
5
  * Handle returned by `app.Scope<MulticastActions>()`. Mirrors the
6
6
  * `App` surface (`Boundary`, `useContext`, `useEnv`, `Resource`) but
7
7
  * typed against a specific multicast action surface `MulticastActions`
8
- * and the enclosing App's Env shape `S`.
8
+ * and the enclosing App's Env shape `E`.
9
9
  *
10
10
  * Notably absent: a nested `Scope` method. Nesting scopes is supported
11
11
  * at the React-tree level &mdash; just render two `<scope.Boundary>`s
@@ -13,12 +13,12 @@ import type * as React from "react";
13
13
  * `app.Scope<MulticastActions>()` call so that its multicast surface is
14
14
  * declared up-front.
15
15
  *
16
- * @template S The enclosing App's Env shape.
16
+ * @template E The enclosing App's Env shape.
17
17
  * @template MulticastActions The multicast Actions class (or union of
18
18
  * classes) this scope's `useContext().actions.dispatch` is allowed
19
19
  * to fire.
20
20
  */
21
- export type Scope<S extends object, MulticastActions> = {
21
+ export type ScopeHandle<E extends object, MulticastActions> = {
22
22
  /**
23
23
  * Boundary component. Wrap a subtree to open a fresh multicast scope
24
24
  * &mdash; every `Distribution.Multicast` action dispatched inside this
@@ -39,17 +39,17 @@ export type Scope<S extends object, MulticastActions> = {
39
39
  * mirroring the way `Actions.Broadcast = BroadcastActions` already
40
40
  * widens the dispatch surface for broadcasts.
41
41
  */
42
- readonly useContext: <LocalModel extends Model | void = void, AC extends Actions | void = void, D extends Props = Props>() => AppContextHandle<LocalModel, MulticastActions extends Actions ? AC extends Actions ? AC & MulticastActions : MulticastActions : AC, D, S>;
42
+ readonly useContext: <LocalModel extends Model | void = void, AC extends Actions | void = void, D extends Props = Props>() => AppContextHandle<LocalModel, MulticastActions extends Actions ? AC extends Actions ? AC & MulticastActions : MulticastActions : AC, D, E>;
43
43
  /**
44
44
  * Read-only Proxy over the enclosing App's Env. Identical to
45
45
  * `app.useEnv` &mdash; the Scope does not introduce its own Env;
46
46
  * scopes are about multicast routing, not ambient state.
47
47
  */
48
- readonly useEnv: () => Readonly<S>;
48
+ readonly useEnv: () => Readonly<E>;
49
49
  /**
50
50
  * Resource factory bound to the enclosing App's Env. Identical to
51
51
  * `app.Resource`; provided on the scope handle for convenience so a
52
52
  * scoped feature can keep all its primitives in one place.
53
53
  */
54
- readonly Resource: AppResource<S>;
54
+ readonly Resource: AppResource<E>;
55
55
  };
@@ -0,0 +1,12 @@
1
+ import { Cache } from '../cache/index';
2
+ import { ScopeHandle } from './types';
3
+ /**
4
+ * Internal constructor for a {@link ScopeHandle}. Called from inside
5
+ * `App<E>()` so the enclosing Env shape `E` is captured at the type
6
+ * level. The optional `cache` is the same value `App({ cache })` was
7
+ * constructed with &mdash; resources declared via `scope.Resource`
8
+ * share that cache.
9
+ *
10
+ * @internal
11
+ */
12
+ export declare function createScope<E extends object, MulticastActions>(cache?: Cache): ScopeHandle<E, MulticastActions>;
@@ -0,0 +1,15 @@
1
+ import { AppFetcher } from '../app/types';
2
+ import { ResourceHandle } from '../resource/types';
3
+ export { useContext, useEnv } from '../app/index';
4
+ export { Scope } from '../scope/index';
5
+ /**
6
+ * Standalone counterpart to `app.Resource`, exported as
7
+ * `shared.Resource`. Takes the **Env shape `E` as a mandatory first
8
+ * generic** so the fetcher's `context.env` is typed even when the
9
+ * resource isn't bound to a single App.
10
+ *
11
+ * Always uses an isolated in-memory cache &mdash; persistent caching
12
+ * is an App-level concern wired through `App({ cache })`, so reach for
13
+ * `app.Resource` when a resource needs to survive reloads.
14
+ */
15
+ export declare function Resource<E extends object, T, P extends object = Record<never, never>>(fetcher: AppFetcher<E, T, P>): ResourceHandle<T, P>;