vapor-chamber 1.28.0 → 1.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +62 -30
  3. package/ROADMAP.md +1 -1
  4. package/dist/LICENSE.txt +4 -2
  5. package/dist/bounds.d.ts +9 -0
  6. package/dist/bounds.d.ts.map +1 -1
  7. package/dist/bounds.js +4 -1
  8. package/dist/bounds.js.map +1 -1
  9. package/dist/chamber-vapor.d.ts +6 -3
  10. package/dist/chamber-vapor.d.ts.map +1 -1
  11. package/dist/chamber-vapor.js +7 -6
  12. package/dist/chamber-vapor.js.map +1 -1
  13. package/dist/chamber.d.ts +22 -22
  14. package/dist/chamber.d.ts.map +1 -1
  15. package/dist/chamber.js +41 -31
  16. package/dist/chamber.js.map +1 -1
  17. package/dist/command-bus.d.ts +13 -0
  18. package/dist/command-bus.d.ts.map +1 -1
  19. package/dist/command-bus.js +24 -6
  20. package/dist/command-bus.js.map +1 -1
  21. package/dist/directives.d.ts.map +1 -1
  22. package/dist/directives2.js +9 -9
  23. package/dist/directives2.js.map +1 -1
  24. package/dist/fast-lane.d.ts.map +1 -1
  25. package/dist/fast-lane.js +19 -4
  26. package/dist/fast-lane.js.map +1 -1
  27. package/dist/form.d.ts.map +1 -1
  28. package/dist/http-cache.d.ts +6 -3
  29. package/dist/http-cache.d.ts.map +1 -1
  30. package/dist/http.d.ts.map +1 -1
  31. package/dist/http.js +8 -9
  32. package/dist/http.js.map +1 -1
  33. package/dist/iife-core.js +1 -1
  34. package/dist/iife-elements.js +1 -1
  35. package/dist/iife.js +1 -1
  36. package/dist/index.d.ts +3 -3
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +8 -2036
  39. package/dist/ledger.js +1 -1
  40. package/dist/mcp.d.ts +1 -1
  41. package/dist/mcp.d.ts.map +1 -1
  42. package/dist/mcp.js +14 -34
  43. package/dist/mcp.js.map +1 -1
  44. package/dist/outbox.js +4 -4
  45. package/dist/outbox.js.map +1 -1
  46. package/dist/plugins-core.js +1 -1
  47. package/dist/plugins-extra.d.ts +14 -0
  48. package/dist/plugins-extra.d.ts.map +1 -1
  49. package/dist/plugins-io.js +1 -1
  50. package/dist/plugins-schema.js +1 -1
  51. package/dist/poll-with.d.ts.map +1 -1
  52. package/dist/router/table.d.ts.map +1 -1
  53. package/dist/router/url.d.ts.map +1 -1
  54. package/dist/router.js +6 -3
  55. package/dist/router.js.map +1 -1
  56. package/dist/schema.d.ts +3 -34
  57. package/dist/schema.d.ts.map +1 -1
  58. package/dist/shared-bus.d.ts +21 -1
  59. package/dist/shared-bus.d.ts.map +1 -1
  60. package/dist/shared-bus.js +5 -2
  61. package/dist/shared-bus.js.map +1 -1
  62. package/dist/signal.d.ts +5 -2
  63. package/dist/signal.d.ts.map +1 -1
  64. package/dist/signal.js.map +1 -1
  65. package/dist/src.js +2095 -0
  66. package/dist/src.js.map +1 -0
  67. package/dist/ssr.d.ts +35 -22
  68. package/dist/ssr.d.ts.map +1 -1
  69. package/dist/ssr.js +2 -2
  70. package/dist/ssr2.js +13 -2
  71. package/dist/ssr2.js.map +1 -1
  72. package/dist/store-base.d.ts.map +1 -1
  73. package/dist/store-base.js +1 -0
  74. package/dist/store-base.js.map +1 -1
  75. package/dist/stream-parser.d.ts +2 -0
  76. package/dist/stream-parser.d.ts.map +1 -1
  77. package/dist/stream-parser.js +12 -0
  78. package/dist/stream-parser.js.map +1 -1
  79. package/dist/testing.d.ts.map +1 -1
  80. package/dist/tool-schema.d.ts +32 -0
  81. package/dist/tool-schema.d.ts.map +1 -0
  82. package/dist/tool-schema.js +34 -0
  83. package/dist/tool-schema.js.map +1 -0
  84. package/dist/transitions/vapor.js +2 -2
  85. package/dist/transitions/vapor.js.map +1 -1
  86. package/dist/transitions.d.ts.map +1 -1
  87. package/dist/transitions2.js +2 -2
  88. package/dist/transitions2.js.map +1 -1
  89. package/dist/transports.d.ts.map +1 -1
  90. package/dist/transports2.js +14 -6
  91. package/dist/transports2.js.map +1 -1
  92. package/dist/vapor-chamber-core.iife.js +31 -3
  93. package/dist/vapor-chamber-core.iife.js.map +1 -1
  94. package/dist/vapor-chamber-core.iife.min.js +1 -1
  95. package/dist/vapor-chamber-elements.iife.js +34 -5
  96. package/dist/vapor-chamber-elements.iife.js.map +1 -1
  97. package/dist/vapor-chamber-elements.iife.min.js +1 -1
  98. package/dist/vapor-chamber.iife.js +98 -39
  99. package/dist/vapor-chamber.iife.js.map +1 -1
  100. package/dist/vapor-chamber.iife.min.js +1 -1
  101. package/dist/vapor.d.ts +13 -16
  102. package/dist/vapor.d.ts.map +1 -1
  103. package/dist/vapor.js +16 -2
  104. package/dist/vapor.js.map +1 -1
  105. package/dist/vite-hmr.d.ts +7 -9
  106. package/dist/vite-hmr.d.ts.map +1 -1
  107. package/dist/vite-hmr.js.map +1 -1
  108. package/dist/vue.d.ts +8 -6
  109. package/dist/vue.d.ts.map +1 -1
  110. package/dist/vue.js +17 -2
  111. package/dist/vue.js.map +1 -1
  112. package/package.json +5 -3
  113. package/scripts/check-size.mjs +39 -3
  114. package/scripts/generate-api-docs.mjs +43 -4
  115. package/src/bounds.ts +12 -0
  116. package/src/chamber-vapor.ts +18 -10
  117. package/src/chamber.ts +99 -54
  118. package/src/command-bus.ts +46 -5
  119. package/src/directives.ts +9 -6
  120. package/src/fast-lane.ts +24 -5
  121. package/src/form.ts +9 -0
  122. package/src/http-cache.ts +14 -12
  123. package/src/http.ts +8 -3
  124. package/src/index.ts +4 -0
  125. package/src/mcp.ts +17 -36
  126. package/src/outbox.ts +3 -3
  127. package/src/plugins-extra.ts +86 -11
  128. package/src/poll-with.ts +17 -2
  129. package/src/router/table.ts +8 -2
  130. package/src/router/url.ts +2 -1
  131. package/src/schema.ts +15 -35
  132. package/src/shared-bus.ts +27 -1
  133. package/src/signal.ts +5 -2
  134. package/src/ssr.ts +42 -22
  135. package/src/store-base.ts +5 -0
  136. package/src/stream-parser.ts +12 -0
  137. package/src/testing.ts +2 -1
  138. package/src/tool-schema.ts +53 -0
  139. package/src/transitions/vapor.ts +2 -2
  140. package/src/transitions.ts +4 -3
  141. package/src/transports.ts +24 -7
  142. package/src/vapor.ts +13 -16
  143. package/src/vite-hmr.ts +7 -9
  144. package/src/vue.ts +15 -6
  145. package/dist/index.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,52 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## v1.30.0 - 2026-10-11
6
+
7
+ - Fixed the HTTP client's cache answering one request with another's response when only the headers differed. The cache key was the URL alone, so after a sign-in as another user one client served the first user's `GET /me` to the second, and a language switch got the earlier language. The key now holds the request's headers, as the dedupe key already did, and `invalidateCache` still matches by URL. A header that changes on every request, such as a trace id, makes those reads miss the cache.
8
+ - Added `serializeForScript` to `vapor-chamber/ssr` and the root. The SSR docs embedded dehydrated commands with `JSON.stringify` inside a `<script>`, which leaves `</script>` in a target or payload as it is, so user text could close the element and run script. `serializeForScript` escapes `<`, `>`, `&`, U+2028 and U+2029. Its output is still JSON and parses to the same value. The docs now use it.
9
+ - Fixed `idempotent()` cancelling a duplicate that never aborted. A concurrent duplicate joined the first dispatch's run, so the first caller's abort cancelled the handler and settled both. Each caller's abort now settles that caller alone, as the HTTP client's joined GETs already do. The run is cancelled once every caller holding it has aborted, and a caller with no signal holds it until it lands.
10
+ - Fixed `cache()` storing an answer that an `invalidate()` or `clear()` overtook. A read still running when the app invalidated its key stored its answer from before the write, so later reads were served it until the TTL ran out. That read still answers its own caller, but is no longer stored. The HTTP client's cache already closes this window the same way.
11
+ - Fixed `pollWith` ending a running job as failed when the monitor answered a 503 or 429, or any answer declaring `Retry-After`. The finished state was then never read. Those answers now wait and poll again, within `maxWait`. A verdict, an abort or a lost reply still ends the job.
12
+ - Fixed `createStreamParser` refusing every empty array: `[]`, `{"items":[]}` and `[1,[]]` all ended with "Unexpected end: unclosed structure", while `{}` parsed. `]` is accepted directly after `[`, and a trailing comma (`[1,]`) is still refused.
13
+ - Fixed `commandKey` giving every circular target the same key, and likewise every target holding a BigInt. JSON could not serialize them, and the fallback `String(target)` is `[object Object]` for all of them. So `idempotent`, `cache`, `serialize` and `supersede` collapsed two different commands into one, and the second never ran. A BigInt is now tagged in the key, and a circular target keys by its identity. A target still keys by its value otherwise: `1` and `'1'` share a key, as before. Size: the Blade consumer bundle grows 60 B brotli (8,374 -> 8,434), and the IIFEs grow raw/brotli by full +155/+5, core +141/+65 and elements +141/+68 B.
14
+ - Added a development warning for an action name that holds `:`. `commandKey` joins the action and the target with `:`, so `('a:b', 'c')` and `('a', 'b:c')` share a key. Escaping the name on every call measured 25-30% of a primitive key (docs/V8-RULES.md rule 3), so it is a naming rule instead. The warning is folded out of production builds and costs nothing there.
15
+ - Fixed `createFormBus` showing errors for values it no longer held after `reset()` during `submit()`. A reset while the async rules ran was followed by their verdict on the discarded values. When those were valid, `onSubmit` sent them. A reset now cancels a submit that has not reached `onSubmit`: its validation is dropped, `onSubmit` does not run, and `submit()` returns `false`. A submit already in `onSubmit` is unchanged.
16
+ - Fixed typed `int` route params past `Number.MAX_SAFE_INTEGER` being rounded to another number. `/orders/9007199254740993` reached the loader as order 9007199254740992, and `resolve()` built links to it. Such a path param no longer fits `int`, so the URL falls through to the next row, and a typed query param reads its declared default, as a malformed value does. Declare the param `string` to receive it whole.
17
+ - Fixed `circuitBreaker` never opening again after a failed half-open probe. Past `resetTimeout` the circuit lets one dispatch through to probe the backend. A failed probe left it half-open, which is not open, so every later dispatch went to the failing backend until one succeeded. A failed probe now opens the circuit for another `resetTimeout` and calls `onOpen` again.
18
+ - Fixed `rateLimit({ max: 0 })` making the outbox flush in a loop. The refusal computed `retryIn` from the oldest dispatch in the window, and an empty window has none, so `retryIn` was `NaN`. The outbox read that as a declared wait and flushed again with no delay, re-saving its queue each time (31 times in 50 ms, measured). With no slot to free, the refusal now declares no `retryIn`, and the record waits for the next flush. Size, with the fix above: full IIFE +34/+10 B raw/brotli; core and elements are unchanged.
19
+ - Fixed `createWsBridge` treating a replaced socket's close as its own. After `disconnect()` then `connect()`, the first socket's close event arrives late, and `connect()` had already cleared the intentional mark. With `reconnect: false` it failed the commands queued for the new socket with `lost:reply` and emptied the queue. With `reconnect: true` it set `connected` to false under an open socket and scheduled a reconnect. A replaced socket's close now only calls `onDisconnect`.
20
+ - Fixed the example Laravel controller (`examples/laravel-backend/VaporChamberController.php`) answering one user's command with another user's response. Its Idempotency-Key replay was keyed by command and key alone, and the `idempotent()` plugin's default key is the command itself, so two users adding the same product within the TTL sent the same key: the second user's add never ran and returned the first user's cart. The key is now scoped to the user (else the session, else the address) and hashed. Hashing also keeps a key that carries a payload within the 255-character key column of the `database` cache store, Laravel's default, where a longer key failed the insert on MySQL and PostgreSQL.
21
+ - Fixed the example controller answering 500 `internal_error` to the exceptions Laravel's own handler answers with a status: `abort(404)` and other HTTP exceptions (now their status and headers, so `abort(429, ..., ['Retry-After' => 30])` carries its wait), `AuthenticationException` (401 `unauthenticated`), a policy's own status (`denyAsNotFound()` now answers 404 `not_found` instead of 403, which said the resource exists), and a `ValidationException` given its own status.
22
+ - Updated the development dependencies within their ranges: vite 8.3.4, @vitejs/plugin-vue 6.0.10, vue-tsc 3.3.13, playwright 1.64.0, happy-dom 20.14.6, @types/node 26.6.5, and astro 7.3.8 in the Astro example. Vue stays on 3.6.0-rc.10 until its rc.11 alignment, and TypeScript on 6.0.3 until vue-tsc supports 7.
23
+ - Fixed `v-vc-command` leaving its control busy for good when the `v-vc-optimistic` function threw. The optimistic call ran before the `try` whose `finally` releases the control, so `vc-loading`, `aria-disabled` and the in-flight guard stayed and every later press was swallowed. A throw is now that press's failure: `vc-error`, announced, and the control is released.
24
+ - Fixed a store with `undo: true` keeping every undone step for its lifetime. A redo reads only the newest undone steps, and `history` drops its redo list on a new action, so older ones could never be redone, yet each held two whole states: 1,000 rounds of an action and its undo with 80 KB states held 89 MB, against 29 MB without the undos. They are now bounded like the 256-step ring. The bound runs on the undo path; an action's path is unchanged.
25
+ - Fixed the MCP handler giving no answer to a tool whose value JSON cannot hold (a BigInt id, a cycle). The encoding threw outside the call's `try`, so the whole reply rejected, and over stdio the client's request was never answered. It is now that call's error result, with its id.
26
+ - Fixed `createWsBridge` stranding queued commands behind one whose envelope JSON cannot hold (a BigInt in the payload). Queued while disconnected, it threw out of the flush on reconnect: the commands behind it were already off the queue and never sent, and `onConnect` was skipped. A command is now encoded before it is queued or sent, so such a command fails its own dispatch, as it already did on an open socket. Each envelope is still encoded once.
27
+ - Fixed `toTools()`, `toAnthropicTools` and `toOpenAITools` emitting a field typed `'any'` as `{ type: 'any' }`, which is not a JSON Schema type and made the whole tool schema invalid. It is now `{}`, as `busToMcpTools` already emits it. The property type in `AnthropicTool` and `OpenAITool` is now `{ type?: string }`.
28
+ - Fixed `createSseBridge` leaking its first stream when `install()` ran twice. The first EventSource stayed open, out of `dispose()`'s reach. A second install now replaces it.
29
+ - Fixed an unsubscribe on `createFastLane({ removal: 'snapshot' })` reaching other registrations. It removed every registration of the same function, and one kept from before `remove()` or `clear()` removed a later registration. It now removes its own, as the default live mode does. The emit path is unchanged.
30
+ - Fixed `createTestBus().inspect()` always reporting no `responderActions`. It now lists the actions `respond()` registered, as the real bus does.
31
+ - Fixed a declared wait that no timer can keep (`retryIn` of NaN, Infinity or below 0) being taken as one. The outbox, the async bus's retry and `pollWith` read any number as a wait, so a plugin or app failure declaring NaN or Infinity made the outbox re-flush with no wait, forever (32 saves in 50 ms), and a NaN made the bus's retry re-send at once instead of backing off. All three now apply one rule (`isDelay`: a number from 0 to the timer maximum), on their failure paths.
32
+ - Fixed `useCommand`, `useCommandQuery` and `useVaporAsyncCommand` with dispatches overlapping. `loading` was written per dispatch, so the first to settle set it false while another still ran: a button bound to it read idle mid-request. `useCommandQuery`'s `data` took every answer in the order it landed, so an older query answering last overwrote the latest (search-as-you-type showed the results for "ab" under "abc"). `loading` now holds while any dispatch is in flight, and `data` is the latest query's answer. `lastError` stays the last failure to arrive, so an older failure is not hidden. The counts are made at a composable's first dispatch, so creating one runs the code it ran in 1.29. Measured against 1.29 (`npm run ab`, two call lengths): creation reads no difference, and dispatch through `useCommand` reads 4.5-6.2 ns faster. A dispatch that never settles now keeps `loading` true.
33
+ - `toTools()`, `toAnthropicTools` and `toOpenAITools` build their schemas with the generator `busToMcpTools` uses, so the three agree. They now declare `required`: `target` and `payload` when the action has them, and each typed field (an `'any'` field stays optional). A model was told nothing was required, and the validator then refused what it left out. The output changes shape: a test that compares it exactly will see `required`.
34
+ - Fixed `fromToolCall` handing a handler `payload: undefined` when a tool call left out the payload its action declares. Validation skips an absent payload, which stays app code's choice on `dispatch()`; a model's input is not app code. Such a call is now checked as an empty payload, so the validator names the missing fields.
35
+ - Size against 1.29, raw/brotli: the full IIFE +404/+100 B, core +191/+79, elements +191/+76. The Blade consumer bundle grows 60 B brotli, all `commandKey` (above).
36
+
37
+ ## v1.29.0 - 2026-10-08
38
+
39
+ - Added the root's whole surface to `vapor-chamber/vue` and `vapor-chamber/vapor`: the bus, plugins, transports and the HTTP client. A Vue app now imports from one entry, `import { createCommandBus, useCommand } from 'vapor-chamber/vue'`. They are the root's own functions, so code that imports the bus from the root is unchanged and shares the same bus.
40
+ - Fixed a bundle that inlines Vue and runs in Node, such as a Vite SSR build with `ssr.noExternal`. The root's runtime lookup loaded a second Vue and replaced what `vapor-chamber/vue` wired at build time. Composables lost reactivity and cleanup, and `untracked()` went back to tracking. Composables on a bundled page with an import map for `vue` hit the same. The lookup now wires only the Vue the app already has, and keeps what `vapor-chamber/vue` wired.
41
+ - Fixed the README's elements IIFE example, which registered no widget: it now hands Vue over with `configureVue` first. The README names one entry per kind of app, and says that `untracked()` cannot suspend tracking on a page with no build step.
42
+ - Fixed the store guide on ordering a store's answers with `serialize`. Its default key is the action, so two different actions of one store were both in flight. One lane per store, keyed to the commands that reach the server, now orders the store's writes and its reads. The guide and `serialize`'s docblock say to key the store's local reducers to `null`: in the lane, a read that awaits one waits for ever. They also say that a command waiting in the lane already reads as loading in `isLoading()`.
43
+ - Vue 3.5 is now tested on every run. Every entry that uses Vue loads and works on it, except `vapor-chamber/vapor`, `vapor-chamber/transitions/vapor` and `vapor-chamber/router/vapor`, which need Vue 3.6.
44
+ - Fixed `useSharedCommandState().dispatch` throwing when a subscriber of `inFlight` or `isAnyLoading` throws. `isAnyLoading` then stayed true for every reader. The dispatch now fails with that error and the count returns to 0, as `useCommand`'s does.
45
+ - Changed `dispatch` and `query` of `useCommand`, `useCommandQuery`, `useCommandGroup`, `useSharedCommandState` and `defineVaporCommand` to be typed by the bus they run on. A sync bus gives a `CommandResult`, an async one a promise of it, and the shared bus a `CommandResult`. A cast or a narrowing written for the old types can go. On an async bus, `useCommand`'s `dispatch` now returns a promise even when a subscriber throws.
46
+ - Added `bus` to the options of `useCommand`, `useCommandState`, `useCommandHistory`, `useCommandQuery`, `useCommandGroup`, `useCommandError`, `defineVaporCommand` and `useDeepCommandState`. Each runs on the bus it is given, sync or async. A concurrent SSR server can now pass a bus per request, as `setCommandBus` advises. Without it they use the shared bus, as before. `useSharedCommandState` now accepts an async bus too.
47
+ - Fixed the retry example, which stated the rule from before 1.27. Fixed the README row for `isLoading()`, which left out when counting starts and stops.
48
+ - Fixed three docs. The SSR examples set the shared bus per request, which their own warning calls unsafe: they now give each request its own bus. `src/signal.ts` and the whitepaper now say what a read of `__VUE__` finds. The whitepaper's script-tag recipe loaded the core IIFE, which never reads the parked Vue: it now loads the full IIFE.
49
+ - Fixed the examples to match this release. The Vue examples import from `vapor-chamber/vue` alone, and drop an `instanceof Promise` check on a dispatch result. Two retry comments now state the current rule. `feature-vite-wire.ts` says a root-only app needs the plugin on Vite.
50
+
5
51
  ## v1.28.0 - 2026-10-07
6
52
 
7
53
  - Fixed `createRouter()` throwing a TypeError on a route row whose `params` is `null`. The row now compiles as one without param types.
package/README.md CHANGED
@@ -43,7 +43,7 @@ so each action has one place to read and to test.
43
43
  - **Vue <!-- vc:vueAligned -->3.6.0-rc.10<!-- /vc:vueAligned --> aligned**: signals, `onScopeDispose`, `getCurrentScope`, alien-signals internals. Tracked per release in the [CHANGELOG](CHANGELOG.md)
44
44
  - **No runtime dependency**. `alien-signals` is an optional peer, installed only by apps that use the `vapor-chamber/alien-signals` connector. Unimported modules tree-shake to zero
45
45
  - **ESM-only**, plus three IIFE `<script>` drop-ins for no-bundler pages
46
- - **<!-- vc:covStatements -->100.0<!-- /vc:covStatements -->% coverage on all four axes** - statements, branches, functions and lines, across **<!-- vc:tests -->3498<!-- /vc:tests --> tests** in <!-- vc:testFiles -->316<!-- /vc:testFiles --> files ([full table](docs/COVERAGE.md)). Every branch in the measured surface is taken by a test
46
+ - **<!-- vc:covStatements -->100.0<!-- /vc:covStatements -->% coverage on all four axes** - statements, branches, functions and lines, across **<!-- vc:tests -->3658<!-- /vc:tests --> tests** in <!-- vc:testFiles -->349<!-- /vc:testFiles --> files ([full table](docs/COVERAGE.md)). Every branch in the measured surface is taken by a test
47
47
 
48
48
  ## Contents
49
49
 
@@ -156,20 +156,23 @@ A misspelled action or field in a component is a compile error, before any reque
156
156
  </details>
157
157
 
158
158
  <details>
159
- <summary><b>Gotcha:</b> in a Vue app, import the composables from <code>vapor-chamber/vue</code>, not the root</summary>
159
+ <summary><b>Gotcha:</b> in a Vue app, import from <code>vapor-chamber/vue</code>, not the root</summary>
160
160
 
161
- Import the composables (`useCommand`, `useCommandState`, `signal`, ...) from the static entry,
162
- `vapor-chamber/vue` (or `vapor-chamber/vapor` in a Vapor app). Import the bus (`createCommandBus`,
163
- `getCommandBus`, plugins, transports) from the root. The static entry hands Vue to the library at
164
- build time, the moment it is imported, so even module-scope state is reactive:
161
+ Import from the static entry, `vapor-chamber/vue` (or `vapor-chamber/vapor` when the app calls the
162
+ library's Vapor API, such as `createVaporChamberApp`). It
163
+ carries the composables (`useCommand`, `useCommandState`, `signal`, ...) and the root's whole
164
+ surface: the bus, plugins, transports. The static entry hands Vue to the library at build time, the
165
+ moment it is imported, so even module-scope state is reactive:
165
166
 
166
167
  ```ts
167
- import { createCommandBus, setCommandBus } from 'vapor-chamber'; // the bus: no Vue needed
168
- import { useCommand, signal } from 'vapor-chamber/vue'; // composables: Vue wired
168
+ import { createCommandBus, setCommandBus, useCommand, signal } from 'vapor-chamber/vue';
169
169
 
170
170
  export const count = signal(0); // reactive, no waiting
171
171
  ```
172
172
 
173
+ They are the root's own functions, so code that still takes the bus from the root shares the same
174
+ bus.
175
+
173
176
  The root has to work with no Vue in the tree, so it can only look for Vue at runtime. It does so
174
177
  through a bare `import('vue')` that resolves under a dev server and **fails in a production
175
178
  bundle**. There, composables imported from the root get plain `{ value }` state (no reactivity),
@@ -247,7 +250,7 @@ as its own chunk, the first time it renders one.
247
250
  **A pure-Vapor app can skip the vDOM renderer entirely** (experimental, v1.x).
248
251
  `vapor-chamber/router/vapor` exports the same `RouterOutlet` name built from Vapor's own helpers,
249
252
  so rendering a route needs no `vaporInteropPlugin`. Take the startup chunk of a Vite production
250
- build. It comes out **<!-- vc:outletSaving -->22.06<!-- /vc:outletSaving --> KB brotli / <!-- vc:outletSavingRaw -->70.4<!-- /vc:outletSavingRaw --> KB raw** smaller than the same
253
+ build. It comes out **<!-- vc:outletSaving -->22.08<!-- /vc:outletSaving --> KB brotli / <!-- vc:outletSavingRaw -->70.4<!-- /vc:outletSavingRaw --> KB raw** smaller than the same
251
254
  app rendering through the vDOM outlet plus interop. The same harness derives that baseline
252
255
  (`tests/vapor/vapor-outlet-size.test.ts`).
253
256
 
@@ -1101,8 +1104,7 @@ re-run. Every composable here suspends reactive tracking around its bus call. If
1101
1104
  **raw bus** inside an effect, wrap it:
1102
1105
 
1103
1106
  ```ts
1104
- import { getCommandBus } from 'vapor-chamber'; // the bus comes from the root
1105
- import { untracked } from 'vapor-chamber/vue'; // untracked() from the Vue entry
1107
+ import { getCommandBus, untracked } from 'vapor-chamber/vue';
1106
1108
 
1107
1109
  watchEffect(() => {
1108
1110
  // without untracked(), anything the HANDLER reads becomes a dependency of
@@ -1111,11 +1113,27 @@ watchEffect(() => {
1111
1113
  });
1112
1114
  ```
1113
1115
 
1116
+ On a page with no build step (a `<script>` tag or an import map), `untracked()` cannot suspend
1117
+ tracking: Vue's browser builds do not export what it needs. See
1118
+ [whitepaper 9.6](docs/whitepaper.md#96-blade--cdn-zero-build).
1119
+
1114
1120
  </details>
1115
1121
 
1116
1122
  <details>
1117
1123
  <summary><b>Which entry to import from</b> - <code>/vapor</code>, <code>/vue</code>, or the package root, and what each one wires</summary>
1118
1124
 
1125
+ One entry per kind of app, measured on built apps of each kind (log s35.231). `/vue` and `/vapor`
1126
+ carry the bus too.
1127
+
1128
+ | App | Import from | Build step |
1129
+ |---|---|---|
1130
+ | App that calls the library's Vapor API (`createVaporChamberApp`, `defineVaporComponent`, `defineVaporAsyncComponent`, `defineVaporCustomElement`, `getVaporInteropPlugin`) | `vapor-chamber/vapor` | Optional, drops the runtime lookup: `vaporChamberWire({ entry: 'vapor' })` on Vite, the define below elsewhere |
1131
+ | Any other Vue app, vDOM or Vapor, including one that uses only Vue's own Vapor APIs or `vapor-chamber/router/vapor` | `vapor-chamber/vue` | Optional, the same: `vaporChamberWire()` on Vite, the define elsewhere |
1132
+ | Server build (SSR) | `vapor-chamber/vue` | Optional: the define drops the runtime lookup from the server bundle |
1133
+ | App that imports only the root | the root | Needed on Vite: `vaporChamberWire()`. Other bundlers have none, so import from a subpath |
1134
+ | `<script>`-tag page | `VaporChamber.configureVue(Vue)` | None. `untracked()` is not available |
1135
+ | ESM page with an import map | `configureVue(Vue)` | None. `untracked()` cannot suspend tracking |
1136
+
1119
1137
  **Import from `vapor-chamber/vue` in a Vue app.** The Gotcha above says why. The root can only
1120
1138
  look for Vue at runtime, and that lookup fails in a production bundle. So `untracked()` silently
1121
1139
  becomes a pass-through there. The static entry resolves Vue at build time, and re-exports the
@@ -1128,7 +1146,8 @@ in a page where DEV is on. That is a page served by Vite's dev server (measured
1128
1146
  on Vite 8), or a test runner with a DOM. A production build drops it, from every chunk. A server never
1129
1147
  shows it: Node resolves that lookup in production too (measured), so the advice would be wrong there.
1130
1148
 
1131
- **On Vue 3.6 with Vapor, import from `vapor-chamber/vapor` instead.** It is a superset of
1149
+ **When the app calls the library's Vapor API, import from `vapor-chamber/vapor` instead.** It needs
1150
+ Vue 3.6. It is a superset of
1132
1151
  `vapor-chamber/vue`, with the same composables and the same tracking fix. It also wires Vue's
1133
1152
  Vapor APIs statically. So there is no `configureVue()` call to write and no runtime probe to
1134
1153
  depend on:
@@ -1173,12 +1192,12 @@ import vue from '@vitejs/plugin-vue';
1173
1192
  import { vaporChamberWire } from 'vapor-chamber/vite';
1174
1193
 
1175
1194
  export default defineConfig({
1176
- plugins: [vue(), vaporChamberWire()], // { entry: 'vapor' } when the app compiles <script setup vapor>
1195
+ plugins: [vue(), vaporChamberWire()], // { entry: 'vapor' } when the app calls the library's Vapor API
1177
1196
  });
1178
1197
  ```
1179
1198
 
1180
- `'vapor'` is your call, not a guess. It wires the Vapor runtime into the bundle, which a vDOM-only
1181
- 3.6 app should not pay for.
1199
+ `'vapor'` is your call, not a guess. It wires Vue's Vapor app and component helpers into the bundle,
1200
+ which an app that does not call the library's Vapor API should not pay for.
1182
1201
 
1183
1202
  The redirect runs in builds only. The dev server resolves the runtime lookup by itself. A
1184
1203
  redirect there, over a pre-bundled install, would put two copies of the library in the page
@@ -1204,8 +1223,13 @@ new webpack.DefinePlugin({ __VC_WIRED_BUILD__: 'true' })
1204
1223
  Leave it out only for an app that imports the composables from the root and wires nothing: there
1205
1224
  the lookup is what finds Vue (`tests/root-probe-builds.test.ts`).
1206
1225
 
1207
- The bus itself (`createCommandBus`, `getCommandBus`, plugins, transports) still comes from the
1208
- package root. It works with no Vue in the tree, so it is not part of a Vue-wiring entry.
1226
+ A Vite app that imports from the subpath, with neither `vaporChamberWire()` nor the define, still
1227
+ ships the lookup as dead code: +2,589 B raw, +932 B brotli on a vDOM app (measured 2026-10-08, log
1228
+ s35.231). Either one removes it.
1229
+
1230
+ Both entries also carry the bus itself (`createCommandBus`, `getCommandBus`, plugins, transports),
1231
+ so a Vue app imports one entry. An app with no Vue imports the bus from the package root, which
1232
+ works with no Vue in the tree.
1209
1233
 
1210
1234
  </details>
1211
1235
 
@@ -1333,7 +1357,7 @@ Minified, comment-free, brotli q=11. Always-current per-export table:
1333
1357
  on any regression past budget.
1334
1358
 
1335
1359
  The two that matter: the dispatch core is **<!-- vc:sizeCore -->4.7<!-- /vc:sizeCore --> KB** and the import-everything barrel is
1336
- <!-- vc:sizeBarrel -->32.2<!-- /vc:sizeBarrel --> KB. The main entries, and why the numbers are
1360
+ <!-- vc:sizeBarrel -->32.3<!-- /vc:sizeBarrel --> KB. The main entries, and why the numbers are
1337
1361
  machine-stamped rather than retyped:
1338
1362
 
1339
1363
  <details>
@@ -1342,14 +1366,14 @@ machine-stamped rather than retyped:
1342
1366
  | Entry | brotli |
1343
1367
  |---|--:|
1344
1368
  | dispatch core (`createCommandBus`, tree-shaken) | **<!-- vc:sizeCore -->4.7<!-- /vc:sizeCore --> KB** |
1345
- | `vapor-chamber` (main barrel, import-*everything*) | <!-- vc:sizeBarrel -->32.2<!-- /vc:sizeBarrel --> KB |
1369
+ | `vapor-chamber` (main barrel, import-*everything*) | <!-- vc:sizeBarrel -->32.3<!-- /vc:sizeBarrel --> KB |
1346
1370
  | `vapor-chamber/router` | <!-- vc:sizeRouter -->10.6<!-- /vc:sizeRouter --> KB |
1347
1371
  | `vapor-chamber/router/vdom` | <!-- vc:sizeRouterVdom -->0.7<!-- /vc:sizeRouterVdom --> KB |
1348
1372
  | `vapor-chamber/router/vapor` | <!-- vc:sizeRouterVapor -->0.7<!-- /vc:sizeRouterVapor --> KB |
1349
1373
  | `vapor-chamber/router/remote` | <!-- vc:sizeRouterRemote -->5.3<!-- /vc:sizeRouterRemote --> KB |
1350
1374
  | `vapor-chamber/router-fetch` | <!-- vc:sizeRouterFetch -->5.6<!-- /vc:sizeRouterFetch --> KB |
1351
- | `vapor-chamber/vue` | <!-- vc:sizeVue -->8.6<!-- /vc:sizeVue --> KB |
1352
- | `vapor-chamber/vapor` | <!-- vc:sizeVapor -->8.9<!-- /vc:sizeVapor --> KB |
1375
+ | `vapor-chamber/vue` (with the root's surface, import-*everything*) | <!-- vc:sizeVue -->32.3<!-- /vc:sizeVue --> KB |
1376
+ | `vapor-chamber/vapor` (the same) | <!-- vc:sizeVapor -->32.5<!-- /vc:sizeVapor --> KB |
1353
1377
  | `vapor-chamber/reactive` | <!-- vc:sizeReactive -->6.1<!-- /vc:sizeReactive --> KB |
1354
1378
  | `vapor-chamber/transports` | <!-- vc:sizeTransports -->5.8<!-- /vc:sizeTransports --> KB |
1355
1379
  | `vapor-chamber/outbox` | <!-- vc:sizeOutbox -->2.7<!-- /vc:sizeOutbox --> KB |
@@ -1358,6 +1382,9 @@ machine-stamped rather than retyped:
1358
1382
  | `vapor-chamber/store` | <!-- vc:sizeStore -->2.2<!-- /vc:sizeStore --> KB |
1359
1383
 
1360
1384
  **Rows are not additive** - every row includes the shared core, which your bundle carries once.
1385
+ The `/vue` and `/vapor` rows include the whole root they carry. An app pays only for what it
1386
+ imports: one import from `/vapor` builds `examples/vapor-sfc` to the same bytes as two (log
1387
+ s35.236).
1361
1388
  `vapor-chamber` is the barrel measured import-everything. Your bundler drops what you don't use.
1362
1389
 
1363
1390
  These figures are **machine-stamped** from [docs/BUNDLE-SIZES.md](./docs/BUNDLE-SIZES.md). The
@@ -1374,8 +1401,8 @@ Three `<script>`-tag drop-ins. Pick by audience, not feature checklist.
1374
1401
  | Variant | Audience | Min | Brotli | Gzip |
1375
1402
  |---|---|--:|--:|--:|
1376
1403
  | **core** | Sprinkled JS on server-rendered pages (Blade, Rails, Django, WordPress). You dispatch user actions to a backend over HTTP. | <!-- vc:sizeIifeCoreRaw -->32.4<!-- /vc:sizeIifeCoreRaw --> KB | <!-- vc:sizeIifeCore -->10.2<!-- /vc:sizeIifeCore --> KB | <!-- vc:sizeIifeCoreGzip -->11.2<!-- /vc:sizeIifeCoreGzip --> KB |
1377
- | **elements** | Embeddable widgets (chat bubbles, checkout buttons, third-party drop-ins). You ship a `<vc-widget>` custom element. | <!-- vc:sizeIifeElementsRaw -->34.0<!-- /vc:sizeIifeElementsRaw --> KB | <!-- vc:sizeIifeElements -->10.6<!-- /vc:sizeIifeElements --> KB | <!-- vc:sizeIifeElementsGzip -->11.7<!-- /vc:sizeIifeElementsGzip --> KB |
1378
- | **full** | SPAs that grew big enough to want everything (realtime, undo/redo, persistence, full Vapor surface). | <!-- vc:sizeIifeFullRaw -->46.7<!-- /vc:sizeIifeFullRaw --> KB | <!-- vc:sizeIifeFull -->14.6<!-- /vc:sizeIifeFull --> KB | <!-- vc:sizeIifeFullGzip -->16.1<!-- /vc:sizeIifeFullGzip --> KB |
1404
+ | **elements** | Embeddable widgets (chat bubbles, checkout buttons, third-party drop-ins). You ship a `<vc-widget>` custom element. | <!-- vc:sizeIifeElementsRaw -->34.0<!-- /vc:sizeIifeElementsRaw --> KB | <!-- vc:sizeIifeElements -->10.7<!-- /vc:sizeIifeElements --> KB | <!-- vc:sizeIifeElementsGzip -->11.7<!-- /vc:sizeIifeElementsGzip --> KB |
1405
+ | **full** | SPAs that grew big enough to want everything (realtime, undo/redo, persistence, full Vapor surface). | <!-- vc:sizeIifeFullRaw -->46.9<!-- /vc:sizeIifeFullRaw --> KB | <!-- vc:sizeIifeFull -->14.7<!-- /vc:sizeIifeFull --> KB | <!-- vc:sizeIifeFullGzip -->16.1<!-- /vc:sizeIifeFullGzip --> KB |
1379
1406
 
1380
1407
  <details>
1381
1408
  <summary><b>What's in each variant</b>, plus drop-in examples</summary>
@@ -1405,7 +1432,10 @@ Three `<script>`-tag drop-ins. Pick by audience, not feature checklist.
1405
1432
  ```html
1406
1433
  <!-- elements: register a custom-element widget in one call -->
1407
1434
  <script src=".../vapor-chamber-elements.iife.min.js"></script>
1408
- <script>
1435
+ <script type="module">
1436
+ // Needed: without Vue's Vapor build handed over, defineWidget returns false.
1437
+ const Vue = await import('.../vue.runtime-with-vapor.esm-browser.prod.js');
1438
+ VaporChamber.configureVue(Vue);
1409
1439
  VaporChamber.defineWidget('vc-cart', {
1410
1440
  props: { sku: String },
1411
1441
  // A Vapor setup() returns a BLOCK - real DOM nodes. There is no compiler
@@ -1491,8 +1521,8 @@ Everything else is optional and tree-shaken when unimported.
1491
1521
  form.ts, schema.ts, devtools.ts, directives.ts, vite-hmr.ts
1492
1522
  ```
1493
1523
 
1494
- **Coverage:** <!-- vc:covStatements -->100.0<!-- /vc:covStatements -->% statements, <!-- vc:covBranches -->100.0<!-- /vc:covBranches -->% branches, <!-- vc:covFunctions -->100.0<!-- /vc:covFunctions -->% functions, <!-- vc:covLines -->100.0<!-- /vc:covLines -->% lines across **<!-- vc:tests -->3498<!-- /vc:tests --> tests**
1495
- (<!-- vc:testFiles -->316<!-- /vc:testFiles --> files). Per-file table:
1524
+ **Coverage:** <!-- vc:covStatements -->100.0<!-- /vc:covStatements -->% statements, <!-- vc:covBranches -->100.0<!-- /vc:covBranches -->% branches, <!-- vc:covFunctions -->100.0<!-- /vc:covFunctions -->% functions, <!-- vc:covLines -->100.0<!-- /vc:covLines -->% lines across **<!-- vc:tests -->3658<!-- /vc:tests --> tests**
1525
+ (<!-- vc:testFiles -->349<!-- /vc:testFiles --> files). Per-file table:
1496
1526
  [docs/COVERAGE.md](docs/COVERAGE.md). Run `npm run test:coverage` for live numbers.
1497
1527
 
1498
1528
  ## Testing
@@ -1625,12 +1655,12 @@ on a Facts layer: `setupDevtools(bus, app, { facts: ['router*'] })`.
1625
1655
 
1626
1656
  | Composable | Description |
1627
1657
  |------------|-------------|
1628
- | `useCommand()` | Vapor-safe: dispatch + register/on/emit + reactive loading/error, auto-cleanup |
1629
- | `useSharedCommandState(options?)` | Aggregate `isAnyLoading` + `errors` ring buffer, **shared** across subscribers on the same bus. For toolbars, status bars, global spinners. `isLoading(action, target?)` answers "is THIS one in flight?" per `commandKey(action, target)`, bus-wide, as its own reactive flag - a reader re-runs only on its key |
1658
+ | `useCommand(options?)` | Vapor-safe: dispatch + register/on/emit + reactive loading/error, auto-cleanup |
1659
+ | `useSharedCommandState(options?)` | Aggregate `isAnyLoading` + `errors` ring buffer, **shared** across subscribers on the same bus. For toolbars, status bars, global spinners. `isLoading(action, target?)` answers "is THIS one in flight?" per `commandKey(action, target)`, bus-wide, as its own reactive flag - a reader re-runs only on its key. Counting starts at the bus's first `isLoading()` call and stops when its last holder leaves with nothing in flight. A dispatch already in flight when counting starts reads `false` |
1630
1660
  | `defineVaporCommand(action, handler, options?)` | Zero-overhead dispatch for hot paths |
1631
- | `useCommandState(initial, handlers)` | State managed by commands |
1661
+ | `useCommandState(initial, handlers, options?)` | State managed by commands |
1632
1662
  | `useCommandHistory(options?)` | Reactive undo/redo |
1633
- | `useCommandGroup(namespace)` | Namespace isolation - prefixes all calls in camelCase |
1663
+ | `useCommandGroup(namespace, options?)` | Namespace isolation - prefixes all calls in camelCase |
1634
1664
  | `useCommandError(options?)` | Reactive error boundary for failed dispatches |
1635
1665
  | `getCommandBus()` | Get the shared bus |
1636
1666
  | `untracked(fn)` | Run a **raw-bus** dispatch without its handler's reads becoming dependencies of the surrounding effect. The composables above already do this - you only need it when calling `getCommandBus()` directly from inside a `watchEffect` / `computed`. No-op without Vue. **Import from `vapor-chamber/vue`** in a Vue app: from the package root it degrades to a pass-through in a production build |
@@ -1641,6 +1671,8 @@ on a Facts layer: `setupDevtools(bus, app, { facts: ['router*'] })`.
1641
1671
  | `getVaporInteropPlugin()` | `vaporInteropPlugin` for mixed trees |
1642
1672
  | `setupDevtools(bus, app, { facts? })` | Connect bus to Vue DevTools (`vapor-chamber/devtools`) |
1643
1673
 
1674
+ Every composable above takes `bus` in its options, sync or async. Without it, it uses the shared bus.
1675
+
1644
1676
  **Router** - see [docs/router.md](docs/router.md) for the full surface:
1645
1677
  `createRouter`, `useRouter`, `useRoute`, `useQueryParam`, `useRouteData`, `useRouteError`,
1646
1678
  `useMenu`, `useBreadcrumbs`, `usePagination`, `onBeforeLeave`, `RouterOutlet`.
package/ROADMAP.md CHANGED
@@ -543,7 +543,7 @@ What this file still owns, because the whitepaper's appendix B does not:
543
543
  | v1.13.0 | rc.3 alignment | Tracking bump + docs: `configureVue()` promoted from no-bundler escape hatch to the recommended deterministic Vapor wiring for all consumers. No API change. |
544
544
  | v1.16.0 | rc.5 alignment | Tracking bump, plus one real contract change: the transition bridge's `phase` / `dispose` became **non-enumerable**, so `{ ...bridge }` no longer carries them. Direct access and destructuring are unaffected. The change exists because `v-bind="t"`, the documented binding, was spreading both into the DOM as attributes. |
545
545
  | v1.17.0 | rc.6 alignment | Two measured perf wins (wildcard fan-out 1.14-1.32x, router active-link stamping 1.77-1.86x), both 0 B. **New subpath `vapor-chamber/vapor`** (no new capability: a build-time wiring channel replacing the probe, wired set measured, custom-element/interop opt-in). Plus one real fix: `vapor-chamber/vue` never passed `hasInjectionContext`. So in a **production bundle** (probe dead) the rc.4 KeepAlive guard fell back to `getCurrentInstance()` and went inert, and `useCommandHistory`/`useCommandError` recorded commands dispatched into a deactivated view. Also: wildcard listeners match on a prefix computed at `on()` time (1.14-1.32x on wildcard fan-out, 0 B brotli, `'*'` unaffected). `configureVue()` documented as MERGING, so a Vapor app adds one name rather than re-enumerating eight. And one roadmap fact corrected: all four wrapped Vapor APIs are statically importable from `vue`, and were at rc.5. |
546
- | v1.18.0 | rc.6 window | **New subpath `vapor-chamber/router/vapor`**: a Vapor-native `RouterOutlet`, experimental. A genuinely new render surface, so it overrides the "new capability parks until after 3.6 stable" posture deliberately and on evidence, per the maturation posture under "Posture" above. The RC window is the runway, and an outlet built over N alignment cycles arrives at stable already hardened. Gated on a measured number before any of it was built: **<!-- vc:outletSaving -->22.06<!-- /vc:outletSaving --> KB brotli / <!-- vc:outletSavingRaw -->70.4<!-- /vc:outletSavingRaw --> KB raw** saved versus the same app rendering through the vDOM outlet plus interop, re-derived per run from a Vite production build by `tests/vapor/vapor-outlet-size.test.ts`. The guard holds two limits: the saving stays >= <!-- vc:outletFloor -->15.0<!-- /vc:outletFloor --> KB, and the Vapor outlet's own machinery over a router-without-outlet floor stays <= <!-- vc:outletOwnArmCeiling -->5.0<!-- /vc:outletOwnArmCeiling --> KB (**measured <!-- vc:outletOwnArm -->4.75<!-- /vc:outletOwnArm --> KB**). It is written to fail loudly if a later RC erodes either, and that failure is a decision trigger, not a threshold to raise. (Until the rc.8 cycle it was one esbuild-measured 19.5 KB bar on the difference, retired after firing twice on improvements: see the whitepaper's rc.8 row.) Contract changes: one additive `RouterErrorCode` (`mode_mismatch`), and route components on this outlet must be `defineVaporComponent` output. Blade rows still need the vDOM outlet. **Plus one BREAKING change to `vapor-chamber/router`**: it no longer builds an http client. So a `{ url }` route table now needs `http` and blade rows need `fetchBlade`, and `routerHttp()` and `bladeFetcher()` ship as the new `vapor-chamber/router/remote` subpath. Two lines for the affected setups, nothing for a generated table with no blade rows. That is the primary setup, and it was paying 3.4 KB brotli for features it never called. Taken in the RC window on the same maturation logic as the outlet above: near-zero adoption now, and the cost of the break only rises. `./router` drops 12.4 -> 9.6 KB brotli. Three more additive codes: `redirect_loop`, `no_router`, `http_unconfigured`. **Plus a new subpath `vapor-chamber/store`**: `defineChamberStore`, experimental, <!-- vc:sizeStore -->2.2<!-- /vc:sizeStore --> KB brotli, importing `vue` and nothing else. Store actions are commands, so `persist`, `history`, `sync`, `optimistic`, `idempotent`, `serialize` and the devtools timeline apply to store state with no store-specific code. URL-worthy fields delegate to the router rather than mirroring it. Lands with a deliberate reversal of whitepaper section 6 recorded in that section, for PACKAGE scope only: the bus still stores no state. No contract change for anyone not importing it. |
546
+ | v1.18.0 | rc.6 window | **New subpath `vapor-chamber/router/vapor`**: a Vapor-native `RouterOutlet`, experimental. A genuinely new render surface, so it overrides the "new capability parks until after 3.6 stable" posture deliberately and on evidence, per the maturation posture under "Posture" above. The RC window is the runway, and an outlet built over N alignment cycles arrives at stable already hardened. Gated on a measured number before any of it was built: **<!-- vc:outletSaving -->22.08<!-- /vc:outletSaving --> KB brotli / <!-- vc:outletSavingRaw -->70.4<!-- /vc:outletSavingRaw --> KB raw** saved versus the same app rendering through the vDOM outlet plus interop, re-derived per run from a Vite production build by `tests/vapor/vapor-outlet-size.test.ts`. The guard holds two limits: the saving stays >= <!-- vc:outletFloor -->15.0<!-- /vc:outletFloor --> KB, and the Vapor outlet's own machinery over a router-without-outlet floor stays <= <!-- vc:outletOwnArmCeiling -->5.0<!-- /vc:outletOwnArmCeiling --> KB (**measured <!-- vc:outletOwnArm -->4.71<!-- /vc:outletOwnArm --> KB**). It is written to fail loudly if a later RC erodes either, and that failure is a decision trigger, not a threshold to raise. (Until the rc.8 cycle it was one esbuild-measured 19.5 KB bar on the difference, retired after firing twice on improvements: see the whitepaper's rc.8 row.) Contract changes: one additive `RouterErrorCode` (`mode_mismatch`), and route components on this outlet must be `defineVaporComponent` output. Blade rows still need the vDOM outlet. **Plus one BREAKING change to `vapor-chamber/router`**: it no longer builds an http client. So a `{ url }` route table now needs `http` and blade rows need `fetchBlade`, and `routerHttp()` and `bladeFetcher()` ship as the new `vapor-chamber/router/remote` subpath. Two lines for the affected setups, nothing for a generated table with no blade rows. That is the primary setup, and it was paying 3.4 KB brotli for features it never called. Taken in the RC window on the same maturation logic as the outlet above: near-zero adoption now, and the cost of the break only rises. `./router` drops 12.4 -> 9.6 KB brotli. Three more additive codes: `redirect_loop`, `no_router`, `http_unconfigured`. **Plus a new subpath `vapor-chamber/store`**: `defineChamberStore`, experimental, <!-- vc:sizeStore -->2.2<!-- /vc:sizeStore --> KB brotli, importing `vue` and nothing else. Store actions are commands, so `persist`, `history`, `sync`, `optimistic`, `idempotent`, `serialize` and the devtools timeline apply to store state with no store-specific code. URL-worthy fields delegate to the router rather than mirroring it. Lands with a deliberate reversal of whitepaper section 6 recorded in that section, for PACKAGE scope only: the bus still stores no state. No contract change for anyone not importing it. |
547
547
  | v1.19.0 | rc.7 alignment | Tracking bump (peer `>=3.6.0-rc.7`). All 50 rc.7 commits read at source, every one pass-through. The cycle carried code anyway, because it also read every module in `src`: twenty-three defects, each reproduced before it was fixed. The largest class: five shipped plugins (`logger`, `history`, `circuitBreaker`, `metrics`, `persist`) read `next()`'s result as a `CommandResult`. On the async bus that is a Promise, so each took the wrong branch in silence. Now one shared rule, `src/settled.ts`. Numeric options that failed open on a bad value (NaN) now fall back to their documented default. Contract changes, all small: a second form on one bus now throws instead of taking the first one over (forms take an `id` prefix, default `'form'`, so a single form dispatches the names it always did). One additive `RouterErrorCode`, `cyclic_parent`. `configureVue` added to the `full` and `elements` IIFE namespaces. And the ESM build's DEV flag now folds in a consumer's production build, so dev-only diagnostic strings stop shipping: 14,037 -> 12,764 B raw and 4,453 -> 3,999 B brotli on a real Vite app build, about 10%. That held for one-chunk apps. In a code-split app, a chunk that imported DEV kept its strings until v1.20.0 derived DEV per module. |
548
548
  | v1.20.0 | rc.8 alignment | Tracking bump (peer `>=3.6.0-rc.8`), plus one contract NARROWING, taken as a minor under the pre-stable policy below. `retry()`'s default no longer retries an error carrying an HTTP status unless it is 408, 429 or 5xx. So `retry()` stacked in front of the HTTP bridge stops re-sending a 422. A handler that throws a 4xx-status error and relied on `retry()` re-running it passes its own `isRetryable`. Additive: `classifyError` and `isRetryableStatus` exported from the root. `vaporChamberWire()` from `vapor-chamber/vite`, a build-only Vite plugin that wires Vue into an app importing the root, with no import changed. `vcCommandVapor` from `vapor-chamber/directives`, `v-vc:command` for Vapor components (the plugin's install-time Vapor warning is gone). Also in this release: `useSharedCommandState().isLoading(action, target?)`. A plugin's throw is a `plugin:failed:plugin` result, and a sealed bus refuses `clear()`. `dispose()` settles waiting `request()`s, and the sync `request()` honours its signal. A before-hook's throw is a `core:refused:hook` result, and `history()` records an async redo once. The ESM build's DEV is derived per module. Sizes measured as they ship (Vite production builds, about 10% lower). `alien-signals` is an optional peer. `dispose()` runs each plugin's `dispose()`, and `retry()` has one. What an undo handler or a redo dispatches carries origin `undo` / `redo` and is never recorded. The TestBus's `request()`/`respond()` are real. |
549
549
  | v2.0.0 | One minor cycle after 3.6 stable | Stable-landing realignment: finalize the identity decision (Vapor-first vs bus-first). The `vue36` flavor + registry collapse were withdrawn at rc.3 (superseded by `configureVue()`, under 0.9 KB at stake: see "What is transitional"). `useVaporCommand`->`useCommand` shipped early in v1.7.0. See the checklist below. |
package/dist/LICENSE.txt CHANGED
@@ -1,10 +1,10 @@
1
- vapor-chamber v1.28.0 - licence information for the distributed JavaScript
1
+ vapor-chamber v1.30.0 - licence information for the distributed JavaScript
2
2
  ==============================================================================
3
3
 
4
4
  Every JavaScript file in this directory carries the one-line reference
5
5
  "/*! See LICENSE.txt */". This file is that reference.
6
6
 
7
- These 61 files:
7
+ These 63 files:
8
8
 
9
9
  a11y.js
10
10
  alien-signals.js
@@ -44,12 +44,14 @@ These 61 files:
44
44
  router/vdom.js
45
45
  shared-bus.js
46
46
  signal.js
47
+ src.js
47
48
  ssr.js
48
49
  ssr2.js
49
50
  store-base.js
50
51
  store.js
51
52
  store/core.js
52
53
  stream-parser.js
54
+ tool-schema.js
53
55
  transitions.js
54
56
  transitions/vapor.js
55
57
  transitions2.js
package/dist/bounds.d.ts CHANGED
@@ -67,4 +67,13 @@ export declare const MAX_TIMEOUT_MS = 2147483647;
67
67
  * Delays pass `max: MAX_TIMEOUT_MS`.
68
68
  */
69
69
  export declare function countOption(value: unknown, fallback: number, min?: number, max?: number): number;
70
+ /**
71
+ * True for a wait a timer can keep: a number from 0 to MAX_TIMEOUT_MS. The
72
+ * rule every reader of a declared `retryIn` applies (the async bus's retry,
73
+ * the outbox, pollWith), so the wait is checked where it is read, whoever
74
+ * declared it. `typeof === 'number'` let NaN and Infinity through: a timer
75
+ * fires at once on both, and the outbox re-flushed in a loop on either
76
+ * (tests/retry-in-unusable.test.ts).
77
+ */
78
+ export declare function isDelay(value: unknown): value is number;
70
79
  //# sourceMappingURL=bounds.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bounds.d.ts","sourceRoot":"","sources":["../src/bounds.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH;wEACwE;AACxE,eAAO,MAAM,cAAc,aAAgB,CAAC;AAE5C;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,SAAI,EAAE,GAAG,SAA2B,GAAG,MAAM,CAG7G"}
1
+ {"version":3,"file":"bounds.d.ts","sourceRoot":"","sources":["../src/bounds.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH;wEACwE;AACxE,eAAO,MAAM,cAAc,aAAgB,CAAC;AAE5C;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,SAAI,EAAE,GAAG,SAA2B,GAAG,MAAM,CAG7G;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAEvD"}
package/dist/bounds.js CHANGED
@@ -4,4 +4,7 @@ function countOption(value, fallback, min = 0, max = Number.POSITIVE_INFINITY) {
4
4
  const n = Number(value);
5
5
  return Number.isNaN(n) ? fallback : Math.min(max, Math.max(min, Math.trunc(n)));
6
6
  }
7
- export { countOption as n, MAX_TIMEOUT_MS as t };
7
+ function isDelay(value) {
8
+ return typeof value === "number" && value >= 0 && value <= 2147483647;
9
+ }
10
+ export { countOption as n, isDelay as r, MAX_TIMEOUT_MS as t };
@@ -1 +1 @@
1
- {"version":3,"file":"bounds.js","names":[],"sources":["../src/bounds.ts"],"sourcesContent":["/**\n * vapor-chamber - the one rule for a numeric option.\n *\n * Internal. Every cap in this library is a number a caller supplies, and a\n * caller's number is frequently not one: `Number(localStorage.getItem(...))`, a\n * parsed env var, a JSON field, a form value. The failure is never loud.\n *\n * COMPARISON DIRECTION IS THE WHOLE STORY, and it decides which way a bad value\n * fails. Every comparison against NaN is false, so:\n *\n * length < max gate on RECORDING NaN -> records nothing FAILS SAFE\n * length > max gate on EVICTING NaN -> evicts nothing FAILS OPEN\n * count >= max gate on REFUSING NaN -> refuses nothing FAILS OPEN\n *\n * Two of the three are unbounded growth or a defeated guard, and they are the\n * two that are idiomatic to write. Measured through the public API before this\n * module existed: `history({ maxSize: NaN })` kept 500 entries against a cap of\n * 50, `metrics({ maxSize: NaN })` kept 1500 against 1000,\n * `circuitBreaker({ threshold: NaN })` sat closed through 20 straight failures,\n * and `StreamParser({ maxDepth: NaN })` accepted 5,000 levels of nesting\n * without once firing the depth guard that exists to stop exactly that.\n *\n * WHY A MODULE AND NOT `Math.max(0, raw | 0)` AT EACH SITE. That idiom was\n * already applied by hand at five sites, and this sweep found seven more it had\n * never reached - the same shape as the arrow sweep that left 48 arrows in 15\n * files and the glyph sweep that left four in shipped strings. A rule spelled\n * out at each call site is a rule that is missing wherever nobody remembered it.\n *\n * It is also subtly wrong twice over. `Infinity | 0` is 0, so those five sites\n * turned \"no bound\" into \"store nothing\" - the opposite of the request. And\n * `| 0` truncates to 32 bits, so a cap of 3_000_000_000 silently went negative.\n * Both disappear here: `Math.trunc(Infinity)` is `Infinity` and `Math.min`\n * carries it through, so the unbounded case needs no branch of its own.\n *\n * ONE SITE DELIBERATELY DOES NOT USE THIS, and saying so here is the point of\n * saying it at all - an exception nobody records is just a gap. `command-bus.ts`\n * keeps its inline `| 0` for `maxBufferSize`, because importing this module into\n * the one file every consumer pulls costs 50 B brotli in the minimal Blade\n * bundle `esm-treeshake.test.ts` gates, which breaches that ceiling on its own.\n * `| 0` already handles the NaN case there; what it gives up is `Infinity`,\n * recorded at that call site.\n *\n * WHY A BAD OPTION FALLS BACK TO THE DEFAULT rather than to zero. The five\n * hand-clamped sites folded NaN to 0 on the argument that storing nothing is\n * \"bounded, and loud rather than silent\". Written out per site, that argument\n * collapses: an outbox that queues nothing DISCARDS the commands it exists to\n * hold; `idempotent` that remembers nothing DOUBLE-EXECUTES commands declared\n * idempotent; a transition that times out in 1ms removes the animation instead\n * of the hang. None of those is louder than the unbounded failure - only\n * costlier. A NaN option is indistinguishable in intent from a missing one (it\n * is what a failed config read produces), so it now behaves like a missing one.\n */\n\n/** The largest delay `setTimeout` accepts. Past it a timer fires IMMEDIATELY\n * rather than never, turning a long backoff into no backoff at all. */\nexport const MAX_TIMEOUT_MS = 2_147_483_647;\n\n/**\n * Normalize a caller-supplied count, size, depth, attempt cap or delay.\n *\n * - NaN, or anything that is not a number -> `fallback`, the documented default\n * - below `min` -> `min`, which catches negatives and -Infinity together\n * - above `max` -> `max`; left at `Infinity`, \"no bound\" passes through intact\n * - fractional -> truncated toward zero\n *\n * `min` defaults to 0 because a cap of zero is meaningful (store nothing) and\n * distinct from a missing option. Sites where zero is nonsense - an attempt\n * count, a line length, a circuit threshold - pass `min: 1` and say why.\n * Delays pass `max: MAX_TIMEOUT_MS`.\n */\nexport function countOption(value: unknown, fallback: number, min = 0, max = Number.POSITIVE_INFINITY): number {\n const n = Number(value);\n return Number.isNaN(n) ? fallback : Math.min(max, Math.max(min, Math.trunc(n)));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuDA,IAAa,iBAAiB;;;;;;;;;;;;;;AAe9B,SAAgB,YAAY,OAAgB,UAAkB,MAAM,GAAG,MAAM,OAAO,mBAA2B;CAC7G,MAAM,IAAI,OAAO,KAAK;CACtB,OAAO,OAAO,MAAM,CAAC,IAAI,WAAW,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC;AAChF"}
1
+ {"version":3,"file":"bounds.js","names":[],"sources":["../src/bounds.ts"],"sourcesContent":["/**\n * vapor-chamber - the one rule for a numeric option.\n *\n * Internal. Every cap in this library is a number a caller supplies, and a\n * caller's number is frequently not one: `Number(localStorage.getItem(...))`, a\n * parsed env var, a JSON field, a form value. The failure is never loud.\n *\n * COMPARISON DIRECTION IS THE WHOLE STORY, and it decides which way a bad value\n * fails. Every comparison against NaN is false, so:\n *\n * length < max gate on RECORDING NaN -> records nothing FAILS SAFE\n * length > max gate on EVICTING NaN -> evicts nothing FAILS OPEN\n * count >= max gate on REFUSING NaN -> refuses nothing FAILS OPEN\n *\n * Two of the three are unbounded growth or a defeated guard, and they are the\n * two that are idiomatic to write. Measured through the public API before this\n * module existed: `history({ maxSize: NaN })` kept 500 entries against a cap of\n * 50, `metrics({ maxSize: NaN })` kept 1500 against 1000,\n * `circuitBreaker({ threshold: NaN })` sat closed through 20 straight failures,\n * and `StreamParser({ maxDepth: NaN })` accepted 5,000 levels of nesting\n * without once firing the depth guard that exists to stop exactly that.\n *\n * WHY A MODULE AND NOT `Math.max(0, raw | 0)` AT EACH SITE. That idiom was\n * already applied by hand at five sites, and this sweep found seven more it had\n * never reached - the same shape as the arrow sweep that left 48 arrows in 15\n * files and the glyph sweep that left four in shipped strings. A rule spelled\n * out at each call site is a rule that is missing wherever nobody remembered it.\n *\n * It is also subtly wrong twice over. `Infinity | 0` is 0, so those five sites\n * turned \"no bound\" into \"store nothing\" - the opposite of the request. And\n * `| 0` truncates to 32 bits, so a cap of 3_000_000_000 silently went negative.\n * Both disappear here: `Math.trunc(Infinity)` is `Infinity` and `Math.min`\n * carries it through, so the unbounded case needs no branch of its own.\n *\n * ONE SITE DELIBERATELY DOES NOT USE THIS, and saying so here is the point of\n * saying it at all - an exception nobody records is just a gap. `command-bus.ts`\n * keeps its inline `| 0` for `maxBufferSize`, because importing this module into\n * the one file every consumer pulls costs 50 B brotli in the minimal Blade\n * bundle `esm-treeshake.test.ts` gates, which breaches that ceiling on its own.\n * `| 0` already handles the NaN case there; what it gives up is `Infinity`,\n * recorded at that call site.\n *\n * WHY A BAD OPTION FALLS BACK TO THE DEFAULT rather than to zero. The five\n * hand-clamped sites folded NaN to 0 on the argument that storing nothing is\n * \"bounded, and loud rather than silent\". Written out per site, that argument\n * collapses: an outbox that queues nothing DISCARDS the commands it exists to\n * hold; `idempotent` that remembers nothing DOUBLE-EXECUTES commands declared\n * idempotent; a transition that times out in 1ms removes the animation instead\n * of the hang. None of those is louder than the unbounded failure - only\n * costlier. A NaN option is indistinguishable in intent from a missing one (it\n * is what a failed config read produces), so it now behaves like a missing one.\n */\n\n/** The largest delay `setTimeout` accepts. Past it a timer fires IMMEDIATELY\n * rather than never, turning a long backoff into no backoff at all. */\nexport const MAX_TIMEOUT_MS = 2_147_483_647;\n\n/**\n * Normalize a caller-supplied count, size, depth, attempt cap or delay.\n *\n * - NaN, or anything that is not a number -> `fallback`, the documented default\n * - below `min` -> `min`, which catches negatives and -Infinity together\n * - above `max` -> `max`; left at `Infinity`, \"no bound\" passes through intact\n * - fractional -> truncated toward zero\n *\n * `min` defaults to 0 because a cap of zero is meaningful (store nothing) and\n * distinct from a missing option. Sites where zero is nonsense - an attempt\n * count, a line length, a circuit threshold - pass `min: 1` and say why.\n * Delays pass `max: MAX_TIMEOUT_MS`.\n */\nexport function countOption(value: unknown, fallback: number, min = 0, max = Number.POSITIVE_INFINITY): number {\n const n = Number(value);\n return Number.isNaN(n) ? fallback : Math.min(max, Math.max(min, Math.trunc(n)));\n}\n\n/**\n * True for a wait a timer can keep: a number from 0 to MAX_TIMEOUT_MS. The\n * rule every reader of a declared `retryIn` applies (the async bus's retry,\n * the outbox, pollWith), so the wait is checked where it is read, whoever\n * declared it. `typeof === 'number'` let NaN and Infinity through: a timer\n * fires at once on both, and the outbox re-flushed in a loop on either\n * (tests/retry-in-unusable.test.ts).\n */\nexport function isDelay(value: unknown): value is number {\n return typeof value === 'number' && value >= 0 && value <= MAX_TIMEOUT_MS;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuDA,IAAa,iBAAiB;;;;;;;;;;;;;;AAe9B,SAAgB,YAAY,OAAgB,UAAkB,MAAM,GAAG,MAAM,OAAO,mBAA2B;CAC7G,MAAM,IAAI,OAAO,KAAK;CACtB,OAAO,OAAO,MAAM,CAAC,IAAI,WAAW,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC;AAChF;;;;;;;;;AAUA,SAAgB,QAAQ,OAAiC;CACvD,OAAO,OAAO,UAAU,YAAY,SAAS,KAAK,SAAA;AACpD"}
@@ -10,7 +10,8 @@
10
10
  *
11
11
  * Separated from chamber.ts to keep the core composable module CDCC-compliant.
12
12
  */
13
- import type { Handler, RegisterOptions, CommandResult } from './command-bus';
13
+ import type { BaseBus, CommandBus, Handler, RegisterOptions, CommandResult } from './command-bus';
14
+ import { type BusOption, type ResultOn } from './shared-bus';
14
15
  /**
15
16
  * Create a Vapor app instance with vapor-chamber ready.
16
17
  * Requires Vue 3.6+. Throws if Vapor is not available.
@@ -117,6 +118,8 @@ export declare function defineVaporComponent<T = any>(options: object): T | null
117
118
  * const AsyncPanel = defineVaporAsyncComponent(() => import('./Panel.vue'));
118
119
  */
119
120
  export declare function defineVaporAsyncComponent<T = any>(loader: (() => Promise<unknown>) | object): T | null;
121
+ /** `defineVaporCommand`'s options: how the handler registers, and on which bus. */
122
+ export type VaporCommandOptions<B extends BaseBus = BaseBus> = RegisterOptions & BusOption<B>;
120
123
  /**
121
124
  * defineVaporCommand - zero-overhead command for hot paths in Vapor mode.
122
125
  *
@@ -132,8 +135,8 @@ export declare function defineVaporAsyncComponent<T = any>(loader: (() => Promis
132
135
  * });
133
136
  * dispatch({ name: 'page_view', params: { page: '/landing' } });
134
137
  */
135
- export declare function defineVaporCommand(action: string, handler: Handler, options?: RegisterOptions): {
136
- dispatch: (target: any, payload?: any) => CommandResult;
138
+ export declare function defineVaporCommand<B extends BaseBus = CommandBus>(action: string, handler: Handler, options?: VaporCommandOptions<B>): {
139
+ dispatch: (target: any, payload?: any) => ResultOn<B>;
137
140
  dispose: () => void;
138
141
  };
139
142
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"chamber-vapor.d.ts","sourceRoot":"","sources":["../src/chamber-vapor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAcH,OAAO,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,aAAa,EAAc,MAAM,eAAe,CAAC;AA2BzF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,GAAG,GAAG,EAC9C,aAAa,EAAE,MAAM,EACrB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAClC,IAAI,CAaN;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,IAAI,GAAG,GAAG,IAAI,CAElD;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,GAAG,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAOlG;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,GAAG,GAAG,EAAE,OAAO,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAOvE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,yBAAyB,CAAC,CAAC,GAAG,GAAG,EAC/C,MAAM,EAAE,CAAC,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,MAAM,GACxC,CAAC,GAAG,IAAI,CAOV;AAMD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,eAAe;uBAYC,GAAG,YAAY,GAAG,KAAG,aAAa;;EAS7D;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,CAAC,EAAE;IAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,aAAa,CAAC,CAAA;CAAE;uBAUlG,MAAM,UAAU,GAAG,YAAY,GAAG,KAAG,OAAO,CAAC,aAAa,CAAC;;;;EAgC5F"}
1
+ {"version":3,"file":"chamber-vapor.d.ts","sourceRoot":"","sources":["../src/chamber-vapor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAaH,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAGlG,OAAO,EAAc,KAAK,SAAS,EAAE,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAC;AAyBzE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,GAAG,GAAG,EAC9C,aAAa,EAAE,MAAM,EACrB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAClC,IAAI,CAaN;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,IAAI,GAAG,GAAG,IAAI,CAElD;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,GAAG,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAOlG;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,GAAG,GAAG,EAAE,OAAO,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAOvE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,yBAAyB,CAAC,CAAC,GAAG,GAAG,EAC/C,MAAM,EAAE,CAAC,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,MAAM,GACxC,CAAC,GAAG,IAAI,CAOV;AAMD,mFAAmF;AACnF,MAAM,MAAM,mBAAmB,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,IAAI,eAAe,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;AAE9F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,OAAO,GAAG,UAAU,EAC/D,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,mBAAmB,CAAC,CAAC,CAAC;uBAcN,GAAG,YAAY,GAAG,KAAG,QAAQ,CAAC,CAAC,CAAC;;EAS3D;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,CAAC,EAAE;IAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,aAAa,CAAC,CAAA;CAAE;uBAalG,MAAM,UAAU,GAAG,YAAY,GAAG,KAAG,OAAO,CAAC,aAAa,CAAC;;;;EAgC5F"}
@@ -1,8 +1,8 @@
1
1
  /*! See LICENSE.txt */
2
- import { x as errResult } from "./command-bus.js";
2
+ import { S as errResult } from "./command-bus.js";
3
3
  import { a as getDefineVaporComponentFn, b as vueDetectionHint, c as getVaporInteropRef, d as tryAutoCleanup, f as untracked, i as getDefineVaporAsyncComponentFn, o as getDefineVaporCustomElementFn, s as getVaporAppFn } from "./chamber.js";
4
4
  import { n as signal } from "./signal.js";
5
- import { t as getCommandBus } from "./shared-bus.js";
5
+ import { r as resolveBus } from "./shared-bus.js";
6
6
  var DEV = typeof process !== "undefined" || import.meta.env?.DEV ? process.env.NODE_ENV !== "production" : false;
7
7
  function devWarnNoVapor(api) {
8
8
  console.warn(`[vapor-chamber] ${api}() returned null - Vue 3.6+ with Vapor mode was not detected, so the component was NOT created. ${vueDetectionHint()} For VDOM mode use the matching define* helper from vue instead.`);
@@ -40,7 +40,7 @@ function defineVaporAsyncComponent(loader) {
40
40
  return fn(loader);
41
41
  }
42
42
  function defineVaporCommand(action, handler, options) {
43
- const bus = getCommandBus();
43
+ const bus = resolveBus(options?.bus);
44
44
  const unregister = bus.register(action, handler, options);
45
45
  function dispatch(target, payload) {
46
46
  return untracked(() => bus.dispatch(action, target, payload));
@@ -55,12 +55,13 @@ function defineVaporCommand(action, handler, options) {
55
55
  };
56
56
  }
57
57
  function useVaporAsyncCommand(asyncBus) {
58
- const bus = asyncBus ?? getCommandBus();
58
+ const bus = resolveBus(asyncBus);
59
59
  const loading = signal(false);
60
60
  const lastError = signal(null);
61
+ let n = 0;
61
62
  async function dispatch(action, target, payload) {
62
63
  try {
63
- loading.value = true;
64
+ if (n++ === 0) loading.value = true;
64
65
  lastError.value = null;
65
66
  const result = await untracked(() => bus.dispatch(action, target, payload));
66
67
  if (!result.ok) lastError.value = result.error ?? null;
@@ -70,7 +71,7 @@ function useVaporAsyncCommand(asyncBus) {
70
71
  lastError.value = error;
71
72
  return errResult(error);
72
73
  } finally {
73
- loading.value = false;
74
+ if (--n === 0) loading.value = false;
74
75
  }
75
76
  }
76
77
  function dispose() {
@@ -1 +1 @@
1
- {"version":3,"file":"chamber-vapor.js","names":["_errResult"],"sources":["../src/chamber-vapor.ts"],"sourcesContent":["/**\n * vapor-chamber - Vue 3.6+ Vapor-specific API\n *\n * This file renders nothing: it forwards Vue's own define* functions, so\n * rendering-side work (attrs fallthrough, interop, hydration) lands below it.\n * The wrappers and createVaporChamberApp take an opt-in return generic\n * (`<T = any>`) and `object`-typed options: importing Vue's Vapor types here\n * would put a hard `vue` type dependency on the main barrel and break Vue-less\n * command-bus consumers, so callers opt in: `defineVaporComponent<MyComp>(opts)`.\n *\n * Separated from chamber.ts to keep the core composable module CDCC-compliant.\n */\n\nimport {\n getCommandBus,\n signal,\n tryAutoCleanup,\n untracked,\n getVaporAppFn,\n getVaporInteropRef,\n getDefineVaporCustomElementFn,\n getDefineVaporComponentFn,\n getDefineVaporAsyncComponentFn,\n vueDetectionHint,\n} from './chamber';\nimport type { Handler, RegisterOptions, CommandResult, CommandMap } from './command-bus';\nimport { _errResult } from './command-bus';\nimport { DEV } from './dev';\n\n/**\n * Why the `defineVapor*` wrappers warn instead of just returning null.\n *\n * Each returns `null` when the Vapor runtime is absent, and callers are\n * documented to check. But a bare `null` is the same failure shape that hid a\n * real bug for several releases: `tryKeepAliveHooks` guarded on an accessor\n * that silently answers \"no\" under Vapor, so a feature was inert with nothing\n * to see (rc.4 cycle - see tests/keepalive-input-scope-fixture.test.ts). Silent\n * negatives are how detection bugs survive.\n *\n * So the null stays - throwing would break the documented contract and the\n * tests that assert it - but it is not quiet. The call sites gate on\n * `DEV`, so the whole call (and every byte of message text) folds out of the\n * production IIFE builds; this helper is then unreferenced and tree-shaken.\n */\nfunction devWarnNoVapor(api: string): void {\n console.warn(\n `[vapor-chamber] ${api}() returned null - Vue 3.6+ with Vapor mode was not detected, ` +\n `so the component was NOT created. ${vueDetectionHint()} ` +\n 'For VDOM mode use the matching define* helper from vue instead.',\n );\n}\n\n/**\n * Create a Vapor app instance with vapor-chamber ready.\n * Requires Vue 3.6+. Throws if Vapor is not available.\n *\n * Pass-through over Vue's createVaporApp - scope-ID handling, HMR app-instance\n * refresh, and setup() error recovery are Vue runtime behavior (per-beta detail:\n * CHANGELOG / whitepaper alignment log).\n *\n * `rootProps` are Vue's raw props, passed through unchanged: a plain value is\n * the value, and a FUNCTION is a getter that Vue calls for the value. So a\n * callback is wrapped, `{ onSave: () => onSave }`; passed bare it is called\n * and the component receives what it returned. (A function set on a\n * `defineWidget` element property is the opposite: it arrives as itself.)\n * Pinned by tests/vapor/vapor-subpath-wiring.test.ts.\n *\n * @example\n * import { createVaporChamberApp } from 'vapor-chamber';\n * import App from './App.vue';\n * createVaporChamberApp(App).mount('#app');\n *\n * @example Root props\n * createVaporChamberApp(App, { title: 'Cart', onSave: () => onSave }).mount('#app');\n */\nexport function createVaporChamberApp<TApp = any>(\n rootComponent: object,\n rootProps?: Record<string, unknown>,\n): TApp {\n const fn = getVaporAppFn();\n if (!fn) {\n // The bare \"Vapor required\" message was the same whether Vue was absent,\n // present-without-Vapor, or present-but-unreachable - three different\n // problems with three different fixes. vueDetectionHint() names which.\n throw new Error(\n // The diagnosis ships (vueDetectionHint says why); the advice is DEV's.\n `[vapor-chamber] Vue 3.6+ with Vapor mode required. ${vueDetectionHint()}` +\n (DEV ? ' For VDOM mode, use createApp() from vue instead.' : '')\n );\n }\n return fn(rootComponent, rootProps) as TApp;\n}\n\n/**\n * Returns the vaporInteropPlugin if available (Vue 3.6+).\n * Use this to enable mixed Vapor/VDOM component trees.\n *\n * Pass-through over Vue's plugin. The returned reference is safe to hold\n * across HMR cycles (Vue does not mutate the bridge).\n *\n * @example\n * import { createApp } from 'vue';\n * import { getVaporInteropPlugin } from 'vapor-chamber';\n * const plugin = getVaporInteropPlugin();\n * if (plugin) createApp(App).use(plugin).mount('#app');\n */\nexport function getVaporInteropPlugin(): any | null {\n return getVaporInteropRef();\n}\n\n// ---------------------------------------------------------------------------\n// Vapor Custom Elements (Vue 3.6 - defineVaporCustomElement introduced in 3.6.0-alpha.4, #14017)\n// ---------------------------------------------------------------------------\n\n/**\n * defineVaporCustomElement - create a custom element backed by Vapor rendering.\n *\n * Wraps Vue's `defineVaporCustomElement()` (introduced in 3.6.0-alpha.4, #14017). The generated custom\n * element uses Vapor's compiler-optimized rendering instead of the VDOM, giving\n * zero-overhead DOM updates inside shadow DOM. Safe to call with a reused\n * options object, and children re-render on reactive prop changes (beta.14+;\n * per-beta detail: CHANGELOG / whitepaper alignment log).\n *\n * Returns null if the Vapor runtime (Vue 3.6+) is not detected - check before calling\n * `customElements.define()`.\n *\n * @example\n * import { defineVaporCustomElement } from 'vapor-chamber';\n * const MyEl = defineVaporCustomElement({\n * props: { label: String },\n * // A Vapor setup() returns a BLOCK - real DOM nodes - not a render\n * // function or h() output. An SFC's compiler builds this from its template.\n * setup(props) {\n * const span = document.createElement('span');\n * span.textContent = String(props.label);\n * return span;\n * }\n * });\n * if (MyEl) customElements.define('vc-greeting', MyEl);\n */\nexport function defineVaporCustomElement<T = any>(options: object, extraOptions?: object): T | null {\n const fn = getDefineVaporCustomElementFn();\n if (!fn) {\n if (DEV) devWarnNoVapor(\"defineVaporCustomElement\");\n return null;\n }\n return (extraOptions !== undefined ? fn(options, extraOptions) : fn(options)) as T;\n}\n\n/**\n * defineVaporComponent - define a Vapor component with proper type inference.\n *\n * Wraps Vue's `defineVaporComponent()` (typed since 3.6.0-alpha.5, #13831). Use this to get full\n * TypeScript inference for props, emits, and slots in Vapor components.\n * Pass-through - emits/$attrs routing, v-once interop, scope IDs, and the\n * compiler optimizations are Vue behavior (per-beta detail: CHANGELOG /\n * whitepaper alignment log).\n *\n * Returns null if the Vapor runtime (Vue 3.6+) is not detected.\n *\n * @example\n * import { defineVaporComponent } from 'vapor-chamber';\n * const Comp = defineVaporComponent({\n * props: { count: Number },\n * emits: ['change'],\n * // Returns DOM nodes (a block), not h(): see defineVaporCustomElement above.\n * setup(props) {\n * const div = document.createElement('div');\n * div.textContent = `Count: ${props.count}`;\n * return div;\n * }\n * });\n */\nexport function defineVaporComponent<T = any>(options: object): T | null {\n const fn = getDefineVaporComponentFn();\n if (!fn) {\n if (DEV) devWarnNoVapor(\"defineVaporComponent\");\n return null;\n }\n return fn(options) as T;\n}\n\n/**\n * defineVaporAsyncComponent - define an async Vapor component for lazy loading.\n *\n * Wraps Vue's `defineVaporAsyncComponent()` (introduced in 3.6.0-alpha.3, #13059). Async Vapor\n * components are cached by VaporKeepAlive and hydrate under VDOM Suspense.\n * The loading placeholder receives the deferred component's props and slots\n * (beta.14+) - render a skeleton matching the final shape. Per-beta detail:\n * CHANGELOG / whitepaper alignment log.\n *\n * Returns null if the Vapor runtime (Vue 3.6+) is not detected.\n *\n * @example\n * import { defineVaporAsyncComponent } from 'vapor-chamber';\n * const AsyncPanel = defineVaporAsyncComponent(() => import('./Panel.vue'));\n */\nexport function defineVaporAsyncComponent<T = any>(\n loader: (() => Promise<unknown>) | object,\n): T | null {\n const fn = getDefineVaporAsyncComponentFn();\n if (!fn) {\n if (DEV) devWarnNoVapor(\"defineVaporAsyncComponent\");\n return null;\n }\n return fn(loader) as T;\n}\n\n// ---------------------------------------------------------------------------\n// defineVaporCommand\n// ---------------------------------------------------------------------------\n\n/**\n * defineVaporCommand - zero-overhead command for hot paths in Vapor mode.\n *\n * Unlike useCommand(), this skips reactive loading/error signal creation.\n * Ideal for high-frequency, fire-and-forget patterns where reactive\n * loading/error state isn't needed: scroll-position tracking, mousemove\n * sampling, telemetry / metrics events, debounced search, autosave.\n *\n * @example\n * const { dispatch } = defineVaporCommand('telemetryEvent', (cmd) => {\n * // forward to whatever metrics / analytics SDK you use\n * sendMetric(cmd.target.name, cmd.target.params);\n * });\n * dispatch({ name: 'page_view', params: { page: '/landing' } });\n */\nexport function defineVaporCommand(\n action: string,\n handler: Handler,\n options?: RegisterOptions\n) {\n const bus = getCommandBus<CommandMap>();\n const unregister = bus.register(action, handler, options);\n\n // `untracked`, like every other composable's dispatch. This one was the gap,\n // and the worst place for it: a dispatch is an ACTION, not a read, and this\n // helper is documented for hot paths in Vapor mode - i.e. the call most\n // likely to be made from inside a render effect, where the handler's reads\n // become that effect's dependencies and the component re-renders on state it\n // never mentions. Pass-through when Vue is absent, so the \"zero reactive\n // overhead\" claim above is unaffected for non-Vue consumers.\n function dispatch(target: any, payload?: any): CommandResult {\n return untracked(() => bus.dispatch(action, target, payload));\n }\n\n function dispose() { unregister(); }\n\n tryAutoCleanup(dispose);\n\n return { dispatch, dispose };\n}\n\n// ---------------------------------------------------------------------------\n// useVaporAsyncCommand\n// ---------------------------------------------------------------------------\n\n/**\n * useVaporAsyncCommand - async-aware command dispatch for Vapor components\n * used inside Suspense boundaries.\n *\n * Vue 3.6.0-beta.10 introduced proper async component hydration under VDOM\n * Suspense. This composable wraps an AsyncCommandBus dispatch with reactive\n * loading/error state, making it safe for `<script setup vapor>` components\n * that await async operations. Error-boundary rendering and scheduler-flush\n * behavior around async components are Vue runtime concerns (per-beta detail:\n * CHANGELOG / whitepaper alignment log).\n *\n * The dispatch function returns a Promise<CommandResult>, matching the\n * AsyncCommandBus interface. Use this when your commands hit async transports\n * (HTTP bridge, WS bridge) and you need awaitable results.\n *\n * Vue 3.6.0-rc.8: an awaiting `<script setup vapor>` compiles to an\n * `async setup()` that uses runtime-core's `withAsyncContext` and returns its\n * template as a render closure, and it registers with the NEAREST `<Suspense>`.\n * Composables called after the `await` - this one, `useCommand()` - arm their\n * cleanup on the restored component scope, including when the app unmounts\n * during the await. Pinned by tests/async-vapor-setup-fixture.test.ts.\n *\n * @example\n * // In a <script setup vapor> component under <Suspense>:\n * import { useVaporAsyncCommand } from 'vapor-chamber';\n * const { dispatch, loading, lastError } = useVaporAsyncCommand(asyncBus);\n * const result = await dispatch('orderCreate', { items: cart });\n */\nexport function useVaporAsyncCommand(asyncBus?: { dispatch: (action: string, target: any, payload?: any) => Promise<CommandResult> }) {\n const bus = asyncBus ?? (getCommandBus() as any);\n const loading = signal(false);\n const lastError = signal<Error | null>(null);\n\n // Intentionally hand-rolled, NOT routed through the shared runDispatch() that\n // useCommand / useCommandQuery use. A single async/await is\n // ~1.2x leaner on the dispatch wrapper than runDispatch's .then-chain (measured);\n // this is the awaited HTTP/WS path, so keep it lean. Do not \"consolidate\" into\n // runDispatch - the consistency isn't worth the wrapper overhead here.\n async function dispatch(action: string, target: any, payload?: any): Promise<CommandResult> {\n try {\n // Both opening writes are inside the try: a sync subscriber that throws\n // on either becomes a failed result and `loading` ends false (pinned by\n // tests/vapor-async-throwing-subscriber.test.ts).\n loading.value = true;\n lastError.value = null;\n // Untracked around the SYNCHRONOUS entry only - once the dispatch\n // suspends, the caller's effect has finished and there is no subscriber\n // left to leak into (pinned by the \"synchronous entry is the whole\n // exposure\" test). Same scope as runDispatch's, one call, so the leaner\n // hand-rolled wrapper above stays leaner.\n const result = await untracked(() => bus.dispatch(action, target, payload));\n if (!result.ok) lastError.value = result.error ?? null;\n return result;\n } catch (e) {\n const error = e as Error;\n lastError.value = error;\n return _errResult(error);\n } finally {\n loading.value = false;\n }\n }\n\n // Dispatch-only composable (no register/on, unlike useCommand) - there are no\n // subscriptions to tear down. Kept as a no-op for return-shape symmetry with the\n // other composables (callers may destructure `dispose`).\n function dispose() {}\n\n tryAutoCleanup(dispose);\n\n return { dispatch, loading, lastError, dispose };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,SAAS,eAAe,KAAmB;CACzC,QAAQ,KACN,mBAAmB,IAAI,kGACgB,iBAAiB,EAAE,iEAE5D;AACF;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,sBACd,eACA,WACM;CACN,MAAM,KAAK,cAAc;CACzB,IAAI,CAAC,IAIH,MAAM,IAAI,MAER,sDAAsD,iBAAiB,OACtE,MAAM,sDAAsD,GAC/D;CAEF,OAAO,GAAG,eAAe,SAAS;AACpC;;;;;;;;;;;;;;AAeA,SAAgB,wBAAoC;CAClD,OAAO,mBAAmB;AAC5B;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,SAAgB,yBAAkC,SAAiB,cAAiC;CAClG,MAAM,KAAK,8BAA8B;CACzC,IAAI,CAAC,IAAI;EACP,IAAI,KAAK,eAAe,0BAA0B;EAClD,OAAO;CACT;CACA,OAAQ,iBAAiB,KAAA,IAAY,GAAG,SAAS,YAAY,IAAI,GAAG,OAAO;AAC7E;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,qBAA8B,SAA2B;CACvE,MAAM,KAAK,0BAA0B;CACrC,IAAI,CAAC,IAAI;EACP,IAAI,KAAK,eAAe,sBAAsB;EAC9C,OAAO;CACT;CACA,OAAO,GAAG,OAAO;AACnB;;;;;;;;;;;;;;;;AAiBA,SAAgB,0BACd,QACU;CACV,MAAM,KAAK,+BAA+B;CAC1C,IAAI,CAAC,IAAI;EACP,IAAI,KAAK,eAAe,2BAA2B;EACnD,OAAO;CACT;CACA,OAAO,GAAG,MAAM;AAClB;;;;;;;;;;;;;;;;AAqBA,SAAgB,mBACd,QACA,SACA,SACA;CACA,MAAM,MAAM,cAA0B;CACtC,MAAM,aAAa,IAAI,SAAS,QAAQ,SAAS,OAAO;CASxD,SAAS,SAAS,QAAa,SAA8B;EAC3D,OAAO,gBAAgB,IAAI,SAAS,QAAQ,QAAQ,OAAO,CAAC;CAC9D;CAEA,SAAS,UAAU;EAAE,WAAW;CAAG;CAEnC,eAAe,OAAO;CAEtB,OAAO;EAAE;EAAU;CAAQ;AAC7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,SAAgB,qBAAqB,UAAiG;CACpI,MAAM,MAAM,YAAa,cAAc;CACvC,MAAM,UAAU,OAAO,KAAK;CAC5B,MAAM,YAAY,OAAqB,IAAI;CAO3C,eAAe,SAAS,QAAgB,QAAa,SAAuC;EAC1F,IAAI;GAIF,QAAQ,QAAQ;GAChB,UAAU,QAAQ;GAMlB,MAAM,SAAS,MAAM,gBAAgB,IAAI,SAAS,QAAQ,QAAQ,OAAO,CAAC;GAC1E,IAAI,CAAC,OAAO,IAAI,UAAU,QAAQ,OAAO,SAAS;GAClD,OAAO;EACT,SAAS,GAAG;GACV,MAAM,QAAQ;GACd,UAAU,QAAQ;GAClB,OAAOA,UAAW,KAAK;EACzB,UAAU;GACR,QAAQ,QAAQ;EAClB;CACF;CAKA,SAAS,UAAU,CAAC;CAEpB,eAAe,OAAO;CAEtB,OAAO;EAAE;EAAU;EAAS;EAAW;CAAQ;AACjD"}
1
+ {"version":3,"file":"chamber-vapor.js","names":["_errResult"],"sources":["../src/chamber-vapor.ts"],"sourcesContent":["/**\n * vapor-chamber - Vue 3.6+ Vapor-specific API\n *\n * This file renders nothing: it forwards Vue's own define* functions, so\n * rendering-side work (attrs fallthrough, interop, hydration) lands below it.\n * The wrappers and createVaporChamberApp take an opt-in return generic\n * (`<T = any>`) and `object`-typed options: importing Vue's Vapor types here\n * would put a hard `vue` type dependency on the main barrel and break Vue-less\n * command-bus consumers, so callers opt in: `defineVaporComponent<MyComp>(opts)`.\n *\n * Separated from chamber.ts to keep the core composable module CDCC-compliant.\n */\n\nimport {\n signal,\n tryAutoCleanup,\n untracked,\n getVaporAppFn,\n getVaporInteropRef,\n getDefineVaporCustomElementFn,\n getDefineVaporComponentFn,\n getDefineVaporAsyncComponentFn,\n vueDetectionHint,\n} from './chamber';\nimport type { BaseBus, CommandBus, Handler, RegisterOptions, CommandResult } from './command-bus';\nimport { _errResult } from './command-bus';\nimport { DEV } from './dev';\nimport { resolveBus, type BusOption, type ResultOn } from './shared-bus';\n\n/**\n * Why the `defineVapor*` wrappers warn instead of just returning null.\n *\n * Each returns `null` when the Vapor runtime is absent, and callers are\n * documented to check. But a bare `null` is the same failure shape that hid a\n * real bug for several releases: `tryKeepAliveHooks` guarded on an accessor\n * that silently answers \"no\" under Vapor, so a feature was inert with nothing\n * to see (rc.4 cycle - see tests/keepalive-input-scope-fixture.test.ts). Silent\n * negatives are how detection bugs survive.\n *\n * So the null stays - throwing would break the documented contract and the\n * tests that assert it - but it is not quiet. The call sites gate on\n * `DEV`, so the whole call (and every byte of message text) folds out of the\n * production IIFE builds; this helper is then unreferenced and tree-shaken.\n */\nfunction devWarnNoVapor(api: string): void {\n console.warn(\n `[vapor-chamber] ${api}() returned null - Vue 3.6+ with Vapor mode was not detected, ` +\n `so the component was NOT created. ${vueDetectionHint()} ` +\n 'For VDOM mode use the matching define* helper from vue instead.',\n );\n}\n\n/**\n * Create a Vapor app instance with vapor-chamber ready.\n * Requires Vue 3.6+. Throws if Vapor is not available.\n *\n * Pass-through over Vue's createVaporApp - scope-ID handling, HMR app-instance\n * refresh, and setup() error recovery are Vue runtime behavior (per-beta detail:\n * CHANGELOG / whitepaper alignment log).\n *\n * `rootProps` are Vue's raw props, passed through unchanged: a plain value is\n * the value, and a FUNCTION is a getter that Vue calls for the value. So a\n * callback is wrapped, `{ onSave: () => onSave }`; passed bare it is called\n * and the component receives what it returned. (A function set on a\n * `defineWidget` element property is the opposite: it arrives as itself.)\n * Pinned by tests/vapor/vapor-subpath-wiring.test.ts.\n *\n * @example\n * import { createVaporChamberApp } from 'vapor-chamber';\n * import App from './App.vue';\n * createVaporChamberApp(App).mount('#app');\n *\n * @example Root props\n * createVaporChamberApp(App, { title: 'Cart', onSave: () => onSave }).mount('#app');\n */\nexport function createVaporChamberApp<TApp = any>(\n rootComponent: object,\n rootProps?: Record<string, unknown>,\n): TApp {\n const fn = getVaporAppFn();\n if (!fn) {\n // The bare \"Vapor required\" message was the same whether Vue was absent,\n // present-without-Vapor, or present-but-unreachable - three different\n // problems with three different fixes. vueDetectionHint() names which.\n throw new Error(\n // The diagnosis ships (vueDetectionHint says why); the advice is DEV's.\n `[vapor-chamber] Vue 3.6+ with Vapor mode required. ${vueDetectionHint()}` +\n (DEV ? ' For VDOM mode, use createApp() from vue instead.' : '')\n );\n }\n return fn(rootComponent, rootProps) as TApp;\n}\n\n/**\n * Returns the vaporInteropPlugin if available (Vue 3.6+).\n * Use this to enable mixed Vapor/VDOM component trees.\n *\n * Pass-through over Vue's plugin. The returned reference is safe to hold\n * across HMR cycles (Vue does not mutate the bridge).\n *\n * @example\n * import { createApp } from 'vue';\n * import { getVaporInteropPlugin } from 'vapor-chamber';\n * const plugin = getVaporInteropPlugin();\n * if (plugin) createApp(App).use(plugin).mount('#app');\n */\nexport function getVaporInteropPlugin(): any | null {\n return getVaporInteropRef();\n}\n\n// ---------------------------------------------------------------------------\n// Vapor Custom Elements (Vue 3.6 - defineVaporCustomElement introduced in 3.6.0-alpha.4, #14017)\n// ---------------------------------------------------------------------------\n\n/**\n * defineVaporCustomElement - create a custom element backed by Vapor rendering.\n *\n * Wraps Vue's `defineVaporCustomElement()` (introduced in 3.6.0-alpha.4, #14017). The generated custom\n * element uses Vapor's compiler-optimized rendering instead of the VDOM, giving\n * zero-overhead DOM updates inside shadow DOM. Safe to call with a reused\n * options object, and children re-render on reactive prop changes (beta.14+;\n * per-beta detail: CHANGELOG / whitepaper alignment log).\n *\n * Returns null if the Vapor runtime (Vue 3.6+) is not detected - check before calling\n * `customElements.define()`.\n *\n * @example\n * import { defineVaporCustomElement } from 'vapor-chamber';\n * const MyEl = defineVaporCustomElement({\n * props: { label: String },\n * // A Vapor setup() returns a BLOCK - real DOM nodes - not a render\n * // function or h() output. An SFC's compiler builds this from its template.\n * setup(props) {\n * const span = document.createElement('span');\n * span.textContent = String(props.label);\n * return span;\n * }\n * });\n * if (MyEl) customElements.define('vc-greeting', MyEl);\n */\nexport function defineVaporCustomElement<T = any>(options: object, extraOptions?: object): T | null {\n const fn = getDefineVaporCustomElementFn();\n if (!fn) {\n if (DEV) devWarnNoVapor(\"defineVaporCustomElement\");\n return null;\n }\n return (extraOptions !== undefined ? fn(options, extraOptions) : fn(options)) as T;\n}\n\n/**\n * defineVaporComponent - define a Vapor component with proper type inference.\n *\n * Wraps Vue's `defineVaporComponent()` (typed since 3.6.0-alpha.5, #13831). Use this to get full\n * TypeScript inference for props, emits, and slots in Vapor components.\n * Pass-through - emits/$attrs routing, v-once interop, scope IDs, and the\n * compiler optimizations are Vue behavior (per-beta detail: CHANGELOG /\n * whitepaper alignment log).\n *\n * Returns null if the Vapor runtime (Vue 3.6+) is not detected.\n *\n * @example\n * import { defineVaporComponent } from 'vapor-chamber';\n * const Comp = defineVaporComponent({\n * props: { count: Number },\n * emits: ['change'],\n * // Returns DOM nodes (a block), not h(): see defineVaporCustomElement above.\n * setup(props) {\n * const div = document.createElement('div');\n * div.textContent = `Count: ${props.count}`;\n * return div;\n * }\n * });\n */\nexport function defineVaporComponent<T = any>(options: object): T | null {\n const fn = getDefineVaporComponentFn();\n if (!fn) {\n if (DEV) devWarnNoVapor(\"defineVaporComponent\");\n return null;\n }\n return fn(options) as T;\n}\n\n/**\n * defineVaporAsyncComponent - define an async Vapor component for lazy loading.\n *\n * Wraps Vue's `defineVaporAsyncComponent()` (introduced in 3.6.0-alpha.3, #13059). Async Vapor\n * components are cached by VaporKeepAlive and hydrate under VDOM Suspense.\n * The loading placeholder receives the deferred component's props and slots\n * (beta.14+) - render a skeleton matching the final shape. Per-beta detail:\n * CHANGELOG / whitepaper alignment log.\n *\n * Returns null if the Vapor runtime (Vue 3.6+) is not detected.\n *\n * @example\n * import { defineVaporAsyncComponent } from 'vapor-chamber';\n * const AsyncPanel = defineVaporAsyncComponent(() => import('./Panel.vue'));\n */\nexport function defineVaporAsyncComponent<T = any>(\n loader: (() => Promise<unknown>) | object,\n): T | null {\n const fn = getDefineVaporAsyncComponentFn();\n if (!fn) {\n if (DEV) devWarnNoVapor(\"defineVaporAsyncComponent\");\n return null;\n }\n return fn(loader) as T;\n}\n\n// ---------------------------------------------------------------------------\n// defineVaporCommand\n// ---------------------------------------------------------------------------\n\n/** `defineVaporCommand`'s options: how the handler registers, and on which bus. */\nexport type VaporCommandOptions<B extends BaseBus = BaseBus> = RegisterOptions & BusOption<B>;\n\n/**\n * defineVaporCommand - zero-overhead command for hot paths in Vapor mode.\n *\n * Unlike useCommand(), this skips reactive loading/error signal creation.\n * Ideal for high-frequency, fire-and-forget patterns where reactive\n * loading/error state isn't needed: scroll-position tracking, mousemove\n * sampling, telemetry / metrics events, debounced search, autosave.\n *\n * @example\n * const { dispatch } = defineVaporCommand('telemetryEvent', (cmd) => {\n * // forward to whatever metrics / analytics SDK you use\n * sendMetric(cmd.target.name, cmd.target.params);\n * });\n * dispatch({ name: 'page_view', params: { page: '/landing' } });\n */\nexport function defineVaporCommand<B extends BaseBus = CommandBus>(\n action: string,\n handler: Handler,\n options?: VaporCommandOptions<B>\n) {\n const bus = resolveBus(options?.bus);\n // The options go to register() as they are: it reads the register fields,\n // and an undo's stored copy keeps `bus`, which nothing reads.\n const unregister = bus.register(action, handler, options);\n\n // `untracked`, like every other composable's dispatch. This one was the gap,\n // and the worst place for it: a dispatch is an ACTION, not a read, and this\n // helper is documented for hot paths in Vapor mode - i.e. the call most\n // likely to be made from inside a render effect, where the handler's reads\n // become that effect's dependencies and the component re-renders on state it\n // never mentions. Pass-through when Vue is absent, so the \"zero reactive\n // overhead\" claim above is unaffected for non-Vue consumers.\n function dispatch(target: any, payload?: any): ResultOn<B> {\n return untracked(() => bus.dispatch(action, target, payload)) as ResultOn<B>;\n }\n\n function dispose() { unregister(); }\n\n tryAutoCleanup(dispose);\n\n return { dispatch, dispose };\n}\n\n// ---------------------------------------------------------------------------\n// useVaporAsyncCommand\n// ---------------------------------------------------------------------------\n\n/**\n * useVaporAsyncCommand - async-aware command dispatch for Vapor components\n * used inside Suspense boundaries.\n *\n * Vue 3.6.0-beta.10 introduced proper async component hydration under VDOM\n * Suspense. This composable wraps an AsyncCommandBus dispatch with reactive\n * loading/error state, making it safe for `<script setup vapor>` components\n * that await async operations. Error-boundary rendering and scheduler-flush\n * behavior around async components are Vue runtime concerns (per-beta detail:\n * CHANGELOG / whitepaper alignment log).\n *\n * The dispatch function returns a Promise<CommandResult>, matching the\n * AsyncCommandBus interface. Use this when your commands hit async transports\n * (HTTP bridge, WS bridge) and you need awaitable results.\n *\n * Vue 3.6.0-rc.8: an awaiting `<script setup vapor>` compiles to an\n * `async setup()` that uses runtime-core's `withAsyncContext` and returns its\n * template as a render closure, and it registers with the NEAREST `<Suspense>`.\n * Composables called after the `await` - this one, `useCommand()` - arm their\n * cleanup on the restored component scope, including when the app unmounts\n * during the await. Pinned by tests/async-vapor-setup-fixture.test.ts.\n *\n * @example\n * // In a <script setup vapor> component under <Suspense>:\n * import { useVaporAsyncCommand } from 'vapor-chamber';\n * const { dispatch, loading, lastError } = useVaporAsyncCommand(asyncBus);\n * const result = await dispatch('orderCreate', { items: cart });\n */\nexport function useVaporAsyncCommand(asyncBus?: { dispatch: (action: string, target: any, payload?: any) => Promise<CommandResult> }) {\n const bus: any = resolveBus(asyncBus as BaseBus | undefined);\n const loading = signal(false);\n const lastError = signal<Error | null>(null);\n // In flight: `loading` is true while any dispatch is, as runDispatch's\n // (tests/composable-overlap.test.ts).\n let n = 0;\n\n // Intentionally hand-rolled, NOT routed through the shared runDispatch() that\n // useCommand / useCommandQuery use. A single async/await is\n // ~1.2x leaner on the dispatch wrapper than runDispatch's .then-chain (measured);\n // this is the awaited HTTP/WS path, so keep it lean. Do not \"consolidate\" into\n // runDispatch - the consistency isn't worth the wrapper overhead here.\n async function dispatch(action: string, target: any, payload?: any): Promise<CommandResult> {\n try {\n // Both opening writes are inside the try: a sync subscriber that throws\n // on either becomes a failed result and `loading` ends false (pinned by\n // tests/vapor-async-throwing-subscriber.test.ts).\n if (n++ === 0) loading.value = true;\n lastError.value = null;\n // Untracked around the SYNCHRONOUS entry only - once the dispatch\n // suspends, the caller's effect has finished and there is no subscriber\n // left to leak into (pinned by the \"synchronous entry is the whole\n // exposure\" test). Same scope as runDispatch's, one call, so the leaner\n // hand-rolled wrapper above stays leaner.\n const result = await untracked(() => bus.dispatch(action, target, payload));\n if (!result.ok) lastError.value = result.error ?? null;\n return result;\n } catch (e) {\n const error = e as Error;\n lastError.value = error;\n return _errResult(error);\n } finally {\n if (--n === 0) loading.value = false;\n }\n }\n\n // Dispatch-only composable (no register/on, unlike useCommand) - there are no\n // subscriptions to tear down. Kept as a no-op for return-shape symmetry with the\n // other composables (callers may destructure `dispose`).\n function dispose() {}\n\n tryAutoCleanup(dispose);\n\n return { dispatch, loading, lastError, dispose };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,SAAS,eAAe,KAAmB;CACzC,QAAQ,KACN,mBAAmB,IAAI,kGACgB,iBAAiB,EAAE,iEAE5D;AACF;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,sBACd,eACA,WACM;CACN,MAAM,KAAK,cAAc;CACzB,IAAI,CAAC,IAIH,MAAM,IAAI,MAER,sDAAsD,iBAAiB,OACtE,MAAM,sDAAsD,GAC/D;CAEF,OAAO,GAAG,eAAe,SAAS;AACpC;;;;;;;;;;;;;;AAeA,SAAgB,wBAAoC;CAClD,OAAO,mBAAmB;AAC5B;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,SAAgB,yBAAkC,SAAiB,cAAiC;CAClG,MAAM,KAAK,8BAA8B;CACzC,IAAI,CAAC,IAAI;EACP,IAAI,KAAK,eAAe,0BAA0B;EAClD,OAAO;CACT;CACA,OAAQ,iBAAiB,KAAA,IAAY,GAAG,SAAS,YAAY,IAAI,GAAG,OAAO;AAC7E;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,qBAA8B,SAA2B;CACvE,MAAM,KAAK,0BAA0B;CACrC,IAAI,CAAC,IAAI;EACP,IAAI,KAAK,eAAe,sBAAsB;EAC9C,OAAO;CACT;CACA,OAAO,GAAG,OAAO;AACnB;;;;;;;;;;;;;;;;AAiBA,SAAgB,0BACd,QACU;CACV,MAAM,KAAK,+BAA+B;CAC1C,IAAI,CAAC,IAAI;EACP,IAAI,KAAK,eAAe,2BAA2B;EACnD,OAAO;CACT;CACA,OAAO,GAAG,MAAM;AAClB;;;;;;;;;;;;;;;;AAwBA,SAAgB,mBACd,QACA,SACA,SACA;CACA,MAAM,MAAM,WAAW,SAAS,GAAG;CAGnC,MAAM,aAAa,IAAI,SAAS,QAAQ,SAAS,OAAO;CASxD,SAAS,SAAS,QAAa,SAA4B;EACzD,OAAO,gBAAgB,IAAI,SAAS,QAAQ,QAAQ,OAAO,CAAC;CAC9D;CAEA,SAAS,UAAU;EAAE,WAAW;CAAG;CAEnC,eAAe,OAAO;CAEtB,OAAO;EAAE;EAAU;CAAQ;AAC7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,SAAgB,qBAAqB,UAAiG;CACpI,MAAM,MAAW,WAAW,QAA+B;CAC3D,MAAM,UAAU,OAAO,KAAK;CAC5B,MAAM,YAAY,OAAqB,IAAI;CAG3C,IAAI,IAAI;CAOR,eAAe,SAAS,QAAgB,QAAa,SAAuC;EAC1F,IAAI;GAIF,IAAI,QAAQ,GAAG,QAAQ,QAAQ;GAC/B,UAAU,QAAQ;GAMlB,MAAM,SAAS,MAAM,gBAAgB,IAAI,SAAS,QAAQ,QAAQ,OAAO,CAAC;GAC1E,IAAI,CAAC,OAAO,IAAI,UAAU,QAAQ,OAAO,SAAS;GAClD,OAAO;EACT,SAAS,GAAG;GACV,MAAM,QAAQ;GACd,UAAU,QAAQ;GAClB,OAAOA,UAAW,KAAK;EACzB,UAAU;GACR,IAAI,EAAE,MAAM,GAAG,QAAQ,QAAQ;EACjC;CACF;CAKA,SAAS,UAAU,CAAC;CAEpB,eAAe,OAAO;CAEtB,OAAO;EAAE;EAAU;EAAS;EAAW;CAAQ;AACjD"}