march-hare 0.6.1 → 0.7.1
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 +89 -96
- package/dist/{src/library/action → action}/index.d.ts +19 -15
- package/dist/{src/library/action → action}/utils.d.ts +2 -2
- package/dist/{src/library/boundary → boundary}/components/broadcast/index.d.ts +2 -2
- package/dist/{src/library/boundary → boundary}/components/broadcast/types.d.ts +1 -1
- package/dist/{src/library/boundary → boundary}/components/consumer/components/partition/index.d.ts +1 -1
- package/dist/{src/library/boundary → boundary}/components/consumer/components/partition/types.d.ts +1 -1
- package/dist/{src/library/boundary → boundary}/components/consumer/index.d.ts +5 -5
- package/dist/{src/library/boundary → boundary}/components/consumer/types.d.ts +1 -1
- package/dist/{src/library/boundary → boundary}/components/consumer/utils.d.ts +1 -1
- package/dist/{src/library/boundary → boundary}/components/scope/index.d.ts +3 -3
- package/dist/{src/library/boundary → boundary}/components/scope/types.d.ts +2 -2
- package/dist/{src/library/boundary → boundary}/components/scope/utils.d.ts +2 -2
- package/dist/boundary/components/store/index.d.ts +41 -0
- package/dist/boundary/components/store/types.d.ts +11 -0
- package/dist/boundary/components/store/utils.d.ts +64 -0
- package/dist/{src/library/boundary → boundary}/components/tasks/index.d.ts +2 -2
- package/dist/{src/library/boundary → boundary}/components/tasks/types.d.ts +3 -3
- package/dist/{src/library/boundary → boundary}/components/tasks/utils.d.ts +1 -1
- package/dist/boundary/index.d.ts +21 -0
- package/dist/boundary/types.d.ts +22 -0
- package/dist/cache/index.d.ts +44 -0
- package/dist/cache/types.d.ts +54 -0
- package/dist/{src/library/error → error}/types.d.ts +1 -1
- package/dist/{src/library/error → error}/utils.d.ts +1 -1
- package/dist/{src/library/hooks → hooks}/index.d.ts +3 -3
- package/dist/{src/library/hooks → hooks}/types.d.ts +3 -3
- package/dist/{src/library/hooks → hooks}/utils.d.ts +3 -3
- package/dist/index.d.ts +19 -0
- package/dist/march-hare.js +6 -6
- package/dist/march-hare.umd.cjs +1 -1
- package/dist/resource/index.d.ts +102 -0
- package/dist/resource/types.d.ts +27 -0
- package/dist/resource/utils.d.ts +37 -0
- package/dist/{src/library/types → types}/index.d.ts +162 -21
- package/dist/{src/library/utils → utils}/index.d.ts +3 -43
- package/dist/utils/types.d.ts +18 -0
- package/dist/{src/library/utils → utils}/utils.d.ts +1 -1
- package/package.json +2 -2
- 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
- package/dist/src/library/boundary/index.d.ts +0 -20
- package/dist/src/library/boundary/types.d.ts +0 -4
- package/dist/src/library/index.d.ts +0 -17
- package/dist/src/library/resource/index.d.ts +0 -65
- package/dist/src/library/resource/types.d.ts +0 -150
- package/dist/src/library/resource/utils.d.ts +0 -23
- package/dist/src/library/utils/types.d.ts +0 -101
- /package/dist/{src/library/annotate → annotate}/index.d.ts +0 -0
- /package/dist/{src/library/boundary → boundary}/components/broadcast/utils.d.ts +0 -0
- /package/dist/{src/library/error → error}/index.d.ts +0 -0
- /package/dist/{src/library/utils.d.ts → utils.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -76,27 +76,26 @@ export default function Profile(): React.ReactElement {
|
|
|
76
76
|
}
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
When you need to do more than just assign the payload – such as making an API request – expand `useAction` to a full function. It can be synchronous, asynchronous, or even a generator. Remote data goes through `Resource` rather than a bare `fetch` – declare the resource at module scope
|
|
79
|
+
When you need to do more than just assign the payload – such as making an API request – expand `useAction` to a full function. It can be synchronous, asynchronous, or even a generator. Remote data goes through `Resource` rather than a bare `fetch` – declare the resource at module scope, fetch from handlers via `context.actions.resource(...)`:
|
|
80
80
|
|
|
81
81
|
```ts
|
|
82
82
|
// resources.ts
|
|
83
83
|
import { Resource } from "march-hare";
|
|
84
84
|
|
|
85
|
-
export const user = Resource(() =>
|
|
85
|
+
export const user = Resource(({ controller }) =>
|
|
86
|
+
ky.get(api.user(), { signal: controller.signal }).json<User>(),
|
|
87
|
+
);
|
|
86
88
|
```
|
|
87
89
|
|
|
88
90
|
```tsx
|
|
89
|
-
const get = {
|
|
90
|
-
user: useResource(resource.user),
|
|
91
|
-
};
|
|
92
|
-
|
|
93
91
|
actions.useAction(Actions.Name, async (context) => {
|
|
94
92
|
context.actions.produce(
|
|
95
93
|
({ model }) =>
|
|
96
94
|
void (model.name = context.actions.annotate(model.name, Op.Update)),
|
|
97
95
|
);
|
|
98
96
|
|
|
99
|
-
|
|
97
|
+
// Auto-threads context.task.controller and the Store snapshot.
|
|
98
|
+
const data = await context.actions.resource(user());
|
|
100
99
|
|
|
101
100
|
context.actions.produce(({ model }) => void (model.name = data.name));
|
|
102
101
|
});
|
|
@@ -109,17 +108,17 @@ If you need to access external reactive values (like props or `useState` from pa
|
|
|
109
108
|
```tsx
|
|
110
109
|
const actions = useActions<Model, typeof Actions, { query: string }>(
|
|
111
110
|
model,
|
|
112
|
-
() => ({
|
|
111
|
+
() => ({
|
|
112
|
+
query: props.query,
|
|
113
|
+
}),
|
|
113
114
|
);
|
|
114
115
|
|
|
115
|
-
const get = {
|
|
116
|
-
search: useResource(resource.search),
|
|
117
|
-
};
|
|
118
|
-
|
|
119
116
|
actions.useAction(Actions.Search, async (context) => {
|
|
120
|
-
|
|
117
|
+
const results = await context.actions.resource(
|
|
118
|
+
search({ query: context.data.query }),
|
|
119
|
+
);
|
|
121
120
|
// context.data.query is always the latest value, even after await
|
|
122
|
-
console.log(context.data.query);
|
|
121
|
+
console.log(context.data.query, results);
|
|
123
122
|
});
|
|
124
123
|
```
|
|
125
124
|
|
|
@@ -177,17 +176,13 @@ class Actions {
|
|
|
177
176
|
```
|
|
178
177
|
|
|
179
178
|
```tsx
|
|
180
|
-
const get = {
|
|
181
|
-
user: useResource(resource.user),
|
|
182
|
-
};
|
|
183
|
-
|
|
184
179
|
actions.useAction(Actions.Profile, async (context) => {
|
|
185
180
|
context.actions.produce(
|
|
186
181
|
({ model }) =>
|
|
187
182
|
void (model.name = context.actions.annotate(model.name, Op.Update)),
|
|
188
183
|
);
|
|
189
184
|
|
|
190
|
-
const data = await
|
|
185
|
+
const data = await context.actions.resource(user());
|
|
191
186
|
|
|
192
187
|
context.actions.produce(({ model }) => void (model.name = data.name));
|
|
193
188
|
|
|
@@ -198,12 +193,8 @@ actions.useAction(Actions.Profile, async (context) => {
|
|
|
198
193
|
Once we have the broadcast action, if we want to listen for it and perform another operation in our local component we can do that via `useAction`:
|
|
199
194
|
|
|
200
195
|
```tsx
|
|
201
|
-
const get = {
|
|
202
|
-
friends: useResource(resource.friends),
|
|
203
|
-
};
|
|
204
|
-
|
|
205
196
|
actions.useAction(Actions.Broadcast.Name, async (context, name) => {
|
|
206
|
-
const data = await
|
|
197
|
+
const data = await context.actions.resource(friends({ name }));
|
|
207
198
|
|
|
208
199
|
context.actions.produce(({ model }) => void (model.friends = data));
|
|
209
200
|
});
|
|
@@ -212,14 +203,10 @@ actions.useAction(Actions.Broadcast.Name, async (context, name) => {
|
|
|
212
203
|
Both `read` and `peek` access the latest cached broadcast value without subscribing via `useAction`. The difference is that `read` waits for any pending annotations on the corresponding model field to settle before resolving, whereas `peek` returns the value immediately:
|
|
213
204
|
|
|
214
205
|
```tsx
|
|
215
|
-
const get = {
|
|
216
|
-
friends: useResource(resource.friends),
|
|
217
|
-
};
|
|
218
|
-
|
|
219
206
|
actions.useAction(Actions.FetchFriends, async (context) => {
|
|
220
207
|
const name = await context.actions.resolution(Actions.Broadcast.Name);
|
|
221
208
|
if (!name) return;
|
|
222
|
-
const data = await
|
|
209
|
+
const data = await context.actions.resource(friends({ name }));
|
|
223
210
|
context.actions.produce(({ model }) => void (model.friends = data));
|
|
224
211
|
});
|
|
225
212
|
```
|
|
@@ -268,52 +255,42 @@ Components that mount after a broadcast has already been dispatched automaticall
|
|
|
268
255
|
|
|
269
256
|
<a id="remote-data"></a>
|
|
270
257
|
|
|
271
|
-
For remote data, declare a `Resource` at module scope and
|
|
258
|
+
For remote data, declare a `Resource` at module scope and use it directly. `user(params)` is the unified call form — it returns the sync cache read (`User | null`) and primes a slot that `context.actions.resource(user(params))` consumes for the fetch path (with auto-threaded abort controller and Store snapshot). Every successful fetch caches the response in a module-level slot keyed by the fetcher and the stringified params, so different param-sets are independent. Keep all resources in `resources.ts` and pull them in with named imports:
|
|
272
259
|
|
|
273
260
|
```ts
|
|
274
261
|
// resources.ts
|
|
275
262
|
import { Resource } from "march-hare";
|
|
276
263
|
|
|
277
|
-
export const user = Resource(() =>
|
|
264
|
+
export const user = Resource(({ controller }) =>
|
|
265
|
+
ky.get("/api/user", { signal: controller.signal }).json<User>(),
|
|
266
|
+
);
|
|
278
267
|
|
|
279
|
-
export const pay = Resource((
|
|
280
|
-
ky
|
|
268
|
+
export const pay = Resource<Receipt, Body>(({ controller, params }) =>
|
|
269
|
+
ky
|
|
270
|
+
.post("/api/pay", { json: params, signal: controller.signal })
|
|
271
|
+
.json<Receipt>(),
|
|
281
272
|
);
|
|
282
273
|
```
|
|
283
274
|
|
|
284
275
|
```tsx
|
|
285
276
|
// actions.ts
|
|
286
|
-
import { useActions
|
|
287
|
-
import
|
|
277
|
+
import { useActions } from "march-hare";
|
|
278
|
+
import { user, pay } from "./resources";
|
|
288
279
|
|
|
289
280
|
export function useActions() {
|
|
290
|
-
// Bind the resources first so we can seed the initial model from the
|
|
291
|
-
// cache via `.else(fallback)` — if a previous mount populated the
|
|
292
|
-
// slot, the model starts with that value; otherwise the fallback.
|
|
293
|
-
// Bindings are grouped by HTTP verb so call sites advertise read vs.
|
|
294
|
-
// write at a glance.
|
|
295
|
-
const get = {
|
|
296
|
-
user: useResource(resource.user),
|
|
297
|
-
};
|
|
298
|
-
const post = {
|
|
299
|
-
pay: useResource(resource.pay),
|
|
300
|
-
};
|
|
301
|
-
|
|
302
281
|
const actions = useActions<Model, typeof Actions>({
|
|
303
|
-
|
|
304
|
-
|
|
282
|
+
// Sync cache read at the model literal — returns null when nothing is cached.
|
|
283
|
+
user: user(),
|
|
284
|
+
receipt: null,
|
|
305
285
|
});
|
|
306
286
|
|
|
307
287
|
actions.useAction(Actions.Mount, async (context) => {
|
|
308
|
-
const data = await
|
|
309
|
-
{ over: { minutes: 5 } },
|
|
310
|
-
context.task.controller.signal,
|
|
311
|
-
);
|
|
288
|
+
const data = await context.actions.resource(user()).exceeds({ minutes: 5 });
|
|
312
289
|
context.actions.produce(({ model }) => void (model.user = data));
|
|
313
290
|
});
|
|
314
291
|
|
|
315
292
|
actions.useAction(Actions.Submit, async (context, body) => {
|
|
316
|
-
const receipt = await
|
|
293
|
+
const receipt = await context.actions.resource(pay(body));
|
|
317
294
|
context.actions.produce(({ model }) => void (model.receipt = receipt));
|
|
318
295
|
});
|
|
319
296
|
|
|
@@ -321,15 +298,17 @@ export function useActions() {
|
|
|
321
298
|
}
|
|
322
299
|
```
|
|
323
300
|
|
|
324
|
-
`
|
|
301
|
+
`context.actions.resource(invocation)` returns a thenable. Awaiting it fires the fetch unconditionally; chaining `.exceeds({ minutes: 5 })` short-circuits when the per-params cache age does not yet exceed the supplied freshness window. `.exceeds(duration)` accepts a `Temporal.Duration`, a `DurationLike` object, or an ISO 8601 duration string. `Temporal` is read from the host runtime – bring a polyfill (e.g. [`@js-temporal/polyfill`](https://github.com/js-temporal/temporal-polyfill)) if your target environment does not yet expose it natively.
|
|
325
302
|
|
|
326
|
-
`Resource` takes a single fetcher argument. The fetcher receives
|
|
303
|
+
`Resource` takes a single fetcher argument. The fetcher receives `{ store, controller, params }` — destructure whichever you need. There are no callbacks – no `onSuccess`, no `onError`, no injected `dispatch`. Side-effects after a run (broadcasting, analytics, model writes) live in the `useAction` handler that awaited the call, next to the rest of the flow:
|
|
327
304
|
|
|
328
305
|
```ts
|
|
329
|
-
export const user = Resource(() =>
|
|
306
|
+
export const user = Resource(({ controller }) =>
|
|
307
|
+
ky.get("/api/user", { signal: controller.signal }).json<User>(),
|
|
308
|
+
);
|
|
330
309
|
|
|
331
310
|
actions.useAction(Actions.Mount, async (context) => {
|
|
332
|
-
const data = await
|
|
311
|
+
const data = await context.actions.resource(user());
|
|
333
312
|
await context.actions.dispatch(Actions.Broadcast.UserUpdated, data);
|
|
334
313
|
context.actions.produce(({ model }) => void (model.user = data));
|
|
335
314
|
});
|
|
@@ -340,16 +319,18 @@ actions.useAction(Actions.Mount, async (context) => {
|
|
|
340
319
|
```ts
|
|
341
320
|
type Params = { cursor: string | null };
|
|
342
321
|
|
|
343
|
-
export const feed = Resource((
|
|
322
|
+
export const feed = Resource<Page<Item>, Params>(({ controller, params }) =>
|
|
344
323
|
http
|
|
345
|
-
.get("feed", {
|
|
324
|
+
.get("feed", {
|
|
325
|
+
searchParams: { cursor: params.cursor ?? "" },
|
|
326
|
+
signal: controller.signal,
|
|
327
|
+
})
|
|
346
328
|
.json<Page<Item>>(),
|
|
347
329
|
);
|
|
348
330
|
|
|
349
|
-
const
|
|
350
|
-
feed:
|
|
351
|
-
|
|
352
|
-
const page = await get.feed({ cursor: context.model.cursor });
|
|
331
|
+
const page = await context.actions.resource(
|
|
332
|
+
feed({ cursor: context.model.cursor }),
|
|
333
|
+
);
|
|
353
334
|
```
|
|
354
335
|
|
|
355
336
|
A complete IntersectionObserver-driven infinite-scroll demo lives at [`src/example/transactions/`](./src/example/transactions/) – mock paginated API, scroll-triggered `LoadMore`, `pending()` guard, broadcast on success.
|
|
@@ -359,7 +340,7 @@ For typed failure routing, wrap the call in `try/catch` and use `instanceof` &nd
|
|
|
359
340
|
```ts
|
|
360
341
|
actions.useAction(Actions.Mount, async (context) => {
|
|
361
342
|
try {
|
|
362
|
-
const data = await
|
|
343
|
+
const data = await context.actions.resource(user());
|
|
363
344
|
context.actions.produce(({ model }) => void (model.user = data));
|
|
364
345
|
} catch (error) {
|
|
365
346
|
if (error instanceof RateLimitedError) {
|
|
@@ -377,38 +358,42 @@ See the [Resource recipe](./recipes/use-resource.md) for the three-tier error ha
|
|
|
377
358
|
|
|
378
359
|
### Persisting resources across reloads
|
|
379
360
|
|
|
380
|
-
`
|
|
361
|
+
By default a `Resource`'s cache is in-memory only – it resets on every page load. To keep the most recent successful payload around between sessions, wire a `Cache` instance to the `Resource` definition. The Cache writes through to its adapter on every successful run and seeds the per-params slot from storage on first read, so call sites stay free of explicit `store.set` / `store.get` ceremony.
|
|
381
362
|
|
|
382
363
|
```ts
|
|
383
|
-
|
|
364
|
+
// resources.ts
|
|
365
|
+
import { Cache, Resource } from "march-hare";
|
|
384
366
|
|
|
385
|
-
|
|
367
|
+
const cache = Cache({
|
|
386
368
|
get: (key) => localStorage.getItem(key),
|
|
387
369
|
set: (key, value) => localStorage.setItem(key, value),
|
|
388
370
|
remove: (key) => localStorage.removeItem(key),
|
|
371
|
+
clear: () => localStorage.clear(),
|
|
389
372
|
});
|
|
390
|
-
```
|
|
391
373
|
|
|
392
|
-
|
|
374
|
+
export const cat = Resource(
|
|
375
|
+
async ({ controller }) => fetchCat(controller.signal),
|
|
376
|
+
cache,
|
|
377
|
+
);
|
|
378
|
+
```
|
|
393
379
|
|
|
394
380
|
```ts
|
|
395
|
-
|
|
381
|
+
// actions.ts
|
|
396
382
|
const actions = useActions<Model, typeof Actions>({
|
|
397
|
-
// First render reads
|
|
398
|
-
cat:
|
|
383
|
+
// First render reads the Cache automatically.
|
|
384
|
+
cat: cat(),
|
|
399
385
|
});
|
|
400
386
|
|
|
401
387
|
actions.useAction(Actions.Mount, async (context) => {
|
|
402
|
-
// Short-circuits when
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
context.task.controller.signal,
|
|
406
|
-
);
|
|
407
|
-
store.set(Snapshots.Cat, get.cat.snapshot());
|
|
388
|
+
// Short-circuits when the persisted payload is < 5 minutes old.
|
|
389
|
+
// The Cache writes through automatically on success.
|
|
390
|
+
const fresh = await context.actions.resource(cat()).exceeds({ minutes: 5 });
|
|
408
391
|
context.actions.produce(({ model }) => void (model.cat = fresh));
|
|
409
392
|
});
|
|
410
393
|
```
|
|
411
394
|
|
|
395
|
+
`Cache()` with no adapter is an in-memory scope – useful in tests or when you want a holdable cache without persistence. Per-params keying via `JSON.stringify(params)` is automatic, so `user({ id: 5 })` and `user({ id: 6 })` are distinct slots.
|
|
396
|
+
|
|
412
397
|
See the [storage recipe](./recipes/storage.md) for backend adapters (React Native MMKV, browser extension `chrome.storage`), sign-out purge, and the `unset` sentinel that keeps "nothing stored" distinct from "a legitimately stored null".
|
|
413
398
|
|
|
414
399
|
For targeted event delivery, use channeled actions. Define a channel type as the second generic argument and call the action with a channel object – handlers fire when the dispatch channel matches:
|
|
@@ -481,33 +466,41 @@ Unlike broadcast which reaches all mounted components, multicast is confined to
|
|
|
481
466
|
|
|
482
467
|
See the [multicast recipe](./recipes/multicast-actions.md) for more details.
|
|
483
468
|
|
|
484
|
-
For coordinating between async handlers without re-rendering the JSX tree, use the per-`<Boundary>`
|
|
469
|
+
For coordinating between async handlers and threading ambient values (session tokens, locale, feature flags, current operational mode) without re-rendering the JSX tree, use the per-`<Boundary>` `Store`. Declare your app's Store shape once via module augmentation, supply the initial value to `<Boundary store={...}>`, read via dot notation (`store.session`, `context.store.locale`), and write via `context.actions.produce(({ store }) => { ... })` — the same Immer-style recipe used for the model. Every `Resource` fetcher also receives a snapshot of the Store on its args object. Store is **not** reactive — drive view state through the model.
|
|
485
470
|
|
|
486
471
|
```ts
|
|
487
|
-
import { useActions
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
472
|
+
import { useActions } from "march-hare";
|
|
473
|
+
|
|
474
|
+
// Declare your Store's shape once. Every read/write is typed against this.
|
|
475
|
+
declare module "march-hare" {
|
|
476
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
|
477
|
+
interface Store {
|
|
478
|
+
session: Session | null;
|
|
479
|
+
operating: "idle" | "signing-out";
|
|
480
|
+
}
|
|
492
481
|
}
|
|
493
482
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
);
|
|
483
|
+
// Wire the initial Store into Boundary at app root.
|
|
484
|
+
<Boundary store={{ session: null, operating: "idle" }}>
|
|
485
|
+
<App />
|
|
486
|
+
</Boundary>;
|
|
487
|
+
|
|
488
|
+
export function useAuthActions() {
|
|
489
|
+
const actions = useActions<void, typeof Actions>();
|
|
502
490
|
|
|
503
491
|
actions.useAction(Actions.SignOut, async (context) => {
|
|
504
|
-
context.
|
|
492
|
+
context.actions.produce(({ store }) => {
|
|
493
|
+
store.operating = "signing-out";
|
|
494
|
+
});
|
|
505
495
|
await api.signOut();
|
|
506
|
-
context.
|
|
496
|
+
context.actions.produce(({ store }) => {
|
|
497
|
+
store.session = null;
|
|
498
|
+
store.operating = "idle";
|
|
499
|
+
});
|
|
507
500
|
});
|
|
508
501
|
|
|
509
502
|
actions.useAction(Actions.Refresh, async (context) => {
|
|
510
|
-
if (context.
|
|
503
|
+
if (context.store.operating === "signing-out") return;
|
|
511
504
|
// ...
|
|
512
505
|
});
|
|
513
506
|
|
|
@@ -1,29 +1,33 @@
|
|
|
1
|
-
import { HandlerPayload, BroadcastPayload, MulticastPayload, Distribution, Filter } from '../types/index
|
|
2
|
-
export { getActionSymbol, getLifecycleType, isBroadcastAction, isMulticastAction, getName, isChanneledAction, } from './utils
|
|
1
|
+
import { HandlerPayload, BroadcastPayload, MulticastPayload, Distribution, Filter } from '../types/index';
|
|
2
|
+
export { getActionSymbol, getLifecycleType, isBroadcastAction, isMulticastAction, getName, isChanneledAction, } from './utils';
|
|
3
3
|
/**
|
|
4
4
|
* Interface for the Action factory function.
|
|
5
5
|
*/
|
|
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.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ChanneledAction, AnyAction } from '../types/index
|
|
2
|
-
import { ActionId } from '../boundary/components/tasks/types
|
|
1
|
+
import { ChanneledAction, AnyAction } from '../types/index';
|
|
2
|
+
import { ActionId } from '../boundary/components/tasks/types';
|
|
3
3
|
/**
|
|
4
4
|
* Extracts the underlying symbol from an action or channeled action.
|
|
5
5
|
* This symbol is used as the event emitter key for dispatching.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Props } from './types
|
|
1
|
+
import { Props } from './types';
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
export { useBroadcast, BroadcastEmitter } from './utils
|
|
3
|
+
export { useBroadcast, BroadcastEmitter } from './utils';
|
|
4
4
|
/**
|
|
5
5
|
* Creates a new broadcast context for distributed actions. Only needed if you
|
|
6
6
|
* want to isolate a broadcast context, useful for libraries that want to provide
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Props } from './types
|
|
1
|
+
import { Props } from './types';
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
export { useConsumer } from './utils
|
|
4
|
-
export { Partition } from './components/partition/index
|
|
5
|
-
export type { Props as PartitionProps } from './components/partition/types
|
|
6
|
-
export type { ConsumerRenderer, Entry, ConsumerContext } from './types
|
|
3
|
+
export { useConsumer } from './utils';
|
|
4
|
+
export { Partition } from './components/partition/index';
|
|
5
|
+
export type { Props as PartitionProps } from './components/partition/types';
|
|
6
|
+
export type { ConsumerRenderer, Entry, ConsumerContext } from './types';
|
|
7
7
|
/**
|
|
8
8
|
* Creates a new consumer context for storing distributed action values. Only needed if you
|
|
9
9
|
* want to isolate a consumer context, useful for libraries that want to provide
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { MulticastPayload } from '../../../types/index
|
|
1
|
+
import { MulticastPayload } from '../../../types/index';
|
|
2
2
|
import { ComponentType, ReactNode } from 'react';
|
|
3
|
-
export { useScope, getScope } from './utils
|
|
4
|
-
export type { ScopeEntry, ScopeContext } from './types
|
|
3
|
+
export { useScope, getScope } from './utils';
|
|
4
|
+
export type { ScopeEntry, ScopeContext } from './types';
|
|
5
5
|
/**
|
|
6
6
|
* Higher-order component that opens a multicast scope keyed by the supplied
|
|
7
7
|
* multicast action. Components rendered inside the wrapped tree (and any
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { BroadcastEmitter } from '../broadcast/utils
|
|
2
|
-
import { ActionId } from '../tasks/types
|
|
1
|
+
import { BroadcastEmitter } from '../broadcast/utils';
|
|
2
|
+
import { ActionId } from '../tasks/types';
|
|
3
3
|
/**
|
|
4
4
|
* Represents a single scope in the ancestor chain. The scope key is the
|
|
5
5
|
* action id of the multicast action that opens the scope; each multicast
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ScopeContext, ScopeEntry } from './types
|
|
2
|
-
import { ActionId } from '../tasks/types
|
|
1
|
+
import { ScopeContext, ScopeEntry } from './types';
|
|
2
|
+
import { ActionId } from '../tasks/types';
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
/**
|
|
5
5
|
* React context for the scope chain.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Props } from './types';
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
export { useStore } from './utils';
|
|
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';
|
|
3
|
+
export type { Store } from './index';
|
|
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';
|
|
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>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Props } from './types
|
|
1
|
+
import { Props } from './types';
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
export type { Task } from './types
|
|
3
|
+
export type { Task } from './types';
|
|
4
4
|
/**
|
|
5
5
|
* Creates a new tasks context for action control. Only needed if you
|
|
6
6
|
* want to isolate a tasks context, useful for libraries that want to provide
|