march-hare 0.8.0 → 0.10.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 +491 -211
- package/dist/actions/index.d.ts +46 -0
- package/dist/{hooks → actions}/utils.d.ts +0 -39
- package/dist/app/index.d.ts +132 -0
- package/dist/app/types.d.ts +82 -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/components/tap/index.d.ts +36 -0
- package/dist/boundary/components/tap/types.d.ts +150 -0
- package/dist/boundary/components/tap/utils.d.ts +14 -0
- package/dist/boundary/index.d.ts +10 -10
- package/dist/boundary/types.d.ts +46 -14
- package/dist/cache/index.d.ts +4 -4
- package/dist/coalesce/index.d.ts +57 -0
- package/dist/context/index.d.ts +41 -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 +9 -13
- package/dist/march-hare.js +8 -5
- package/dist/march-hare.umd.cjs +1 -1
- package/dist/resource/index.d.ts +55 -78
- package/dist/resource/types.d.ts +87 -11
- package/dist/resource/utils.d.ts +1 -1
- package/dist/scope/index.d.ts +63 -0
- package/dist/scope/types.d.ts +55 -0
- package/dist/types/index.d.ts +108 -58
- package/dist/utils/index.d.ts +6 -5
- package/dist/with/index.d.ts +111 -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/index.d.ts +0 -83
- /package/dist/{hooks → actions}/types.d.ts +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Data } from './types';
|
|
2
|
+
import { Model, Props, Actions, UseActions } from '../types/index';
|
|
3
|
+
/**
|
|
4
|
+
* A hook for managing state with actions.
|
|
5
|
+
*
|
|
6
|
+
* Call `useActions` first, then use `actions.useAction` to bind handlers
|
|
7
|
+
* to action symbols. Types are pre-baked from the generic parameters, so
|
|
8
|
+
* no additional type annotations are needed on handler calls.
|
|
9
|
+
*
|
|
10
|
+
* The hook returns a result containing:
|
|
11
|
+
* 1. The current model state
|
|
12
|
+
* 2. An actions object with `dispatch`, `inspect`, and `useAction`
|
|
13
|
+
* 3. A read-only snapshot of the data values produced by `getData` —
|
|
14
|
+
* the same values handlers read via `context.data`, exposed here for
|
|
15
|
+
* JSX consumption so the view and the handler share one named source.
|
|
16
|
+
*
|
|
17
|
+
* The `inspect` property provides access to Immertation's annotation system,
|
|
18
|
+
* allowing you to check for pending operations on model properties.
|
|
19
|
+
*
|
|
20
|
+
* @template M The model type representing the component's state.
|
|
21
|
+
* @template AC The actions class containing action definitions.
|
|
22
|
+
* @template D The data type for reactive external values.
|
|
23
|
+
* @param model The initial model state.
|
|
24
|
+
* @param getData Optional function that returns reactive values as data.
|
|
25
|
+
* Values returned are accessible via `context.data` in action handlers,
|
|
26
|
+
* always reflecting the latest values even after await operations.
|
|
27
|
+
* @returns A result `[model, actions, data]` with pre-typed `useAction` method.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* // Basic usage
|
|
32
|
+
* const [model, actions] = useActions<Model, typeof Actions>(model);
|
|
33
|
+
*
|
|
34
|
+
* // Without a model (actions-only)
|
|
35
|
+
* const [, actions] = useActions<void, typeof Actions>();
|
|
36
|
+
*
|
|
37
|
+
* // With reactive data — consumed in JSX and handlers alike.
|
|
38
|
+
* const [model, actions, data] = useActions<
|
|
39
|
+
* Model,
|
|
40
|
+
* typeof Actions,
|
|
41
|
+
* { query: string }
|
|
42
|
+
* >(model, () => ({ query: props.query }));
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
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>;
|
|
46
|
+
export declare function useActions<M extends Model, A extends Actions | void = void, D extends Props = Props>(model: 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,132 @@
|
|
|
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 { Tap } from '../boundary/components/tap/types';
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
export type { AppArgs, AppContextHandle, AppFetcher, AppResource, } from './types';
|
|
8
|
+
/**
|
|
9
|
+
* Returned from {@link App}. Bundles the Boundary, hooks, and Resource
|
|
10
|
+
* factory bound to a single typed Env shape `S`.
|
|
11
|
+
*/
|
|
12
|
+
export type App<S extends object> = {
|
|
13
|
+
/**
|
|
14
|
+
* Boundary component for this App. By default, wraps the subtree with
|
|
15
|
+
* the `env` and `tap` passed to {@link App}; either can be overridden
|
|
16
|
+
* at the call site via the corresponding prop, which is useful when a
|
|
17
|
+
* single `App` definition is rendered against different envs in tests
|
|
18
|
+
* or storybooks.
|
|
19
|
+
*/
|
|
20
|
+
readonly Boundary: React.FC<{
|
|
21
|
+
env?: S;
|
|
22
|
+
tap?: Tap;
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
}>;
|
|
25
|
+
/**
|
|
26
|
+
* Hook returning a stable `Context` handle. The handle's
|
|
27
|
+
* `context.useActions(model?, getData?)` materialises the
|
|
28
|
+
* component's `[model, actions, data]` tuple. Every handler's
|
|
29
|
+
* `context.env` is typed as `S`.
|
|
30
|
+
*/
|
|
31
|
+
readonly useContext: <M extends Model | void = void, AC extends Actions | void = void, D extends Props = Props>() => AppContextHandle<M, AC, D, S>;
|
|
32
|
+
/**
|
|
33
|
+
* Read-only Proxy over the per-Boundary Env, typed as `S`. Reads use
|
|
34
|
+
* dot notation (`env.session`) and always reflect the latest value
|
|
35
|
+
* across `await` boundaries. Writes flow through
|
|
36
|
+
* `context.actions.produce(({ env }) => { ... })`.
|
|
37
|
+
*/
|
|
38
|
+
readonly useEnv: () => Readonly<S>;
|
|
39
|
+
/**
|
|
40
|
+
* `Resource` factory bound to this App's Env. Same shape as the
|
|
41
|
+
* top-level `Resource`: call directly for an in-memory cache, or use
|
|
42
|
+
* `app.Resource.Cachable(cache, fetcher)` for persistence.
|
|
43
|
+
*/
|
|
44
|
+
readonly Resource: AppResource<S>;
|
|
45
|
+
/**
|
|
46
|
+
* Opens a typed multicast scope. The generic `MulticastActions` declares
|
|
47
|
+
* the `Distribution.Multicast` action class (or union of classes)
|
|
48
|
+
* whose dispatches are routed through this scope — the
|
|
49
|
+
* returned handle mirrors the App surface but widens
|
|
50
|
+
* `useContext().actions.dispatch` to accept actions from `MulticastActions`
|
|
51
|
+
* on top of the local `AC` class.
|
|
52
|
+
*
|
|
53
|
+
* Render `<scope.Boundary>` to open the scope at runtime; nesting
|
|
54
|
+
* multiple boundaries from different `app.Scope()` calls is fine,
|
|
55
|
+
* each runs as an independent emitter shadowed for its subtree.
|
|
56
|
+
*
|
|
57
|
+
* The Scope handle deliberately does NOT expose a further `Scope`
|
|
58
|
+
* method — the multicast surface must be declared at the
|
|
59
|
+
* `app.Scope<MulticastActions>()` call site so the type union is explicit.
|
|
60
|
+
*/
|
|
61
|
+
readonly Scope: <MulticastActions>() => Scope<S, MulticastActions>;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Creates an `App` — the entrypoint for a typed Env shape `S`,
|
|
65
|
+
* inferred from `config.env`. `App<S>` exposes `Boundary`, hooks, and
|
|
66
|
+
* a `Resource` factory all wired against the same shape.
|
|
67
|
+
*
|
|
68
|
+
* Each `<app.Boundary>` instance owns its own Env, so different `App`s
|
|
69
|
+
* can coexist in the same tree with completely independent shapes.
|
|
70
|
+
*
|
|
71
|
+
* Pass `tap` to subscribe to every action handler's dispatch / settle /
|
|
72
|
+
* error inside the boundary — useful for analytics, audit logging,
|
|
73
|
+
* Sentry breadcrumbs. See `recipes/tap.md`. Both `env` and `tap` can
|
|
74
|
+
* also be supplied as props on `<app.Boundary>`, where they override the
|
|
75
|
+
* defaults set at `App()` time (handy for test renders and storybooks).
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```tsx
|
|
79
|
+
* import { App, type Tapped } from "march-hare";
|
|
80
|
+
*
|
|
81
|
+
* type Session = { accessToken: string };
|
|
82
|
+
*
|
|
83
|
+
* function tap(event: Tapped) {
|
|
84
|
+
* if (event.type === "error") {
|
|
85
|
+
* Sentry.captureException(event.error, { tags: { action: event.action } });
|
|
86
|
+
* }
|
|
87
|
+
* }
|
|
88
|
+
*
|
|
89
|
+
* export const app = App({
|
|
90
|
+
* env: {
|
|
91
|
+
* session: null as Session | null,
|
|
92
|
+
* operating: "idle" as "idle" | "signing-out",
|
|
93
|
+
* },
|
|
94
|
+
* tap,
|
|
95
|
+
* });
|
|
96
|
+
*
|
|
97
|
+
* // Root render.
|
|
98
|
+
* <app.Boundary>
|
|
99
|
+
* <Root />
|
|
100
|
+
* </app.Boundary>;
|
|
101
|
+
*
|
|
102
|
+
* // In a feature's actions.ts:
|
|
103
|
+
* export function useAuthActions() {
|
|
104
|
+
* const context = app.useContext<void, typeof Actions>();
|
|
105
|
+
* const actions = context.useActions();
|
|
106
|
+
*
|
|
107
|
+
* actions.useAction(Actions.SignOut, async (context) => {
|
|
108
|
+
* context.actions.produce(({ env }) => {
|
|
109
|
+
* env.session = null;
|
|
110
|
+
* });
|
|
111
|
+
* });
|
|
112
|
+
*
|
|
113
|
+
* return actions;
|
|
114
|
+
* }
|
|
115
|
+
*
|
|
116
|
+
* // In resources.ts:
|
|
117
|
+
* export const user = app.Resource<User>((context) =>
|
|
118
|
+
* ky
|
|
119
|
+
* .get("/api/user", {
|
|
120
|
+
* headers: context.env.session
|
|
121
|
+
* ? { Authorization: `Bearer ${context.env.session.accessToken}` }
|
|
122
|
+
* : {},
|
|
123
|
+
* signal: context.controller.signal,
|
|
124
|
+
* })
|
|
125
|
+
* .json<User>(),
|
|
126
|
+
* );
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export declare function App<S extends object = Env>(config?: {
|
|
130
|
+
env?: S;
|
|
131
|
+
tap?: Tap;
|
|
132
|
+
}): App<S>;
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
import { Env } from '../boundary/components/env/index';
|
|
6
|
+
import { WithHandle } from '../with/index';
|
|
7
|
+
/**
|
|
8
|
+
* Args object passed to an `app.Resource` fetcher. Same shape as the
|
|
9
|
+
* base `Resource` fetcher's args but with `env` typed as `S`.
|
|
10
|
+
*/
|
|
11
|
+
export type AppArgs<S, P extends object = Record<never, never>> = Omit<Args<P>, "env"> & {
|
|
12
|
+
readonly env: Readonly<S>;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Fetcher signature for an `app.Resource` declaration. The fetcher's
|
|
16
|
+
* `context.env` is typed against the App's inferred Env shape `S`.
|
|
17
|
+
*/
|
|
18
|
+
export type AppFetcher<S, T, P extends object = Record<never, never>> = (context: AppArgs<S, P>) => Promise<T>;
|
|
19
|
+
/**
|
|
20
|
+
* `app.Resource(fetcher)` — in-memory cache, no persistence.
|
|
21
|
+
* `app.Resource.Cachable(cache, fetcher)` — persistent cache wired
|
|
22
|
+
* to the supplied `Cache` adapter. Both forms type `context.env` as
|
|
23
|
+
* the App's Env shape.
|
|
24
|
+
*/
|
|
25
|
+
export type AppResource<S> = {
|
|
26
|
+
<T, P extends object = Record<never, never>>(fetcher: AppFetcher<S, T, P>): ResourceHandle<T, P>;
|
|
27
|
+
readonly Cachable: <T, P extends object = Record<never, never>>(cache: Cache, fetcher: AppFetcher<S, T, P>) => ResourceHandle<T, P>;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Tuple shape returned by `context.useActions(...)` on an App-bound
|
|
31
|
+
* Context. Re-exports the base {@link UseActions} with the App's `S`
|
|
32
|
+
* threaded through every `HandlerContext` and produce draft.
|
|
33
|
+
*/
|
|
34
|
+
type AppActionsResult<M, AC, D, S> = UseActions<M extends Model | void ? M : void, AC extends Actions | void ? AC : void, D extends Props ? D : Props, S extends Env ? S : Env>;
|
|
35
|
+
/**
|
|
36
|
+
* `useActions(...)` signature on the App-bound Context. Has two forms:
|
|
37
|
+
* void-model components omit the model argument entirely; everyone else
|
|
38
|
+
* passes their initial model as the first argument and an optional data
|
|
39
|
+
* callback as the second.
|
|
40
|
+
*/
|
|
41
|
+
type AppUseActions<M, AC, D, S> = M extends void ? (getData?: Data<D & Props>) => AppActionsResult<M, AC, D, S> : (model: M, getData?: Data<D & Props>) => AppActionsResult<M, AC, D, S>;
|
|
42
|
+
/**
|
|
43
|
+
* `Context` handle returned by `app.useContext()`. Mirrors the base
|
|
44
|
+
* {@link Context} but threads the App's Env shape `S` through every
|
|
45
|
+
* handler's `context.env` and produce draft.
|
|
46
|
+
*
|
|
47
|
+
* @template M The model type for the component's state, or `void`.
|
|
48
|
+
* @template AC The actions class containing this component's action
|
|
49
|
+
* definitions, or `void` for actions-only consumers.
|
|
50
|
+
* @template D The reactive data type returned from the `useActions(...)`
|
|
51
|
+
* data callback.
|
|
52
|
+
* @template S The App's Env shape, supplied at `App({env})` time.
|
|
53
|
+
*/
|
|
54
|
+
export type AppContextHandle<M, AC, D, S> = {
|
|
55
|
+
/**
|
|
56
|
+
* Stable dispatch surface available before `useActions(...)` runs.
|
|
57
|
+
* Exposes only `dispatch(action, payload?)` — useful when an
|
|
58
|
+
* external imperative library needs a dispatch callback at construction
|
|
59
|
+
* time. Inside handlers, prefer `context.actions.dispatch` for the same
|
|
60
|
+
* call.
|
|
61
|
+
*/
|
|
62
|
+
readonly actions: {
|
|
63
|
+
dispatch: Context<M extends Model | void ? M : void, AC extends Actions | void ? AC : void, D extends Props ? D : Props>["actions"]["dispatch"];
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Typed bag of handler factories bound to `M`. Methods accept
|
|
67
|
+
* lodash-style dotted paths and array indices; keys autocomplete from
|
|
68
|
+
* the model declared on `app.useContext<Model, …>()`. See
|
|
69
|
+
* {@link WithHandle}.
|
|
70
|
+
*/
|
|
71
|
+
readonly with: WithHandle<M extends Model | void ? M : void>;
|
|
72
|
+
/**
|
|
73
|
+
* Materialises the component-local model and reactive data, returning
|
|
74
|
+
* the `[model, actions, data]` tuple with `useAction`, `dispatch`,
|
|
75
|
+
* `inspect`, and `stream` attached. Pass the initial model as the first
|
|
76
|
+
* argument (unless `M` is `void`) and an optional data callback as the
|
|
77
|
+
* second — the callback re-runs every render so handlers reading
|
|
78
|
+
* `context.data` always see fresh values across `await` boundaries.
|
|
79
|
+
*/
|
|
80
|
+
readonly useActions: AppUseActions<M, AC, D, S>;
|
|
81
|
+
};
|
|
82
|
+
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;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Props } from './types';
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
export { useTap } from './utils';
|
|
4
|
+
export type { Tap, Tapped, Invocation, Failure, Mutations, Snapshot, } from './types';
|
|
5
|
+
/**
|
|
6
|
+
* Internal provider that wires a {@link Tap} observer into the React
|
|
7
|
+
* context consumed by `useActions` during dispatch. Rendered by the
|
|
8
|
+
* top-level `<Boundary>` (and indirectly by `<app.Boundary>`); not
|
|
9
|
+
* exposed on the public surface — consumers should pass the
|
|
10
|
+
* callback via the `tap` prop of either boundary instead of mounting
|
|
11
|
+
* this provider directly.
|
|
12
|
+
*
|
|
13
|
+
* The supplied `tap` callback is held in a `useRef` and indirected
|
|
14
|
+
* through a stable `useMemo` wrapper. The ref is synchronised inside a
|
|
15
|
+
* `useLayoutEffect` — React's sanctioned write-after-commit
|
|
16
|
+
* window — so the provider stays compatible with Concurrent
|
|
17
|
+
* rendering, where the render function may be invoked more than once
|
|
18
|
+
* per commit and direct mutation during render would race the
|
|
19
|
+
* scheduler.
|
|
20
|
+
*
|
|
21
|
+
* Keeping the context value referentially constant for the lifetime of
|
|
22
|
+
* the boundary means callers may pass inline arrow functions without
|
|
23
|
+
* invalidating the dispatch pipeline on every render — the
|
|
24
|
+
* latest callback is read at fire time, not at provider-render time.
|
|
25
|
+
* When `tap` is `undefined`, the wrapper short-circuits via optional
|
|
26
|
+
* chaining: no allocation per event beyond the wrapper call itself.
|
|
27
|
+
*
|
|
28
|
+
* @param props.tap Observer to receive lifecycle events for every action
|
|
29
|
+
* handler dispatched inside the boundary. See {@link Tap}.
|
|
30
|
+
* @param props.children Subtree that should observe the supplied tap.
|
|
31
|
+
* @returns Children rendered inside the tap context provider.
|
|
32
|
+
*
|
|
33
|
+
* @see {@link Tap} — the observer signature.
|
|
34
|
+
* @see {@link Tapped} — the discriminated union of event shapes.
|
|
35
|
+
*/
|
|
36
|
+
export declare function Tappable({ tap, children }: Props): React.ReactNode;
|