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.
Files changed (53) hide show
  1. package/README.md +89 -96
  2. package/dist/{src/library/action → action}/index.d.ts +19 -15
  3. package/dist/{src/library/action → action}/utils.d.ts +2 -2
  4. package/dist/{src/library/boundary → boundary}/components/broadcast/index.d.ts +2 -2
  5. package/dist/{src/library/boundary → boundary}/components/broadcast/types.d.ts +1 -1
  6. package/dist/{src/library/boundary → boundary}/components/consumer/components/partition/index.d.ts +1 -1
  7. package/dist/{src/library/boundary → boundary}/components/consumer/components/partition/types.d.ts +1 -1
  8. package/dist/{src/library/boundary → boundary}/components/consumer/index.d.ts +5 -5
  9. package/dist/{src/library/boundary → boundary}/components/consumer/types.d.ts +1 -1
  10. package/dist/{src/library/boundary → boundary}/components/consumer/utils.d.ts +1 -1
  11. package/dist/{src/library/boundary → boundary}/components/scope/index.d.ts +3 -3
  12. package/dist/{src/library/boundary → boundary}/components/scope/types.d.ts +2 -2
  13. package/dist/{src/library/boundary → boundary}/components/scope/utils.d.ts +2 -2
  14. package/dist/boundary/components/store/index.d.ts +41 -0
  15. package/dist/boundary/components/store/types.d.ts +11 -0
  16. package/dist/boundary/components/store/utils.d.ts +64 -0
  17. package/dist/{src/library/boundary → boundary}/components/tasks/index.d.ts +2 -2
  18. package/dist/{src/library/boundary → boundary}/components/tasks/types.d.ts +3 -3
  19. package/dist/{src/library/boundary → boundary}/components/tasks/utils.d.ts +1 -1
  20. package/dist/boundary/index.d.ts +21 -0
  21. package/dist/boundary/types.d.ts +22 -0
  22. package/dist/cache/index.d.ts +44 -0
  23. package/dist/cache/types.d.ts +54 -0
  24. package/dist/{src/library/error → error}/types.d.ts +1 -1
  25. package/dist/{src/library/error → error}/utils.d.ts +1 -1
  26. package/dist/{src/library/hooks → hooks}/index.d.ts +3 -3
  27. package/dist/{src/library/hooks → hooks}/types.d.ts +3 -3
  28. package/dist/{src/library/hooks → hooks}/utils.d.ts +3 -3
  29. package/dist/index.d.ts +19 -0
  30. package/dist/march-hare.js +6 -6
  31. package/dist/march-hare.umd.cjs +1 -1
  32. package/dist/resource/index.d.ts +102 -0
  33. package/dist/resource/types.d.ts +27 -0
  34. package/dist/resource/utils.d.ts +37 -0
  35. package/dist/{src/library/types → types}/index.d.ts +162 -21
  36. package/dist/{src/library/utils → utils}/index.d.ts +3 -43
  37. package/dist/utils/types.d.ts +18 -0
  38. package/dist/{src/library/utils → utils}/utils.d.ts +1 -1
  39. package/package.json +2 -2
  40. package/dist/src/library/boundary/components/mode/index.d.ts +0 -15
  41. package/dist/src/library/boundary/components/mode/types.d.ts +0 -7
  42. package/dist/src/library/boundary/components/mode/utils.d.ts +0 -55
  43. package/dist/src/library/boundary/index.d.ts +0 -20
  44. package/dist/src/library/boundary/types.d.ts +0 -4
  45. package/dist/src/library/index.d.ts +0 -17
  46. package/dist/src/library/resource/index.d.ts +0 -65
  47. package/dist/src/library/resource/types.d.ts +0 -150
  48. package/dist/src/library/resource/utils.d.ts +0 -23
  49. package/dist/src/library/utils/types.d.ts +0 -101
  50. /package/dist/{src/library/annotate → annotate}/index.d.ts +0 -0
  51. /package/dist/{src/library/boundary → boundary}/components/broadcast/utils.d.ts +0 -0
  52. /package/dist/{src/library/error → error}/index.d.ts +0 -0
  53. /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 and consume it via `useResource`:
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(() => ky.get(api.user()).json<User>());
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
- const data = await get.user();
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
- () => ({ query: props.query }),
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
- await get.search({ query: context.data.query });
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 get.user();
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 get.friends({ name });
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 get.friends({ name });
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 consume it via `useResource`. Every call fires its own request; the most recent successful response is cached in a module-level `WeakMap` keyed by the fetcher so `.if(...)` and `.else(...)` on the bound handle have something to read from. Convention is to keep all resources in `resources.ts` and import them as a namespace:
258
+ For remote data, declare a `Resource` at module scope and use it directly. `user(params)` is the unified call form &mdash; 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(() => ky.get("/api/user").json<User>());
264
+ export const user = Resource(({ controller }) =>
265
+ ky.get("/api/user", { signal: controller.signal }).json<User>(),
266
+ );
278
267
 
279
- export const pay = Resource((signal, body: Body) =>
280
- ky.post("/api/pay", { json: body, signal }).json<Receipt>(),
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, useResource } from "march-hare";
287
- import * as resource from "./resources";
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
- user: get.user.else(null),
304
- receipt: post.pay.else(null),
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 get.user.if(
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 post.pay(context.task.controller.signal, body);
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
- `useResource(handle)` returns the fetch callable directly. The callable has two attached methods: `.if({ over })` skips the network when the cached payload is still fresh, and `.else(fallback)` reads the cached payload synchronously with a default. `Temporal` is read from the host runtime &ndash; 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. `.if({ over })` accepts a `Temporal.Duration`, a `DurationLike` object, or an ISO 8601 duration string.
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 &ndash; 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 the call-site `params` object as its only argument and returns a `Promise<T>`. There are no callbacks &ndash; 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:
303
+ `Resource` takes a single fetcher argument. The fetcher receives `{ store, controller, params }` &mdash; destructure whichever you need. There are no callbacks &ndash; 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(() => ky.get("/api/user").json<User>());
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 get.user();
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((signal, { cursor }: Params) =>
322
+ export const feed = Resource<Page<Item>, Params>(({ controller, params }) =>
344
323
  http
345
- .get("feed", { searchParams: { cursor: cursor ?? "" }, signal })
324
+ .get("feed", {
325
+ searchParams: { cursor: params.cursor ?? "" },
326
+ signal: controller.signal,
327
+ })
346
328
  .json<Page<Item>>(),
347
329
  );
348
330
 
349
- const get = {
350
- feed: useResource(resource.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/) &ndash; 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 get.user();
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
- `useResource`'s cache lives in a `WeakMap` that's reset on every page load. To keep the most recent successful payload around between sessions, pair it with `utils.store(...)` &ndash; a synchronous key/value wrapper around any backing store (`localStorage` on web, `MMKV` on React Native, etc.) that traffics in the same `Stored<T>` shape as the Resource cache.
361
+ By default a `Resource`'s cache is in-memory only &ndash; 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
- import { utils } from "march-hare";
364
+ // resources.ts
365
+ import { Cache, Resource } from "march-hare";
384
366
 
385
- export const store = utils.store({
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
- `get.cat.else(...)` is overloaded: pass a `Stored<T>` (from `store.get(key)`) and the bound handle seeds its own cache from it when empty &ndash; then chain `.else(null)` for the leaf fallback. `get.cat.snapshot()` produces the symmetric `Stored<T>` for writing back. After this single chain, `.if({ over })` short-circuits on the persisted timestamp on the _first_ mount after a reload, with no second method to call.
374
+ export const cat = Resource(
375
+ async ({ controller }) => fetchCat(controller.signal),
376
+ cache,
377
+ );
378
+ ```
393
379
 
394
380
  ```ts
395
- const get = { cat: useResource(resource.cat) };
381
+ // actions.ts
396
382
  const actions = useActions<Model, typeof Actions>({
397
- // First render reads cache storage → null.
398
- cat: get.cat.else(store.get(Snapshots.Cat)).else(null),
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 storage held a payload < 5 minutes old.
403
- const fresh = await get.cat.if(
404
- { over: { minutes: 5 } },
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 &ndash; 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 &ndash; 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>` mode handle returned by `useMode()`. Thread it through the `useActions` data callback so it shows up as `context.data.mode` inside handlers, fully typed. Mode is **not** reactive &mdash; drive view state through the model, not mode.
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 }) => { ... })` &mdash; 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 &mdash; drive view state through the model.
485
470
 
486
471
  ```ts
487
- import { useActions, useMode } from "march-hare";
488
-
489
- enum Mode {
490
- Idle,
491
- SigningOut,
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
- export function useActions() {
495
- const mode = useMode<Mode>();
496
- // Spell the data shape as the third generic so `context.data.mode` keeps
497
- // its concrete type inside handlers.
498
- const actions = useActions<Model, typeof Actions, { mode: typeof mode }>(
499
- model,
500
- () => ({ mode }),
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.data.mode.update(Mode.SigningOut);
492
+ context.actions.produce(({ store }) => {
493
+ store.operating = "signing-out";
494
+ });
505
495
  await api.signOut();
506
- context.data.mode.update(Mode.Idle);
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.data.mode.read() === Mode.SigningOut) return;
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.ts';
2
- export { getActionSymbol, getLifecycleType, isBroadcastAction, isMulticastAction, getName, isChanneledAction, } from './utils.ts';
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 (defaults to never).
11
- * @param name The action name, used for debugging purposes.
12
- * @returns A typed action object.
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: string): HandlerPayload<P, C>;
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: string, distribution: Distribution.Broadcast): BroadcastPayload<P, C>;
24
- <P = never, C extends Filter = never>(name: string, distribution: Distribution.Multicast): MulticastPayload<P, C>;
25
- <P = never, C extends Filter = never>(name: string, distribution: Distribution.Unicast): HandlerPayload<P, C>;
26
- <P = never, C extends Filter = never>(name: string, distribution: Distribution): HandlerPayload<P, C> | BroadcastPayload<P, C> | MulticastPayload<P, C>;
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.ts';
2
- import { ActionId } from '../boundary/components/tasks/types.ts';
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.ts';
1
+ import { Props } from './types';
2
2
  import * as React from "react";
3
- export { useBroadcast, BroadcastEmitter } from './utils.ts';
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,4 +1,4 @@
1
- import { BroadcastEmitter } from './utils.ts';
1
+ import { BroadcastEmitter } from './utils';
2
2
  import * as React from "react";
3
3
  /**
4
4
  * The broadcast context is a BroadcastEmitter used for distributed actions across components.
@@ -1,4 +1,4 @@
1
- import { Props } from './types.ts';
1
+ import { Props } from './types';
2
2
  import * as React from "react";
3
3
  /**
4
4
  * Renders output for the `stream()` method by subscribing to distributed action events.
@@ -1,4 +1,4 @@
1
- import { ConsumerRenderer } from '../../types.ts';
1
+ import { ConsumerRenderer } from '../../types';
2
2
  /**
3
3
  * Props for the Partition component.
4
4
  * @internal
@@ -1,9 +1,9 @@
1
- import { Props } from './types.ts';
1
+ import { Props } from './types';
2
2
  import * as React from "react";
3
- export { useConsumer } from './utils.ts';
4
- export { Partition } from './components/partition/index.tsx';
5
- export type { Props as PartitionProps } from './components/partition/types.ts';
6
- export type { ConsumerRenderer, Entry, ConsumerContext } from './types.ts';
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,5 +1,5 @@
1
1
  import { Inspect, State } from 'immertation';
2
- import { ActionId } from '../tasks/types.ts';
2
+ import { ActionId } from '../tasks/types';
3
3
  import * as React from "react";
4
4
  /**
5
5
  * Callback function for the stream() method.
@@ -1,4 +1,4 @@
1
- import { ConsumerContext } from './types.ts';
1
+ import { ConsumerContext } from './types';
2
2
  import * as React from "react";
3
3
  /**
4
4
  * React context for the consumer store.
@@ -1,7 +1,7 @@
1
- import { MulticastPayload } from '../../../types/index.ts';
1
+ import { MulticastPayload } from '../../../types/index';
2
2
  import { ComponentType, ReactNode } from 'react';
3
- export { useScope, getScope } from './utils.ts';
4
- export type { ScopeEntry, ScopeContext } from './types.ts';
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.ts';
2
- import { ActionId } from '../tasks/types.ts';
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.ts';
2
- import { ActionId } from '../tasks/types.ts';
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()` &mdash; 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 &mdash; 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 &mdash; 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
+ * &mdash; 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 &mdash; 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.ts';
1
+ import { Props } from './types';
2
2
  import * as React from "react";
3
- export type { Task } from './types.ts';
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