march-hare 0.7.5 → 0.9.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 +496 -204
- package/dist/{hooks → actions}/index.d.ts +1 -2
- package/dist/{hooks → actions}/utils.d.ts +0 -39
- package/dist/app/index.d.ts +112 -0
- package/dist/app/types.d.ts +49 -0
- package/dist/boundary/components/broadcast/utils.d.ts +1 -1
- package/dist/boundary/components/env/index.d.ts +26 -0
- package/dist/boundary/components/env/types.d.ts +11 -0
- package/dist/boundary/components/env/utils.d.ts +36 -0
- package/dist/boundary/components/scope/index.d.ts +1 -39
- package/dist/boundary/components/scope/types.d.ts +17 -13
- package/dist/boundary/components/scope/utils.d.ts +12 -8
- package/dist/boundary/components/sharing/index.d.ts +43 -0
- package/dist/boundary/index.d.ts +10 -10
- package/dist/boundary/types.d.ts +6 -16
- package/dist/cache/index.d.ts +4 -4
- package/dist/coalesce/index.d.ts +57 -0
- package/dist/context/index.d.ts +39 -0
- package/dist/context/types.d.ts +14 -0
- package/dist/error/index.d.ts +1 -1
- package/dist/error/types.d.ts +8 -19
- package/dist/index.d.ts +8 -12
- package/dist/march-hare.js +7 -5
- package/dist/march-hare.umd.cjs +1 -1
- package/dist/resource/index.d.ts +52 -78
- package/dist/resource/types.d.ts +83 -10
- package/dist/scope/index.d.ts +63 -0
- package/dist/scope/types.d.ts +55 -0
- package/dist/types/index.d.ts +116 -229
- package/dist/utils/index.d.ts +6 -5
- package/dist/with/index.d.ts +40 -0
- package/package.json +1 -1
- package/dist/boundary/components/store/index.d.ts +0 -41
- package/dist/boundary/components/store/types.d.ts +0 -11
- package/dist/boundary/components/store/utils.d.ts +0 -64
- /package/dist/{hooks → actions}/types.d.ts +0 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Data } from './types';
|
|
2
2
|
import { Model, Props, Actions, UseActions } from '../types/index';
|
|
3
|
-
export { With } from './utils';
|
|
4
3
|
/**
|
|
5
4
|
* A hook for managing state with actions.
|
|
6
5
|
*
|
|
@@ -43,5 +42,5 @@ export { With } from './utils';
|
|
|
43
42
|
* >(initialModel, () => ({ query: props.query }));
|
|
44
43
|
* ```
|
|
45
44
|
*/
|
|
46
|
-
export declare function useActions<
|
|
45
|
+
export declare function useActions<M extends void = void, A extends Actions | void = void, D extends Props = Props>(getData?: Data<D>): UseActions<M, A, D>;
|
|
47
46
|
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>;
|
|
@@ -63,45 +63,6 @@ export declare function useLifecycles({ unicast, broadcast, dispatchers, scope,
|
|
|
63
63
|
* @returns A memoized data proxy of the object.
|
|
64
64
|
*/
|
|
65
65
|
export declare function useData<P extends Props>(props: P): P;
|
|
66
|
-
/**
|
|
67
|
-
* Handler factories that wire an action directly to a model field.
|
|
68
|
-
*
|
|
69
|
-
* - {@link With.Update} assigns the dispatched payload to `model[key]`.
|
|
70
|
-
* - {@link With.Invert} flips a boolean field on `model[key]`.
|
|
71
|
-
*
|
|
72
|
-
* Both are typed so the call site fails to compile when `key` is missing or
|
|
73
|
-
* has an incompatible type.
|
|
74
|
-
*
|
|
75
|
-
* @example
|
|
76
|
-
* ```ts
|
|
77
|
-
* import { With } from "march-hare";
|
|
78
|
-
*
|
|
79
|
-
* type Model = { name: string; sidebar: boolean };
|
|
80
|
-
*
|
|
81
|
-
* class Actions {
|
|
82
|
-
* static SetName = Action<string>("SetName");
|
|
83
|
-
* static ToggleSidebar = Action("ToggleSidebar");
|
|
84
|
-
* }
|
|
85
|
-
*
|
|
86
|
-
* actions.useAction(Actions.SetName, With.Update("name"));
|
|
87
|
-
* actions.useAction(Actions.ToggleSidebar, With.Invert("sidebar"));
|
|
88
|
-
* ```
|
|
89
|
-
*/
|
|
90
|
-
export declare const With: {
|
|
91
|
-
/**
|
|
92
|
-
* Returns a handler that assigns the action payload to `model[key]`.
|
|
93
|
-
*
|
|
94
|
-
* Type-checks at the call site: the payload type must be assignable to
|
|
95
|
-
* the model property's type, and the key must exist on the model.
|
|
96
|
-
*/
|
|
97
|
-
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;
|
|
98
|
-
/**
|
|
99
|
-
* Returns a handler that inverts a boolean field on the model.
|
|
100
|
-
*
|
|
101
|
-
* Type-checks at the call site: `model[key]` must be a boolean.
|
|
102
|
-
*/
|
|
103
|
-
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;
|
|
104
|
-
};
|
|
105
66
|
/**
|
|
106
67
|
* Scans a handler registry for a lifecycle action of the given type.
|
|
107
68
|
*
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Env } from '../boundary/components/env/index';
|
|
2
|
+
import { Actions, Model, Props } from '../types/index';
|
|
3
|
+
import { Scope } from '../scope/index';
|
|
4
|
+
import { AppContextHandle, AppResource } from './types';
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
export type { AppArgs, AppContextHandle, AppFetcher, AppResource, } from './types';
|
|
7
|
+
/**
|
|
8
|
+
* Returned from {@link App}. Bundles the Boundary, hooks, and Resource
|
|
9
|
+
* factory bound to a single typed Env shape `S`.
|
|
10
|
+
*/
|
|
11
|
+
export type App<S extends object> = {
|
|
12
|
+
/**
|
|
13
|
+
* Boundary component for this App. Wraps the subtree with the initial
|
|
14
|
+
* `env` value passed to {@link App}.
|
|
15
|
+
*/
|
|
16
|
+
readonly Boundary: React.FC<{
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
}>;
|
|
19
|
+
/**
|
|
20
|
+
* Hook returning a stable `Context` handle. The handle's
|
|
21
|
+
* `context.useActions(initialModel?, getData?)` materialises the
|
|
22
|
+
* component's `[model, actions, data]` tuple. Every handler's
|
|
23
|
+
* `context.env` is typed as `S`.
|
|
24
|
+
*/
|
|
25
|
+
readonly useContext: <M extends Model | void = void, AC extends Actions | void = void, D extends Props = Props>() => AppContextHandle<M, AC, D, S>;
|
|
26
|
+
/**
|
|
27
|
+
* Read-only Proxy over the per-Boundary Env, typed as `S`. Reads use
|
|
28
|
+
* dot notation (`env.session`) and always reflect the latest value
|
|
29
|
+
* across `await` boundaries. Writes flow through
|
|
30
|
+
* `context.actions.produce(({ env }) => { ... })`.
|
|
31
|
+
*/
|
|
32
|
+
readonly useEnv: () => Readonly<S>;
|
|
33
|
+
/**
|
|
34
|
+
* `Resource` factory bound to this App's Env. Same shape as the
|
|
35
|
+
* top-level `Resource`: call directly for an in-memory cache, or use
|
|
36
|
+
* `app.Resource.Cachable(cache, fetcher)` for persistence.
|
|
37
|
+
*/
|
|
38
|
+
readonly Resource: AppResource<S>;
|
|
39
|
+
/**
|
|
40
|
+
* Opens a typed multicast scope. The generic `MulticastActions` declares
|
|
41
|
+
* the `Distribution.Multicast` action class (or union of classes)
|
|
42
|
+
* whose dispatches are routed through this scope — the
|
|
43
|
+
* returned handle mirrors the App surface but widens
|
|
44
|
+
* `useContext().actions.dispatch` to accept actions from `MulticastActions`
|
|
45
|
+
* on top of the local `AC` class.
|
|
46
|
+
*
|
|
47
|
+
* Render `<scope.Boundary>` to open the scope at runtime; nesting
|
|
48
|
+
* multiple boundaries from different `app.Scope()` calls is fine,
|
|
49
|
+
* each runs as an independent emitter shadowed for its subtree.
|
|
50
|
+
*
|
|
51
|
+
* The Scope handle deliberately does NOT expose a further `Scope`
|
|
52
|
+
* method — the multicast surface must be declared at the
|
|
53
|
+
* `app.Scope<MulticastActions>()` call site so the type union is explicit.
|
|
54
|
+
*/
|
|
55
|
+
readonly Scope: <MulticastActions>() => Scope<S, MulticastActions>;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Creates an `App` — the entrypoint for a typed Env shape `S`,
|
|
59
|
+
* inferred from `config.env`. `App<S>` exposes `Boundary`, hooks, and
|
|
60
|
+
* a `Resource` factory all wired against the same shape.
|
|
61
|
+
*
|
|
62
|
+
* Each `<app.Boundary>` instance owns its own Env, so different `App`s
|
|
63
|
+
* can coexist in the same tree with completely independent shapes.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```tsx
|
|
67
|
+
* import { App } from "march-hare";
|
|
68
|
+
*
|
|
69
|
+
* type Session = { accessToken: string };
|
|
70
|
+
*
|
|
71
|
+
* export const app = App({
|
|
72
|
+
* env: {
|
|
73
|
+
* session: null as Session | null,
|
|
74
|
+
* operating: "idle" as "idle" | "signing-out",
|
|
75
|
+
* },
|
|
76
|
+
* });
|
|
77
|
+
*
|
|
78
|
+
* // Root render.
|
|
79
|
+
* <app.Boundary>
|
|
80
|
+
* <Root />
|
|
81
|
+
* </app.Boundary>;
|
|
82
|
+
*
|
|
83
|
+
* // In a feature's actions.ts:
|
|
84
|
+
* export function useAuthActions() {
|
|
85
|
+
* const context = app.useContext<void, typeof Actions>();
|
|
86
|
+
* const actions = context.useActions();
|
|
87
|
+
*
|
|
88
|
+
* actions.useAction(Actions.SignOut, async (context) => {
|
|
89
|
+
* context.actions.produce(({ env }) => {
|
|
90
|
+
* env.session = null;
|
|
91
|
+
* });
|
|
92
|
+
* });
|
|
93
|
+
*
|
|
94
|
+
* return actions;
|
|
95
|
+
* }
|
|
96
|
+
*
|
|
97
|
+
* // In resources.ts:
|
|
98
|
+
* export const user = app.Resource<User>((context) =>
|
|
99
|
+
* ky
|
|
100
|
+
* .get("/api/user", {
|
|
101
|
+
* headers: context.env.session
|
|
102
|
+
* ? { Authorization: `Bearer ${context.env.session.accessToken}` }
|
|
103
|
+
* : {},
|
|
104
|
+
* signal: context.controller.signal,
|
|
105
|
+
* })
|
|
106
|
+
* .json<User>(),
|
|
107
|
+
* );
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export declare function App<S extends object = Env>(config?: {
|
|
111
|
+
env: S;
|
|
112
|
+
}): App<S>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Args, ResourceHandle } from '../resource/types';
|
|
2
|
+
import { Cache } from '../cache/index';
|
|
3
|
+
import { Actions, Context, Model, Props, UseActions } from '../types/index';
|
|
4
|
+
import { Data } from '../actions/types';
|
|
5
|
+
/**
|
|
6
|
+
* Args object passed to an `app.Resource` fetcher. Same shape as the
|
|
7
|
+
* base `Resource` fetcher's args but with `env` typed as `S`.
|
|
8
|
+
*/
|
|
9
|
+
export type AppArgs<S, P extends object = Record<never, never>> = Omit<Args<P>, "env"> & {
|
|
10
|
+
readonly env: Readonly<S>;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Fetcher signature for an `app.Resource` declaration. The fetcher's
|
|
14
|
+
* `context.env` is typed against the App's inferred Env shape `S`.
|
|
15
|
+
*/
|
|
16
|
+
export type AppFetcher<S, T, P extends object = Record<never, never>> = (context: AppArgs<S, P>) => Promise<T>;
|
|
17
|
+
/**
|
|
18
|
+
* `app.Resource(fetcher)` — in-memory cache, no persistence.
|
|
19
|
+
* `app.Resource.Cachable(cache, fetcher)` — persistent cache wired
|
|
20
|
+
* to the supplied `Cache` adapter. Both forms type `context.env` as
|
|
21
|
+
* the App's Env shape.
|
|
22
|
+
*/
|
|
23
|
+
export type AppResource<S> = {
|
|
24
|
+
<T, P extends object = Record<never, never>>(fetcher: AppFetcher<S, T, P>): ResourceHandle<T, P>;
|
|
25
|
+
readonly Cachable: <T, P extends object = Record<never, never>>(cache: Cache, fetcher: AppFetcher<S, T, P>) => ResourceHandle<T, P>;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Phantom marker so consumers of the App's hooks see `env: S` typing
|
|
29
|
+
* at the type-system level; at runtime the value is the same proxy as
|
|
30
|
+
* the loose `Env` type.
|
|
31
|
+
*
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
type EnvView<S> = {
|
|
35
|
+
readonly __appEnv?: S;
|
|
36
|
+
};
|
|
37
|
+
type AppActionsResult<M, AC, D, S> = UseActions<M extends Model | void ? M : void, AC extends Actions | void ? AC : void, D extends Props ? D : Props> & EnvView<S>;
|
|
38
|
+
type AppUseActions<M, AC, D, S> = M extends void ? (getData?: Data<D & Props>) => AppActionsResult<M, AC, D, S> : (initialModel: M, getData?: Data<D & Props>) => AppActionsResult<M, AC, D, S>;
|
|
39
|
+
/**
|
|
40
|
+
* `Context` handle returned by `app.useContext()`. Mirrors the base
|
|
41
|
+
* {@link Context} but with the Env-typed slots overridden to `S`.
|
|
42
|
+
*/
|
|
43
|
+
export type AppContextHandle<M, AC, D, S> = {
|
|
44
|
+
readonly actions: {
|
|
45
|
+
dispatch: Context<M extends Model | void ? M : void, AC extends Actions | void ? AC : void, D extends Props ? D : Props>["actions"]["dispatch"];
|
|
46
|
+
};
|
|
47
|
+
readonly useActions: AppUseActions<M, AC, D, S>;
|
|
48
|
+
};
|
|
49
|
+
export {};
|
|
@@ -6,7 +6,7 @@ import * as React from "react";
|
|
|
6
6
|
* When a broadcast or multicast action is dispatched, the payload is
|
|
7
7
|
* stored so that late-mounting components can replay it via
|
|
8
8
|
* {@link useLifecycles} and handlers can read it via
|
|
9
|
-
* `context.actions.
|
|
9
|
+
* `context.actions.final()`.
|
|
10
10
|
*/
|
|
11
11
|
export declare class BroadcastEmitter extends EventEmitter {
|
|
12
12
|
private cache;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Props } from './types';
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
export { useEnv } from './utils';
|
|
4
|
+
/**
|
|
5
|
+
* Loose runtime shape for the per-`<Boundary>` Env. Each {@link App}
|
|
6
|
+
* narrows this to its own typed env via `App<S>({ env })`; the
|
|
7
|
+
* loose type exists so the framework's internal plumbing
|
|
8
|
+
* (`<Boundary>`, `useEnv`, handler `context.env`, Resource
|
|
9
|
+
* fetcher `context.env`) does not need to be parametric over S.
|
|
10
|
+
*
|
|
11
|
+
* Consumers should declare their Env shape inline via `App({ env })`
|
|
12
|
+
* — the inferred `S` is what flows through `app.useContext`,
|
|
13
|
+
* `app.useEnv`, and `app.Resource`. Module augmentation of `Env`
|
|
14
|
+
* is no longer required.
|
|
15
|
+
*/
|
|
16
|
+
export type Env = Record<string, unknown>;
|
|
17
|
+
/**
|
|
18
|
+
* Provides a per-Boundary {@link Env} value to every component inside
|
|
19
|
+
* the boundary. Usually wired in via the `<Boundary env={initial}>`
|
|
20
|
+
* prop rather than used directly.
|
|
21
|
+
*
|
|
22
|
+
* The Env is **not** reactive. Mutating it does not trigger a
|
|
23
|
+
* re-render. Drive view state through the model; use the Env for
|
|
24
|
+
* cross-handler coordination.
|
|
25
|
+
*/
|
|
26
|
+
export declare function Env({ initial, children }: Props): React.ReactNode;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { Env } from './index';
|
|
3
|
+
export type { Env } from './index';
|
|
4
|
+
/**
|
|
5
|
+
* Props for the Env provider component. Accepts the initial Env
|
|
6
|
+
* value that satisfies the augmented {@link Env} interface.
|
|
7
|
+
*/
|
|
8
|
+
export type Props = {
|
|
9
|
+
initial: Env;
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
import { Env } from './index';
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
/**
|
|
5
|
+
* React context exposing the per-Boundary Env 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<Env>>;
|
|
11
|
+
/**
|
|
12
|
+
* Hook that returns a read-only handle to the per-Boundary {@link Env}.
|
|
13
|
+
* Reads use plain dot notation (`env.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 Env inside an action handler
|
|
18
|
+
* via `context.actions.produce(({ model, env }) => { env.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
|
+
* Prefer `app.useEnv()` from an {@link App} instance — the App
|
|
23
|
+
* factory infers the Env's shape from `app.env` and types every
|
|
24
|
+
* read/write against it. The bare `useEnv()` exists for non-App
|
|
25
|
+
* call sites and returns the loose {@link Env} record type.
|
|
26
|
+
*/
|
|
27
|
+
export declare function useEnv(): Env;
|
|
28
|
+
/**
|
|
29
|
+
* Internal accessor for the per-Boundary Env ref — used by the
|
|
30
|
+
* Resource layer to pass a fresh snapshot to each fetcher invocation
|
|
31
|
+
* and by the action layer to write through `context.actions.produce`.
|
|
32
|
+
* Not exported from the library.
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
export declare function useEnvRef(): RefObject<Env>;
|
|
@@ -1,40 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
import { ComponentType, ReactNode } from 'react';
|
|
3
|
-
export { useScope, getScope } from './utils';
|
|
1
|
+
export { Context, useScope, getScope } from './utils';
|
|
4
2
|
export type { ScopeEntry, ScopeContext } from './types';
|
|
5
|
-
/**
|
|
6
|
-
* Higher-order component that opens a multicast scope keyed by the supplied
|
|
7
|
-
* multicast action. Components rendered inside the wrapped tree (and any
|
|
8
|
-
* descendants) participate in the scope: every dispatch of `action` reaches
|
|
9
|
-
* every subscriber inside this boundary.
|
|
10
|
-
*
|
|
11
|
-
* Each multicast action defines its own scope — pass the same action you
|
|
12
|
-
* declared with `Distribution.Multicast` to both `withScope` and
|
|
13
|
-
* `actions.dispatch`.
|
|
14
|
-
*
|
|
15
|
-
* Multicast caches the most recent dispatched value per scope so late-mounted
|
|
16
|
-
* components can read it via `context.actions.resolution()`.
|
|
17
|
-
*
|
|
18
|
-
* @param action - The multicast action that opens this scope.
|
|
19
|
-
* @param Component - The component to wrap.
|
|
20
|
-
* @returns A component that renders the original inside a fresh scope boundary.
|
|
21
|
-
*
|
|
22
|
-
* @example
|
|
23
|
-
* ```tsx
|
|
24
|
-
* export class Scope {
|
|
25
|
-
* static Mood = Action<Mood>("Mood", Distribution.Multicast);
|
|
26
|
-
* }
|
|
27
|
-
*
|
|
28
|
-
* function Mood() {
|
|
29
|
-
* return (
|
|
30
|
-
* <>
|
|
31
|
-
* <Happy />
|
|
32
|
-
* <Sad />
|
|
33
|
-
* </>
|
|
34
|
-
* );
|
|
35
|
-
* }
|
|
36
|
-
*
|
|
37
|
-
* export default withScope(Scope.Mood, Mood);
|
|
38
|
-
* ```
|
|
39
|
-
*/
|
|
40
|
-
export declare function withScope<P extends object, T = unknown>(action: MulticastPayload<T>, Component: ComponentType<P>): (props: P) => ReactNode;
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
import { BroadcastEmitter } from '../broadcast/utils';
|
|
2
|
-
import { ActionId } from '../tasks/types';
|
|
3
2
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* Runtime entry for a single multicast scope opened by an
|
|
4
|
+
* `<app.Scope().Boundary>`. The `id` uniquely identifies this scope
|
|
5
|
+
* instance; the `emitter` carries every multicast event dispatched
|
|
6
|
+
* inside the boundary, keyed internally by the action's symbol.
|
|
7
|
+
*
|
|
8
|
+
* Nested boundaries shadow outer ones via React context.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
7
11
|
*/
|
|
8
12
|
export type ScopeEntry = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
/** BroadcastEmitter for multicast events within this scope (caches last payload per event) */
|
|
12
|
-
emitter: BroadcastEmitter;
|
|
13
|
+
readonly id: symbol;
|
|
14
|
+
readonly emitter: BroadcastEmitter;
|
|
13
15
|
};
|
|
14
16
|
/**
|
|
15
|
-
* The scope context
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* The scope context. `null` when no `<app.Scope().Boundary>` exists in
|
|
18
|
+
* the ancestor chain; otherwise the nearest entry — React context
|
|
19
|
+
* shadowing makes nested boundaries override outer ones for the
|
|
20
|
+
* subtree.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
19
23
|
*/
|
|
20
|
-
export type ScopeContext =
|
|
24
|
+
export type ScopeContext = ScopeEntry | null;
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import { ScopeContext, ScopeEntry } from './types';
|
|
2
|
-
import { ActionId } from '../tasks/types';
|
|
3
2
|
import * as React from "react";
|
|
4
3
|
/**
|
|
5
|
-
* React context for the scope
|
|
6
|
-
*
|
|
4
|
+
* React context for the nearest multicast scope. `null` at the root.
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
7
|
*/
|
|
8
8
|
export declare const Context: React.Context<ScopeContext>;
|
|
9
9
|
/**
|
|
10
|
-
* Hook
|
|
10
|
+
* Hook that returns the nearest multicast scope entry. `null` when
|
|
11
|
+
* the caller is not rendered inside any `<app.Scope().Boundary>`.
|
|
11
12
|
*
|
|
12
|
-
* @
|
|
13
|
+
* @internal
|
|
13
14
|
*/
|
|
14
15
|
export declare function useScope(): ScopeContext;
|
|
15
16
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
17
|
+
* Pass-through accessor. Kept for the dispatch/subscribe sites that
|
|
18
|
+
* previously needed an action-keyed lookup; now the scope is a single
|
|
19
|
+
* entry (or `null`), so this returns it as-is.
|
|
20
|
+
*
|
|
21
|
+
* @internal
|
|
18
22
|
*/
|
|
19
|
-
export declare function getScope(context: ScopeContext
|
|
23
|
+
export declare function getScope(context: ScopeContext): ScopeEntry | null;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { PendingCall } from '../../../resource/index';
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Per-`<Boundary>` registry for `.coalesce(token)` sharing. Outer map
|
|
5
|
+
* keys on the `PendingCall.run` function identity (stable per Resource
|
|
6
|
+
* via the `build()` closure); inner map keys on
|
|
7
|
+
* `${paramsKey}|${coalesceKey(token)}`. While an entry exists every
|
|
8
|
+
* caller awaiting `.coalesce(token)` for the same Resource + params +
|
|
9
|
+
* token receives the same promise.
|
|
10
|
+
*
|
|
11
|
+
* Lifted into React context so each `<app.Boundary>` owns its own
|
|
12
|
+
* registry — two `App` instances in the same tree cannot collide
|
|
13
|
+
* on a shared token like `.coalesce("k")`.
|
|
14
|
+
*
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export type Sharing = WeakMap<PendingCall["run"], Map<string, Promise<unknown>>>;
|
|
18
|
+
/**
|
|
19
|
+
* React context exposing the per-Boundary sharing registry. The
|
|
20
|
+
* fallback is a fresh `WeakMap` used when `useSharing()` is read
|
|
21
|
+
* outside any `<Boundary>` — calls work but never share with any
|
|
22
|
+
* other component.
|
|
23
|
+
*
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
export declare const Context: React.Context<Sharing>;
|
|
27
|
+
/**
|
|
28
|
+
* Wraps children with a Boundary-scoped sharing registry for
|
|
29
|
+
* `.coalesce(token)`. Rendered as part of {@link Boundary}; not
|
|
30
|
+
* exposed standalone.
|
|
31
|
+
*
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
export declare function SharingProvider({ children, }: {
|
|
35
|
+
children: React.ReactNode;
|
|
36
|
+
}): React.ReactElement;
|
|
37
|
+
/**
|
|
38
|
+
* Hook returning the per-Boundary sharing registry. Used by the
|
|
39
|
+
* `.coalesce(token)` chainable inside `useActions`.
|
|
40
|
+
*
|
|
41
|
+
* @internal
|
|
42
|
+
*/
|
|
43
|
+
export declare function useSharing(): Sharing;
|
package/dist/boundary/index.d.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import { Props } from './types';
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Low-level boundary primitive. Wraps children with the Broadcaster,
|
|
5
|
+
* Env, and Tasks providers required by every March Hare hook.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* Most applications should reach for {@link App} instead —
|
|
8
|
+
* `App<S>({ env })` returns a typed `app.Boundary` along with
|
|
9
|
+
* matching `useContext` / `useEnv` / `Resource` factories that all
|
|
10
|
+
* close over the App's inferred env shape `S`. The bare `Boundary`
|
|
11
|
+
* is exposed for advanced or library-internal use where the loose
|
|
12
|
+
* Env record type is sufficient.
|
|
13
13
|
*
|
|
14
14
|
* @example
|
|
15
15
|
* ```tsx
|
|
16
|
-
* <Boundary
|
|
16
|
+
* <Boundary env={{ session: null, locale: "en-GB" }}>
|
|
17
17
|
* <App />
|
|
18
18
|
* </Boundary>
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
|
-
export declare function Boundary({
|
|
21
|
+
export declare function Boundary({ env, children }: Props): React.ReactNode;
|
package/dist/boundary/types.d.ts
CHANGED
|
@@ -1,22 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Env } from './components/env/types';
|
|
2
2
|
import type * as React from "react";
|
|
3
3
|
export type Props = {
|
|
4
4
|
/**
|
|
5
|
-
* Initial value of the per-Boundary {@link
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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.
|
|
5
|
+
* Initial value of the per-Boundary {@link Env}. Prefer `App({ env })`
|
|
6
|
+
* — it infers the Env shape and threads it through `app.useContext`,
|
|
7
|
+
* `app.useEnv`, and `app.Resource`. Pass `env` directly here only for
|
|
8
|
+
* advanced cases where the loose {@link Env} record type is sufficient.
|
|
19
9
|
*/
|
|
20
|
-
|
|
10
|
+
env?: Env;
|
|
21
11
|
children: React.ReactNode;
|
|
22
12
|
};
|
package/dist/cache/index.d.ts
CHANGED
|
@@ -28,11 +28,11 @@ export type { Adapter, Encoded } from './types';
|
|
|
28
28
|
* clear: () => localStorage.clear(),
|
|
29
29
|
* });
|
|
30
30
|
*
|
|
31
|
-
* //
|
|
32
|
-
* export const cat = Resource(
|
|
33
|
-
* async ({ controller }) => fetchCat(controller.signal),
|
|
31
|
+
* // Wire it into a Resource — successful runs write through automatically.
|
|
32
|
+
* export const cat = Resource({
|
|
34
33
|
* cache,
|
|
35
|
-
* )
|
|
34
|
+
* fetch: (context) => fetchCat(context.controller.signal),
|
|
35
|
+
* });
|
|
36
36
|
* ```
|
|
37
37
|
*/
|
|
38
38
|
export type Cache = {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Coalesce } from '../resource/types';
|
|
2
|
+
/**
|
|
3
|
+
* Sentinel token used when `.coalesce()` is called with no explicit
|
|
4
|
+
* argument. Every untokened caller for the same `(Resource, params)`
|
|
5
|
+
* slot collapses onto this single key, so multiple callers chaining
|
|
6
|
+
* `.coalesce()` share one in-flight fetch.
|
|
7
|
+
*
|
|
8
|
+
* The symbol description follows the `march-hare.{category}/{name}`
|
|
9
|
+
* convention used by the rest of the library's internal symbols.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export declare const token: unique symbol;
|
|
14
|
+
/**
|
|
15
|
+
* Builds the per-call dedupe key for `.coalesce(token)`.
|
|
16
|
+
*
|
|
17
|
+
* The full registry key (constructed at the call site) is
|
|
18
|
+
* `${JSON.stringify(params)}|${coalesceKey(token)}`; this function is
|
|
19
|
+
* responsible only for the trailing token segment. Every supported
|
|
20
|
+
* `Coalesce` primitive (`string`, `number`, `bigint`, `boolean`,
|
|
21
|
+
* `symbol`) is prefixed with a single-character type tag so that
|
|
22
|
+
* structurally identical values from different types stay distinct
|
|
23
|
+
* — e.g. the string `"5"` does not collide with the number `5`,
|
|
24
|
+
* and `Symbol("X")` does not collide with the string `"X"`.
|
|
25
|
+
*
|
|
26
|
+
* Symbols are keyed by their `description` (falling back to
|
|
27
|
+
* `String(value)` for description-less symbols) so two `Symbol("X")`
|
|
28
|
+
* instances declared in separate modules still hash to the same key.
|
|
29
|
+
* That is intentional: the public contract is "same description →
|
|
30
|
+
* same coalesce group", which keeps the API friendly for the common
|
|
31
|
+
* enum-token pattern at call sites.
|
|
32
|
+
*
|
|
33
|
+
* Any unrecognised value falls through to the object branch and is
|
|
34
|
+
* keyed by its JSON shape; `Coalesce` is constrained to primitives at
|
|
35
|
+
* the type level, so this branch is reachable only from `unknown`
|
|
36
|
+
* coercion in tests.
|
|
37
|
+
*
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
export declare function coalesceKey(value: Coalesce): string;
|
|
41
|
+
/**
|
|
42
|
+
* Wraps `promise` so that aborting `signal` rejects the returned
|
|
43
|
+
* promise with `signal.reason`, even when `promise` itself never
|
|
44
|
+
* settles. The original promise is left alone — the underlying
|
|
45
|
+
* work continues (other awaiters keep their grip) and only this
|
|
46
|
+
* caller's view of it is severed.
|
|
47
|
+
*
|
|
48
|
+
* Used by the `.coalesce(token)` chainable: the shared in-flight fetch
|
|
49
|
+
* runs on a detached `AbortController` so one caller's abort does not
|
|
50
|
+
* cancel work the others are still waiting on, while each caller's own
|
|
51
|
+
* `context.task.controller` still aborts its personal await via this
|
|
52
|
+
* wrapper. The `{ once: true }` listener and the cleanup on settle keep
|
|
53
|
+
* the wrapper free of leaks across long-lived signals.
|
|
54
|
+
*
|
|
55
|
+
* @internal
|
|
56
|
+
*/
|
|
57
|
+
export declare function withAbort<T>(promise: Promise<T>, signal: AbortSignal): Promise<T>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Actions, Context as ContextHandle, Model, Props } from '../types/index';
|
|
2
|
+
/**
|
|
3
|
+
* Returns a stable, typed controller handle up-front — before a
|
|
4
|
+
* model is declared via `context.useActions(...)`. Use this when an
|
|
5
|
+
* external imperative library (form, animation, third-party SDK) needs a
|
|
6
|
+
* dispatch callback at construction time, while the value that library
|
|
7
|
+
* returns must flow back into the controller's data callback.
|
|
8
|
+
*
|
|
9
|
+
* The handle exposes `dispatch(action, payload?)` and a `useActions(...)`
|
|
10
|
+
* method that materialises the component-local model and reactive data
|
|
11
|
+
* — the M and D pair of `useContext<M, AC, D>` — and
|
|
12
|
+
* returns the `[model, actions, data]` tuple with `useAction`, `dispatch`,
|
|
13
|
+
* `inspect`, and `stream` attached. The first invocation of
|
|
14
|
+
* `context.actions.dispatch(...)` must come from an event handler — not
|
|
15
|
+
* synchronously during render — because the underlying dispatch
|
|
16
|
+
* target is wired up when `context.useActions(...)` runs in the same
|
|
17
|
+
* render pass.
|
|
18
|
+
*
|
|
19
|
+
* @template M The model type representing the component's state.
|
|
20
|
+
* @template AC The actions class containing action definitions.
|
|
21
|
+
* @template D The data type for reactive external values.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const context = useContext<Model, typeof Actions, Data>();
|
|
26
|
+
*
|
|
27
|
+
* const form = useForm({
|
|
28
|
+
* onSubmit: () => void context.actions.dispatch(Actions.Submit),
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* const actions = context.useActions(
|
|
32
|
+
* { user: resource.user() },
|
|
33
|
+
* () => ({ form }),
|
|
34
|
+
* );
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
export declare function useContext<M extends Model | void = void, AC extends Actions | void = void, D extends Props = Props>(): ContextHandle<M, AC, D>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The signature `useContext` stores in a ref once
|
|
3
|
+
* `context.useActions(...)` runs — the underlying dispatch target.
|
|
4
|
+
* `context.actions.dispatch(action, payload?)` forwards through this
|
|
5
|
+
* function so the handle returned by `useContext` is callable before
|
|
6
|
+
* the matching `useActions` call has wired everything up.
|
|
7
|
+
*
|
|
8
|
+
* Erased to `unknown` because the public surface re-types `dispatch` per
|
|
9
|
+
* call site via the action's generic constraints; the runtime contract
|
|
10
|
+
* is just "(action, payload?) => Promise<void>".
|
|
11
|
+
*
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
export type DispatchTarget = (action: unknown, payload?: unknown) => Promise<void>;
|