march-hare 0.12.1 → 0.13.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 (132) hide show
  1. package/README.md +66 -25
  2. package/dist/action/index.d.ts +2 -2
  3. package/dist/action/utils.d.ts +2 -2
  4. package/dist/actions/index.d.ts +2 -2
  5. package/dist/actions/types.d.ts +3 -3
  6. package/dist/actions/utils.d.ts +3 -3
  7. package/dist/app/index.d.ts +33 -87
  8. package/dist/app/types.d.ts +79 -26
  9. package/dist/boundary/components/broadcast/index.d.ts +2 -2
  10. package/dist/boundary/components/broadcast/types.d.ts +1 -1
  11. package/dist/boundary/components/consumer/components/partition/index.d.ts +1 -1
  12. package/dist/boundary/components/consumer/components/partition/types.d.ts +1 -1
  13. package/dist/boundary/components/consumer/index.d.ts +5 -5
  14. package/dist/boundary/components/consumer/types.d.ts +1 -1
  15. package/dist/boundary/components/consumer/utils.d.ts +1 -1
  16. package/dist/boundary/components/env/index.d.ts +3 -16
  17. package/dist/boundary/components/env/types.d.ts +24 -2
  18. package/dist/boundary/components/env/utils.d.ts +1 -1
  19. package/dist/boundary/components/scope/index.d.ts +2 -2
  20. package/dist/boundary/components/scope/types.d.ts +1 -1
  21. package/dist/boundary/components/scope/utils.d.ts +1 -1
  22. package/dist/boundary/components/sharing/index.d.ts +3 -3
  23. package/dist/boundary/components/tap/index.d.ts +3 -3
  24. package/dist/boundary/components/tap/types.d.ts +2 -2
  25. package/dist/boundary/components/tap/utils.d.ts +1 -1
  26. package/dist/boundary/components/tasks/index.d.ts +2 -2
  27. package/dist/boundary/components/tasks/utils.d.ts +1 -1
  28. package/dist/boundary/index.d.ts +3 -3
  29. package/dist/boundary/types.d.ts +3 -3
  30. package/dist/cache/index.d.ts +68 -12
  31. package/dist/cache/types.d.ts +33 -19
  32. package/dist/cli/bin/mh.js +10 -0
  33. package/dist/cli/lib/banner/index.js +14 -0
  34. package/dist/cli/lib/commands/app/index.js +37 -0
  35. package/dist/cli/lib/commands/feature/index.js +55 -0
  36. package/dist/cli/lib/commands/index.js +89 -0
  37. package/dist/cli/lib/commands/init/index.js +29 -0
  38. package/dist/cli/lib/commands/shared/index.js +56 -0
  39. package/dist/cli/lib/index.js +56 -0
  40. package/dist/cli/lib/parser/index.js +24 -0
  41. package/dist/cli/lib/prompt/index.js +61 -0
  42. package/dist/cli/lib/runner/index.js +46 -0
  43. package/dist/cli/lib/runner/types.js +1 -0
  44. package/dist/cli/lib/runner/utils.js +60 -0
  45. package/dist/cli/lib/types.js +1 -0
  46. package/dist/cli/lib/utils.js +20 -0
  47. package/dist/cli/templates/app/action/actions.ts.ejs.t +10 -0
  48. package/dist/cli/templates/app/action/types.ts.ejs.t +7 -0
  49. package/dist/cli/templates/app/integration/index.integration.tsx.ejs.t +13 -0
  50. package/dist/cli/templates/app/page/actions.ts.ejs.t +14 -0
  51. package/dist/cli/templates/app/page/index.tsx.ejs.t +20 -0
  52. package/dist/cli/templates/app/page/styles.ts.ejs.t +35 -0
  53. package/dist/cli/templates/app/page/types.ts.ejs.t +12 -0
  54. package/dist/cli/templates/feature/action/actions.ts.ejs.t +10 -0
  55. package/dist/cli/templates/feature/action/types.ts.ejs.t +7 -0
  56. package/dist/cli/templates/feature/multicast/types.ts.ejs.t +7 -0
  57. package/dist/cli/templates/feature/presentational/index.tsx.ejs.t +14 -0
  58. package/dist/cli/templates/feature/presentational/types.ts.ejs.t +12 -0
  59. package/dist/cli/templates/feature/presentational/utils.ts.ejs.t +8 -0
  60. package/dist/cli/templates/feature/stateful/actions.ts.ejs.t +16 -0
  61. package/dist/cli/templates/feature/stateful/index.tsx.ejs.t +19 -0
  62. package/dist/cli/templates/feature/stateful/types.ts.ejs.t +16 -0
  63. package/dist/cli/templates/feature/stateful/utils.ts.ejs.t +8 -0
  64. package/dist/cli/templates/feature/unit/index.test.tsx.ejs.t +21 -0
  65. package/dist/cli/templates/init/new/README.md.ejs.t +48 -0
  66. package/dist/cli/templates/init/new/eslint.config.js.ejs.t +88 -0
  67. package/dist/cli/templates/init/new/gitignore.ejs.t +9 -0
  68. package/dist/cli/templates/init/new/index.html.ejs.t +18 -0
  69. package/dist/cli/templates/init/new/package.json.ejs.t +54 -0
  70. package/dist/cli/templates/init/new/playwright.config.ts.ejs.t +17 -0
  71. package/dist/cli/templates/init/new/prettierrc.ejs.t +8 -0
  72. package/dist/cli/templates/init/new/src.app.index.tsx.ejs.t +14 -0
  73. package/dist/cli/templates/init/new/src.app.pages.home.actions.ts.ejs.t +16 -0
  74. package/dist/cli/templates/init/new/src.app.pages.home.index.tsx.ejs.t +30 -0
  75. package/dist/cli/templates/init/new/src.app.pages.home.integration.tsx.ejs.t +28 -0
  76. package/dist/cli/templates/init/new/src.app.pages.home.styles.ts.ejs.t +45 -0
  77. package/dist/cli/templates/init/new/src.app.pages.home.types.ts.ejs.t +12 -0
  78. package/dist/cli/templates/init/new/src.app.utils.ts.ejs.t +9 -0
  79. package/dist/cli/templates/init/new/src.features.greet.actions.ts.ejs.t +20 -0
  80. package/dist/cli/templates/init/new/src.features.greet.index.test.tsx.ejs.t +21 -0
  81. package/dist/cli/templates/init/new/src.features.greet.index.tsx.ejs.t +24 -0
  82. package/dist/cli/templates/init/new/src.features.greet.types.ts.ejs.t +18 -0
  83. package/dist/cli/templates/init/new/src.features.greet.utils.ts.ejs.t +8 -0
  84. package/dist/cli/templates/init/new/src.index.tsx.ejs.t +8 -0
  85. package/dist/cli/templates/init/new/src.shared.components.button.index.test.tsx.ejs.t +13 -0
  86. package/dist/cli/templates/init/new/src.shared.components.button.index.tsx.ejs.t +10 -0
  87. package/dist/cli/templates/init/new/src.shared.components.button.types.ts.ejs.t +6 -0
  88. package/dist/cli/templates/init/new/src.shared.resources.index.ts.ejs.t +4 -0
  89. package/dist/cli/templates/init/new/src.shared.theme.index.ts.ejs.t +51 -0
  90. package/dist/cli/templates/init/new/src.shared.types.index.ts.ejs.t +23 -0
  91. package/dist/cli/templates/init/new/src.test-setup.ts.ejs.t +10 -0
  92. package/dist/cli/templates/init/new/src.vite-env.d.ts.ejs.t +4 -0
  93. package/dist/cli/templates/init/new/tests.home.e2e.ts.ejs.t +14 -0
  94. package/dist/cli/templates/init/new/tsconfig.json.ejs.t +29 -0
  95. package/dist/cli/templates/init/new/vite.config.ts.ejs.t +17 -0
  96. package/dist/cli/templates/init/new/vitest.config.ts.ejs.t +24 -0
  97. package/dist/cli/templates/shared/component/index.tsx.ejs.t +9 -0
  98. package/dist/cli/templates/shared/component/types.ts.ejs.t +8 -0
  99. package/dist/cli/templates/shared/resource/index.ts.ejs.t +15 -0
  100. package/dist/cli/templates/shared/resource/types.ts.ejs.t +10 -0
  101. package/dist/cli/templates/shared/type-broadcast/types.ts.ejs.t +7 -0
  102. package/dist/cli/templates/shared/type-payload/types.ts.ejs.t +9 -0
  103. package/dist/cli/templates/shared/unit-component/index.test.tsx.ejs.t +13 -0
  104. package/dist/cli/templates/shared/unit-resource/index.test.ts.ejs.t +15 -0
  105. package/dist/cli/templates/shared/unit-util/index.test.ts.ejs.t +11 -0
  106. package/dist/cli/templates/shared/util/index.ts.ejs.t +6 -0
  107. package/dist/coalesce/index.d.ts +1 -1
  108. package/dist/context/index.d.ts +2 -2
  109. package/dist/error/index.d.ts +18 -1
  110. package/dist/error/types.d.ts +1 -18
  111. package/dist/error/utils.d.ts +1 -1
  112. package/dist/index.d.ts +16 -14
  113. package/dist/march-hare.js +7 -6
  114. package/dist/march-hare.js.map +1 -0
  115. package/dist/march-hare.umd.cjs +2 -1
  116. package/dist/march-hare.umd.cjs.map +1 -0
  117. package/dist/resource/index.d.ts +32 -61
  118. package/dist/resource/types.d.ts +45 -22
  119. package/dist/resource/utils.d.ts +31 -3
  120. package/dist/scope/index.d.ts +4 -64
  121. package/dist/scope/types.d.ts +8 -8
  122. package/dist/scope/utils.d.ts +12 -0
  123. package/dist/shared/index.d.ts +12 -21
  124. package/dist/types/index.d.ts +114 -29
  125. package/dist/utils/index.d.ts +3 -3
  126. package/dist/utils/types.d.ts +1 -3
  127. package/dist/utils/utils.d.ts +1 -3
  128. package/dist/with/index.d.ts +17 -62
  129. package/dist/with/types.d.ts +66 -0
  130. package/dist/with/utils.d.ts +61 -0
  131. package/package.json +21 -4
  132. package/src/cli/README.md +314 -0
package/README.md CHANGED
@@ -27,8 +27,9 @@
27
27
  1. [Multicast actions](#multicast-actions)
28
28
  1. [Global data](#global-data)
29
29
  1. [Reusable components](#reusable-components)
30
+ 1. [Scaffolding CLI](#scaffolding-cli)
30
31
 
31
- For advanced topics, see the [recipes directory](./recipes/).
32
+ For advanced topics, see the [recipes directory](./recipes/). For a worked end-to-end example with the FSD layout, see [`src/example/`](./src/example/README.md). To scaffold a new project that mirrors that example, see [`src/cli/`](./src/cli/README.md).
32
33
 
33
34
  ## Benefits
34
35
 
@@ -40,7 +41,7 @@ For advanced topics, see the [recipes directory](./recipes/).
40
41
  - Reduces context proliferation – events replace many contexts.
41
42
  - No need to memoize callbacks – handlers are stable references with fresh closure access.
42
43
  - Clear separation between business logic and markup.
43
- - Complements [Feature Slice Design](https://feature-sliced.design/) architecture.
44
+ - Complements [Feature Sliced Design](https://feature-sliced.design/) architecture — **App = host, Scope = feature**; see [Reusable components](#reusable-components).
44
45
  - Strongly typed dispatches, models, payloads, etc.
45
46
  - Built-in request cancellation with `AbortController`.
46
47
  - Granular async state tracking per model field.
@@ -393,7 +394,12 @@ Components that mount after a broadcast has already been dispatched automaticall
393
394
 
394
395
  ## Resource handling
395
396
 
396
- For remote data, declare an `app.Resource` at module scope. `resource.user(params)` is the unified call form &mdash; it returns the sync cache read (`User | null`) and primes a slot that `context.actions.resource(resource.user(params))` consumes for the fetch path (with auto-threaded abort controller and a live handle to the per-`<Boundary>` Env). 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 the whole module in as a namespace (`import * as resource from "./resources"`):
397
+ For remote data, declare an `app.Resource` at module scope. The resulting handle has two call forms:
398
+
399
+ - `resource.user.get(params)` &mdash; synchronous cache read, returns `User | null`. Use it in model literals, JSX, or anywhere you need the cached value without triggering a fetch.
400
+ - `resource.user(params)` &mdash; produces an `Invocation` you pass to `context.actions.resource(...)` for the fetch path (with auto-threaded abort controller and a live handle to the per-`<Boundary>` Env).
401
+
402
+ 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 the whole module in as a namespace (`import * as resource from "./resources"`):
397
403
 
398
404
  ```ts
399
405
  // resources.ts
@@ -430,7 +436,7 @@ function useActions() {
430
436
  const context = app.useContext<Model, typeof Actions>();
431
437
  const actions = context.useActions({
432
438
  // Sync cache read at the model literal — returns null when nothing is cached.
433
- user: resource.user(),
439
+ user: resource.user.get(),
434
440
  receipt: null,
435
441
  });
436
442
 
@@ -536,21 +542,28 @@ actions.useAction(Actions.Mount, async (context) => {
536
542
 
537
543
  See the [Resource recipe](./recipes/use-resource.md) for the three-tier error handling model, parameterised resources, and limitations.
538
544
 
539
- By default an `app.Resource`'s cache is in-memory only &ndash; it resets on every page load. To keep the most recent successful payload around between sessions, switch to `app.Resource.Cachable(cache, fetcher)`. The cache is the **first** argument &mdash; persistence is the headline of this form, the fetcher is the operation. Every successful fetch writes through to the Cache; first reads via the call form auto-seed from the Cache's adapter:
545
+ By default an `app.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` into `App({ cache })`. Every `app.Resource` declared on that App writes through to the shared Cache and seeds from it on the next reload; resources are namespaced internally so they don't collide on shared params keys:
540
546
 
541
547
  ```ts
542
- // resources.ts
543
- import { Cache } from "march-hare";
544
- import { app } from "./app";
548
+ // app.ts
549
+ import { App, Cache } from "march-hare";
545
550
 
546
- const cache = Cache({
547
- get: (key) => localStorage.getItem(key),
548
- set: (key, value) => localStorage.setItem(key, value),
549
- remove: (key) => localStorage.removeItem(key),
550
- clear: () => localStorage.clear(),
551
+ export const app = App({
552
+ env: { session: null as Session | null },
553
+ cache: Cache({
554
+ get: (key) => localStorage.getItem(key),
555
+ set: (key, value) => localStorage.setItem(key, value),
556
+ remove: (key) => localStorage.removeItem(key),
557
+ clear: () => localStorage.clear(),
558
+ }),
551
559
  });
560
+ ```
561
+
562
+ ```ts
563
+ // resources.ts
564
+ import { app } from "./app";
552
565
 
553
- export const cat = app.Resource.Cachable(cache, (context) =>
566
+ export const cat = app.Resource((context) =>
554
567
  fetchCat(context.controller.signal),
555
568
  );
556
569
  ```
@@ -571,7 +584,7 @@ function useActions() {
571
584
  const context = app.useContext<Model, typeof Actions>();
572
585
  const actions = context.useActions({
573
586
  // First render reads the Cache automatically.
574
- cat: resource.cat(),
587
+ cat: resource.cat.get(),
575
588
  });
576
589
 
577
590
  actions.useAction(Actions.Mount, async (context) => {
@@ -594,7 +607,9 @@ export default function CatCard(): React.ReactElement {
594
607
 
595
608
  `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.
596
609
 
597
- 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".
610
+ The adapter contract is **strictly synchronous** &ndash; `get` / `set` / `remove` / `clear` all return immediately, with no `Promise`. The model-literal read (`{ user: resource.user.get() }`) is evaluated during render and has no place to wait. React Native projects should use [`react-native-mmkv`](https://github.com/mrousavy/react-native-mmkv), which is sync out of the box and drops straight into the contract; `AsyncStorage` is incompatible. Truly async backends (IndexedDB, `chrome.storage.local`) need a sync facade hydrated at app entry &ndash; see the [storage recipe](./recipes/storage.md).
611
+
612
+ See the [storage recipe](./recipes/storage.md) for backend adapters (React Native `react-native-mmkv`, browser `localStorage`, browser extension `chrome.storage`), sign-out purge, and the `unset` sentinel that keeps "nothing stored" distinct from "a legitimately stored null".
598
613
 
599
614
  ## Channeled actions
600
615
 
@@ -703,7 +718,7 @@ A few rules worth knowing:
703
718
  - **No `scope.Scope()`.** The handle deliberately omits a nested factory. Open another scope by calling `app.Scope<...>()` again and rendering its `<Boundary>` &mdash; that way the multicast surface stays declared at the call site.
704
719
  - **Replay on late-mount is per-scope.** Like broadcast, multicast caches its most recent payload per action symbol; components that mount later inside the same boundary pick up the cached value through their `useAction` handler. See the [mount deduplication recipe](./recipes/mount-broadcast-deduplication.md) if you also fetch in `Lifecycle.Mount()`.
705
720
 
706
- See the [multicast recipe](./recipes/multicast-actions.md) for more details.
721
+ See the [multicast recipe](./recipes/multicast-actions.md) for more details. When the scope itself needs to be reusable across multiple hosts, reach for `shared.Scope<HostEnvs, typeof MulticastActions>()` &mdash; the standalone form covered in [Reusable components](#reusable-components). The rule of thumb: never reach for a second `App()` to get a private channel; that's what multicast scopes exist for.
707
722
 
708
723
  ## Global data
709
724
 
@@ -800,15 +815,16 @@ export const app = App();
800
815
 
801
816
  ## Reusable components
802
817
 
818
+ > **App = host, Scope = feature.** One `App<HostEnv>()` per deployable; everything inside it is a component. A component that needs a private channel reaches for `shared.Scope<HostEnvs, _>()`, never another `App()`. A component that runs under more than one `App` reaches for `shared.useContext<HostEnvs, M, A>()` instead of binding to a specific `app`. That one rule keeps the dependency graph acyclic, lets cross-cutting state (session, locale, permissions) live in a single Env, and gives [Feature Sliced Design](https://feature-sliced.design/) a 1:1 runtime expression &mdash; shared layer reuses `shared.X` against a `HostEnvs` union, features open `shared.Scope`s, hosts declare the App.
819
+
803
820
  Importing `app` from a single location is fine inside a feature, but it breaks when a component needs to run under **more than one** `App` &mdash; a shared `<Profile />` used by both a web app and a mobile shell, for example. For that case, every `app.X` factory has a **standalone counterpart** on the `shared` namespace that takes the Env shape `E` as its mandatory first generic:
804
821
 
805
- | Bound to an App | Standalone (`shared.X`) |
806
- | ---------------------------- | ---------------------------------------- |
807
- | `app.useContext<M, A, D>()` | `shared.useContext<E, M, A, D>()` |
808
- | `app.useEnv()` | `shared.useEnv<E>()` |
809
- | `app.Resource<T, P>(...)` | `shared.Resource<E, T, P>(...)` |
810
- | `app.Resource.Cachable(...)` | `shared.Resource.Cachable<E, T, P>(...)` |
811
- | `app.Scope<A>()` | `shared.Scope<E, A>()` |
822
+ | Bound to an App | Standalone (`shared.X`) |
823
+ | --------------------------- | --------------------------------- |
824
+ | `app.useContext<M, A, D>()` | `shared.useContext<E, M, A, D>()` |
825
+ | `app.useEnv()` | `shared.useEnv<E>()` |
826
+ | `app.Resource<T, P>(...)` | `shared.Resource<E, T, P>(...)` |
827
+ | `app.Scope<A>()` | `shared.Scope<E, A>()` |
812
828
 
813
829
  The standalone forms take the same runtime path as the App-bound ones &mdash; `E` is purely a type-level binding the caller supplies so reusable code stays App-agnostic.
814
830
 
@@ -876,6 +892,31 @@ function Where(): React.ReactElement {
876
892
  }
877
893
  ```
878
894
 
879
- `shared.Resource<E, T, P>` and `shared.Resource.Cachable<E, T, P>` are the same story for shared resources &mdash; declare them at module scope, pass the Env union as the first generic, and the fetcher's `context.env` is typed against it. `shared.Scope<E, A>()` opens a multicast scope without going through an App handle. See the [reusable components recipe](./recipes/reusable-components.md) for the full pattern including discriminator-keyed switches and the `App()`-with-no-env case.
895
+ `shared.Resource<E, T, P>` is the same story for shared resources &mdash; declare them at module scope, pass the Env union as the first generic, and the fetcher's `context.env` is typed against it. Shared resources always use an isolated in-memory cache; reach for `app.Resource` when persistence is required, since the cache is wired into the App via `App({ cache })`. `shared.Scope<E, A>()` opens a multicast scope without going through an App handle. See the [reusable components recipe](./recipes/reusable-components.md) for the full pattern including discriminator-keyed switches and the `App()`-with-no-env case.
896
+
897
+ When a reusable component or resource is genuinely Env-agnostic &mdash; the fetcher never touches `context.env`, the hook never calls `shared.useEnv` &mdash; pass `Envless` as `E` instead of spelling out `Record<never, never>`: `shared.Resource<Envless, T>`, `shared.useContext<Envless, M, A>()`. It's a named alias for the empty-record shape exported from `march-hare`, kept around purely for legibility at the call site.
880
898
 
881
899
  For one-line handler binding &mdash; flipping a boolean, assigning a payload to a leaf, pinning a field to a fixed value &mdash; reach for `context.with.{update,invert,always}`. See the [`With` helpers recipe](./recipes/with-helpers.md) for the full surface.
900
+
901
+ ## Scaffolding CLI
902
+
903
+ A [Hygen](https://github.com/jondot/hygen)-style scaffolder ships under [`src/cli/`](./src/cli/) as the `mh` binary. It mirrors the layout of [`src/example/`](./src/example/) and the FSD layering rules enforced by `eslint-plugin-boundaries` &mdash; imports flow strictly downward (`app → features → shared`).
904
+
905
+ ```bash
906
+ cd src/cli
907
+ npm install
908
+ npm link # creates the global `mh` binary
909
+ ```
910
+
911
+ Run it with no arguments for an interactive menu, or drive any leaf command directly:
912
+
913
+ ```bash
914
+ mh # banner + interactive menu
915
+ mh init my-project # bootstrap a new project
916
+ mh feature new add-cat # add a stateful feature
917
+ mh app new dashboard # add a page
918
+ mh shared component card # add a shared component
919
+ mh feature action counter Reset # inject an Action + handler stub
920
+ ```
921
+
922
+ Every command lives in a tree &mdash; typing `mh feature` opens a sub-menu, typing `mh feature new` prompts for a name, typing `mh feature new add-cat` runs non-interactively. See [`src/cli/README.md`](./src/cli/README.md) for the full command surface, the template format, and instructions for adding your own generators.
@@ -1,5 +1,5 @@
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.js';
2
+ export { getActionSymbol, getLifecycleType, isBroadcastAction, isMulticastAction, getName, isChanneledAction, } from './utils.js';
3
3
  /**
4
4
  * Interface for the Action factory function.
5
5
  */
@@ -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.js';
2
+ import { ActionId } from '../boundary/components/tasks/types.js';
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,5 +1,5 @@
1
- import { Data } from './types';
2
- import { Model, Props, Actions, UseActions } from '../types/index';
1
+ import { Data } from './types.js';
2
+ import { Model, Props, Actions, UseActions } from '../types/index.js';
3
3
  /**
4
4
  * A hook for managing state with actions.
5
5
  *
@@ -1,8 +1,8 @@
1
1
  import { default as EventEmitter } from 'eventemitter3';
2
2
  import { RefObject } from 'react';
3
- import { Model, HandlerContext, Actions, Props, Tasks, ActionId, Phase, Filter } from '../types/index';
4
- import { BroadcastEmitter } from '../boundary/components/broadcast/utils';
5
- import { ScopeContext } from '../boundary/components/scope/types';
3
+ import { Model, HandlerContext, Actions, Props, Tasks, ActionId, Phase, Filter } from '../types/index.js';
4
+ import { BroadcastEmitter } from '../boundary/components/broadcast/utils.js';
5
+ import { ScopeContext } from '../boundary/components/scope/types.js';
6
6
  /**
7
7
  * Function signature for action handlers registered via `useAction`.
8
8
  * Receives the reactive context and payload, returning void or a promise/generator.
@@ -1,8 +1,8 @@
1
1
  import { RefObject } from 'react';
2
- import { Props, Model, Actions, Filter, ActionId, HandlerPayload, ChanneledAction, HandlerContext } from '../types/index';
2
+ import { Props, Model, Actions, Filter, ActionId, HandlerPayload, ChanneledAction, HandlerContext } from '../types/index.js';
3
3
  import { default as EventEmitter } from 'eventemitter3';
4
- import { Dispatchers, LifecycleConfig, Scope } from './types';
5
- import { isChanneledAction, getActionSymbol } from '../action/index';
4
+ import { Dispatchers, LifecycleConfig, Scope } from './types.js';
5
+ import { isChanneledAction, getActionSymbol } from '../action/index.js';
6
6
  import * as React from "react";
7
7
  /**
8
8
  * Creates a new object with getters for each property of the input object.
@@ -1,67 +1,13 @@
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';
1
+ import { Env } from '../boundary/components/env/types.js';
2
+ import { Cache } from '../cache/index.js';
3
+ import { Actions, Model, Props } from '../types/index.js';
4
+ import { AppHandle, AppContextHandle } from './types.js';
5
+ import { Tap } from '../boundary/components/tap/types.js';
6
+ export type { AppArgs, AppContextHandle, AppFetcher, AppResource, } from './types.js';
7
+ export type { AppHandle } from './types.js';
8
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. Wraps the subtree with the `env`
15
- * and `tap` declared on {@link App} &mdash; both are fixed at App
16
- * construction time and cannot be overridden at the render site.
17
- * Runtime mutations to the Env flow through
18
- * `context.actions.produce(({ env }) => { ... })`; if a test or
19
- * storybook needs a different initial Env, declare a separate `App`.
20
- */
21
- readonly Boundary: React.FC<{
22
- children: React.ReactNode;
23
- }>;
24
- /**
25
- * Hook returning a stable `Context` handle. The handle's
26
- * `context.useActions(model?, getData?)` materialises the
27
- * component's `[model, actions, data]` tuple. Every handler's
28
- * `context.env` is typed as `S`.
29
- */
30
- readonly useContext: <M extends Model | void = void, AC extends Actions | void = void, D extends Props = Props>() => AppContextHandle<M, AC, D, S>;
31
- /**
32
- * Read-only Proxy over the per-Boundary Env, typed as `S`. Reads use
33
- * dot notation (`env.session`) and always reflect the latest value
34
- * across `await` boundaries. Writes flow through
35
- * `context.actions.produce(({ env }) => { ... })`.
36
- */
37
- readonly useEnv: () => Readonly<S>;
38
- /**
39
- * `Resource` factory bound to this App's Env. Same shape as the
40
- * top-level `Resource`: call directly for an in-memory cache, or use
41
- * `app.Resource.Cachable(cache, fetcher)` for persistence.
42
- */
43
- readonly Resource: AppResource<S>;
44
- /**
45
- * Opens a typed multicast scope. The generic `MulticastActions` declares
46
- * the `Distribution.Multicast` action class (or union of classes)
47
- * whose dispatches are routed through this scope &mdash; the
48
- * returned handle mirrors the App surface but widens
49
- * `useContext().actions.dispatch` to accept actions from `MulticastActions`
50
- * on top of the local `AC` class.
51
- *
52
- * Render `<scope.Boundary>` to open the scope at runtime; nesting
53
- * multiple boundaries from different `app.Scope()` calls is fine,
54
- * each runs as an independent emitter shadowed for its subtree.
55
- *
56
- * The Scope handle deliberately does NOT expose a further `Scope`
57
- * method &mdash; the multicast surface must be declared at the
58
- * `app.Scope<MulticastActions>()` call site so the type union is explicit.
59
- */
60
- readonly Scope: <MulticastActions>() => Scope<S, MulticastActions>;
61
- };
62
- /**
63
- * Creates an `App` &mdash; the entrypoint for a typed Env shape `S`,
64
- * inferred from `config.env`. `App<S>` exposes `Boundary`, hooks, and
9
+ * Creates an `App` &mdash; the entrypoint for a typed Env shape `E`,
10
+ * inferred from `config.env`. `App<E>` exposes `Boundary`, hooks, and
65
11
  * a `Resource` factory all wired against the same shape.
66
12
  *
67
13
  * Each `<app.Boundary>` instance owns its own Env, so different `App`s
@@ -69,15 +15,22 @@ export type App<S extends object> = {
69
15
  *
70
16
  * Pass `tap` to subscribe to every action handler's dispatch / settle /
71
17
  * error inside the boundary &mdash; useful for analytics, audit logging,
72
- * Sentry breadcrumbs. See `recipes/tap.md`. Both `env` and `tap` are
73
- * fixed at `App()` time; `<app.Boundary>` does not accept overrides.
74
- * Mutate the live Env through `context.actions.produce(({ env }) => …)`,
75
- * and declare a separate `App` when a test or storybook needs a
76
- * different initial value.
18
+ * Sentry breadcrumbs. See `recipes/tap.md`. Pass `cache` to persist
19
+ * every `app.Resource(fetcher)` declaration through a single
20
+ * {@link Cache} &mdash; each resource is namespaced inside the cache by
21
+ * its declaration order, so reloads seed from storage automatically and
22
+ * resources do not collide on shared params keys. Omit `cache` to keep
23
+ * each resource's payloads in an isolated in-memory slot.
24
+ *
25
+ * `env`, `tap`, and `cache` are all fixed at `App()` time;
26
+ * `<app.Boundary>` does not accept overrides. Mutate the live Env
27
+ * through `context.actions.produce(({ env }) => …)`, and declare a
28
+ * separate `App` when a test or storybook needs a different initial
29
+ * value.
77
30
  *
78
31
  * @example
79
32
  * ```tsx
80
- * import { App, type Taps } from "march-hare";
33
+ * import { App, Cache, type Taps } from "march-hare";
81
34
  *
82
35
  * type Session = { accessToken: string };
83
36
  *
@@ -93,6 +46,12 @@ export type App<S extends object> = {
93
46
  * operating: "idle" as "idle" | "signing-out",
94
47
  * },
95
48
  * tap,
49
+ * cache: Cache({
50
+ * get: (key) => localStorage.getItem(key),
51
+ * set: (key, value) => localStorage.setItem(key, value),
52
+ * remove: (key) => localStorage.removeItem(key),
53
+ * clear: () => localStorage.clear(),
54
+ * }),
96
55
  * });
97
56
  *
98
57
  * // Root render.
@@ -100,21 +59,7 @@ export type App<S extends object> = {
100
59
  * <Root />
101
60
  * </app.Boundary>;
102
61
  *
103
- * // In a feature's actions.ts:
104
- * export function useAuthActions() {
105
- * const context = app.useContext<void, typeof Actions>();
106
- * const actions = context.useActions();
107
- *
108
- * actions.useAction(Actions.SignOut, async (context) => {
109
- * context.actions.produce(({ env }) => {
110
- * env.session = null;
111
- * });
112
- * });
113
- *
114
- * return actions;
115
- * }
116
- *
117
- * // In resources.ts:
62
+ * // In resources.ts &mdash; persisted via the App's cache.
118
63
  * export const user = app.Resource<User>((context) =>
119
64
  * ky
120
65
  * .get("/api/user", {
@@ -127,10 +72,11 @@ export type App<S extends object> = {
127
72
  * );
128
73
  * ```
129
74
  */
130
- export declare function App<S extends object = Env>(config?: {
131
- env?: S;
75
+ export declare function App<E extends object = Env>(config?: {
76
+ env?: E;
132
77
  tap?: Tap;
133
- }): App<S>;
78
+ cache?: Cache;
79
+ }): AppHandle<E>;
134
80
  /**
135
81
  * Standalone counterpart to `app.useContext`, exported as
136
82
  * `shared.useContext` &mdash; same call shape, but takes the **Env
@@ -1,47 +1,100 @@
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';
1
+ import { Args, ResourceHandle } from '../resource/types.js';
2
+ import { Actions, Context, Model, Props, UseActions } from '../types/index.js';
3
+ import { Data } from '../actions/types.js';
4
+ import { Env } from '../boundary/components/env/types.js';
5
+ import { WithHandle } from '../with/types.js';
7
6
  /**
8
7
  * Args object passed to an `app.Resource` fetcher. Same shape as the
9
- * base `Resource` fetcher's args but with `env` typed as `S`.
8
+ * base `Resource` fetcher's args but with `env` typed as `E`.
10
9
  */
11
- export type AppArgs<S, P extends object = Record<never, never>> = Omit<Args<P>, "env"> & {
12
- readonly env: Readonly<S>;
10
+ export type AppArgs<E, P extends object = Record<never, never>> = Omit<Args<P>, "env"> & {
11
+ readonly env: Readonly<E>;
13
12
  };
14
13
  /**
15
14
  * Fetcher signature for an `app.Resource` declaration. The fetcher's
16
- * `context.env` is typed against the App's inferred Env shape `S`.
15
+ * `context.env` is typed against the App's inferred Env shape `E`.
17
16
  */
18
- export type AppFetcher<S, T, P extends object = Record<never, never>> = (context: AppArgs<S, P>) => Promise<T>;
17
+ export type AppFetcher<E, T, P extends object = Record<never, never>> = (context: AppArgs<E, P>) => Promise<T>;
19
18
  /**
20
- * `app.Resource(fetcher)` &mdash; in-memory cache, no persistence.
21
- * `app.Resource.Cachable(cache, fetcher)` &mdash; persistent cache wired
22
- * to the supplied `Cache` adapter. Both forms type `context.env` as
23
- * the App's Env shape.
19
+ * `app.Resource(fetcher)` declares a remote interaction bound to the
20
+ * App's Env shape. Cache behaviour is decided at App construction:
21
+ * pass `App({ cache })` to share a single {@link Cache} (typically
22
+ * backed by `localStorage`/MMKV) across every resource on the App, or
23
+ * omit it to keep each resource's payloads in an isolated in-memory
24
+ * slot.
24
25
  */
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
- };
26
+ export type AppResource<E> = <T, P extends object = Record<never, never>>(fetcher: AppFetcher<E, T, P>) => ResourceHandle<T, P>;
29
27
  /**
30
28
  * Tuple shape returned by `context.useActions(...)` on an App-bound
31
- * Context. Re-exports the base {@link UseActions} with the App's `S`
29
+ * Context. Re-exports the base {@link UseActions} with the App's `E`
32
30
  * threaded through every `HandlerContext` and produce draft.
33
31
  */
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>;
32
+ type AppActionsResult<M, AC, D, E> = UseActions<M extends Model | void ? M : void, AC extends Actions | void ? AC : void, D extends Props ? D : Props, E extends Env ? E : Env>;
35
33
  /**
36
34
  * `useActions(...)` signature on the App-bound Context. Has two forms:
37
35
  * void-model components omit the model argument entirely; everyone else
38
36
  * passes their initial model as the first argument and an optional data
39
37
  * callback as the second.
40
38
  */
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>;
39
+ type AppUseActions<M, AC, D, E> = M extends void ? (getData?: Data<D & Props>) => AppActionsResult<M, AC, D, E> : (model: M, getData?: Data<D & Props>) => AppActionsResult<M, AC, D, E>;
40
+ /**
41
+ * Returned from {@link App}. Bundles the Boundary, hooks, and Resource
42
+ * factory bound to a single typed Env shape `E`.
43
+ */
44
+ export type AppHandle<E extends object> = {
45
+ /**
46
+ * Boundary component for this App. Wraps the subtree with the `env`
47
+ * and `tap` declared on {@link App} &mdash; both are fixed at App
48
+ * construction time and cannot be overridden at the render site.
49
+ * Runtime mutations to the Env flow through
50
+ * `context.actions.produce(({ env }) => { ... })`; if a test or
51
+ * storybook needs a different initial Env, declare a separate `App`.
52
+ */
53
+ readonly Boundary: import('react').FC<{
54
+ children: import('react').ReactNode;
55
+ }>;
56
+ /**
57
+ * Hook returning a stable `Context` handle. The handle's
58
+ * `context.useActions(model?, getData?)` materialises the
59
+ * component's `[model, actions, data]` tuple. Every handler's
60
+ * `context.env` is typed as `E`.
61
+ */
62
+ readonly useContext: <M extends Model | void = void, AC extends Actions | void = void, D extends Props = Props>() => AppContextHandle<M, AC, D, E>;
63
+ /**
64
+ * Read-only Proxy over the per-Boundary Env, typed as `E`. Reads use
65
+ * dot notation (`env.session`) and always reflect the latest value
66
+ * across `await` boundaries. Writes flow through
67
+ * `context.actions.produce(({ env }) => { ... })`.
68
+ */
69
+ readonly useEnv: () => Readonly<E>;
70
+ /**
71
+ * `Resource` factory bound to this App's Env. Resources declared
72
+ * through this factory share the cache passed to `App({ cache })`
73
+ * &mdash; or fall back to a per-resource in-memory slot when no
74
+ * cache is configured on the App.
75
+ */
76
+ readonly Resource: AppResource<E>;
77
+ /**
78
+ * Opens a typed multicast scope. The generic `MulticastActions` declares
79
+ * the `Distribution.Multicast` action class (or union of classes)
80
+ * whose dispatches are routed through this scope &mdash; the
81
+ * returned handle mirrors the App surface but widens
82
+ * `useContext().actions.dispatch` to accept actions from `MulticastActions`
83
+ * on top of the local `AC` class.
84
+ *
85
+ * Render `<scope.Boundary>` to open the scope at runtime; nesting
86
+ * multiple boundaries from different `app.Scope()` calls is fine,
87
+ * each runs as an independent emitter shadowed for its subtree.
88
+ *
89
+ * The Scope handle deliberately does NOT expose a further `Scope`
90
+ * method &mdash; the multicast surface must be declared at the
91
+ * `app.Scope<MulticastActions>()` call site so the type union is explicit.
92
+ */
93
+ readonly Scope: <MulticastActions>() => import('../scope/types.ts').ScopeHandle<E, MulticastActions>;
94
+ };
42
95
  /**
43
96
  * `Context` handle returned by `app.useContext()`. Mirrors the base
44
- * {@link Context} but threads the App's Env shape `S` through every
97
+ * {@link Context} but threads the App's Env shape `E` through every
45
98
  * handler's `context.env` and produce draft.
46
99
  *
47
100
  * @template M The model type for the component's state, or `void`.
@@ -49,9 +102,9 @@ type AppUseActions<M, AC, D, S> = M extends void ? (getData?: Data<D & Props>) =
49
102
  * definitions, or `void` for actions-only consumers.
50
103
  * @template D The reactive data type returned from the `useActions(...)`
51
104
  * data callback.
52
- * @template S The App's Env shape, supplied at `App({env})` time.
105
+ * @template E The App's Env shape, supplied at `App({env})` time.
53
106
  */
54
- export type AppContextHandle<M, AC, D, S> = {
107
+ export type AppContextHandle<M, AC, D, E> = {
55
108
  /**
56
109
  * Stable dispatch surface available before `useActions(...)` runs.
57
110
  * Exposes only `dispatch(action, payload?)` &mdash; useful when an
@@ -77,6 +130,6 @@ export type AppContextHandle<M, AC, D, S> = {
77
130
  * second &mdash; the callback re-runs every render so handlers reading
78
131
  * `context.data` always see fresh values across `await` boundaries.
79
132
  */
80
- readonly useActions: AppUseActions<M, AC, D, S>;
133
+ readonly useActions: AppUseActions<M, AC, D, E>;
81
134
  };
82
135
  export {};
@@ -1,6 +1,6 @@
1
- import { Props } from './types';
1
+ import { Props } from './types.js';
2
2
  import * as React from "react";
3
- export { useBroadcast, BroadcastEmitter } from './utils';
3
+ export { useBroadcast, BroadcastEmitter } from './utils.js';
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';
1
+ import { BroadcastEmitter } from './utils.js';
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';
1
+ import { Props } from './types.js';
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';
1
+ import { ConsumerRenderer } from '../../types.js';
2
2
  /**
3
3
  * Props for the Partition component.
4
4
  * @internal
@@ -1,9 +1,9 @@
1
- import { Props } from './types';
1
+ import { Props } from './types.js';
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.js';
4
+ export { Partition } from './components/partition/index.js';
5
+ export type { Props as PartitionProps } from './components/partition/types.js';
6
+ export type { ConsumerRenderer, Entry, ConsumerContext } from './types.js';
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';
2
+ import { ActionId } from '../tasks/types.js';
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';
1
+ import { ConsumerContext } from './types.js';
2
2
  import * as React from "react";
3
3
  /**
4
4
  * React context for the consumer store.
@@ -1,21 +1,8 @@
1
- import { Props } from './types';
1
+ import { Props } from './types.js';
2
2
  import * as React from "react";
3
- export { useEnv } from './utils';
3
+ export { useEnv } from './utils.js';
4
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
- * &mdash; 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
5
+ * Provides a per-Boundary {@link EnvType} value to every component inside
19
6
  * the boundary. Usually wired in via the `<Boundary env={initial}>`
20
7
  * prop rather than used directly.
21
8
  *