vapor-chamber 1.25.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1121 -2
- package/README.md +109 -50
- package/ROADMAP.md +13 -13
- package/dist/LICENSE.txt +8 -2
- package/dist/blade.js +6 -6
- package/dist/blade.js.map +1 -1
- package/dist/chamber-vapor.d.ts +10 -0
- package/dist/chamber-vapor.d.ts.map +1 -1
- package/dist/chamber-vapor.js +5 -4
- package/dist/chamber-vapor.js.map +1 -1
- package/dist/chamber.d.ts +6 -31
- package/dist/chamber.d.ts.map +1 -1
- package/dist/chamber.js +139 -98
- package/dist/chamber.js.map +1 -1
- package/dist/command-bus.d.ts +75 -80
- package/dist/command-bus.d.ts.map +1 -1
- package/dist/command-bus.js +209 -150
- package/dist/command-bus.js.map +1 -1
- package/dist/dict.d.ts +1 -1
- package/dist/dict.js.map +1 -1
- package/dist/directives.d.ts +9 -1
- package/dist/directives.d.ts.map +1 -1
- package/dist/directives.js +2 -2
- package/dist/directives2.js +12 -4
- package/dist/directives2.js.map +1 -1
- package/dist/failure.d.ts +89 -0
- package/dist/failure.d.ts.map +1 -0
- package/dist/failure.js +70 -0
- package/dist/failure.js.map +1 -0
- package/dist/fast-lane.d.ts +4 -3
- package/dist/fast-lane.d.ts.map +1 -1
- package/dist/fast-lane.js +29 -18
- package/dist/fast-lane.js.map +1 -1
- package/dist/form.d.ts.map +1 -1
- package/dist/freeze.js.map +1 -1
- package/dist/http-cache.d.ts +21 -2
- package/dist/http-cache.d.ts.map +1 -1
- package/dist/http-errors.d.ts +26 -2
- package/dist/http-errors.d.ts.map +1 -1
- package/dist/http.d.ts +55 -36
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +221 -109
- package/dist/http.js.map +1 -1
- package/dist/iife-core.js +1 -1
- package/dist/iife-elements.d.ts +6 -0
- package/dist/iife-elements.d.ts.map +1 -1
- package/dist/iife-elements.js +1 -1
- package/dist/iife-elements.js.map +1 -1
- package/dist/iife.js +2 -2
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +491 -86
- package/dist/index.js.map +1 -1
- package/dist/keys.js +11 -9
- package/dist/keys.js.map +1 -1
- package/dist/ledger.d.ts +4 -0
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +20 -6
- package/dist/ledger.js.map +1 -1
- package/dist/library-names.d.ts +15 -0
- package/dist/library-names.d.ts.map +1 -0
- package/dist/library-names.js +13 -0
- package/dist/library-names.js.map +1 -0
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +4 -3
- package/dist/mcp.js.map +1 -1
- package/dist/outbox.d.ts +30 -14
- package/dist/outbox.d.ts.map +1 -1
- package/dist/outbox.js +24 -15
- package/dist/outbox.js.map +1 -1
- package/dist/plugins-core.d.ts +3 -2
- package/dist/plugins-core.d.ts.map +1 -1
- package/dist/plugins-core.js +32 -25
- package/dist/plugins-core.js.map +1 -1
- package/dist/plugins-extra.d.ts +3 -1
- package/dist/plugins-extra.d.ts.map +1 -1
- package/dist/plugins-io.d.ts.map +1 -1
- package/dist/plugins-io.js +2 -0
- package/dist/plugins-io.js.map +1 -1
- package/dist/plugins-schema.d.ts.map +1 -1
- package/dist/plugins-schema.js +7 -8
- package/dist/plugins-schema.js.map +1 -1
- package/dist/poll-with.d.ts +38 -0
- package/dist/poll-with.d.ts.map +1 -0
- package/dist/reactive.js +1 -1
- package/dist/reactive.js.map +1 -1
- package/dist/router/blade.d.ts.map +1 -1
- package/dist/router/dom.d.ts.map +1 -1
- package/dist/router/engine.d.ts +6 -2
- package/dist/router/engine.d.ts.map +1 -1
- package/dist/router/errors.d.ts +25 -16
- package/dist/router/errors.d.ts.map +1 -1
- package/dist/router/index.d.ts +5 -4
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +2 -2
- package/dist/router/loaders.d.ts +12 -3
- package/dist/router/loaders.d.ts.map +1 -1
- package/dist/router/remote.js +1 -1
- package/dist/router/table.d.ts +2 -1
- package/dist/router/table.d.ts.map +1 -1
- package/dist/router/types.d.ts +2 -1
- package/dist/router/types.d.ts.map +1 -1
- package/dist/router/vapor.d.ts +1 -1
- package/dist/router/vapor.d.ts.map +1 -1
- package/dist/router/vapor.js +11 -8
- package/dist/router/vapor.js.map +1 -1
- package/dist/router/vdom.js +1 -1
- package/dist/router/vdom.js.map +1 -1
- package/dist/router-fetch/index.d.ts.map +1 -1
- package/dist/router-fetch/index.js +3 -2
- package/dist/router-fetch/index.js.map +1 -1
- package/dist/router.js +142 -89
- package/dist/router.js.map +1 -1
- package/dist/schema.d.ts +1 -3
- package/dist/schema.d.ts.map +1 -1
- package/dist/settled.d.ts.map +1 -1
- package/dist/settled.js +1 -1
- package/dist/settled.js.map +1 -1
- package/dist/shared-bus.d.ts +46 -0
- package/dist/shared-bus.d.ts.map +1 -0
- package/dist/shared-bus.js +24 -0
- package/dist/shared-bus.js.map +1 -0
- package/dist/ssr.d.ts +2 -2
- package/dist/ssr.d.ts.map +1 -1
- package/dist/ssr2.js +5 -6
- package/dist/ssr2.js.map +1 -1
- package/dist/store/core.d.ts +4 -0
- package/dist/store/core.d.ts.map +1 -0
- package/dist/store/core.js +34 -0
- package/dist/store/core.js.map +1 -0
- package/dist/store-base.d.ts +104 -0
- package/dist/store-base.d.ts.map +1 -0
- package/dist/store-base.js +217 -0
- package/dist/store-base.js.map +1 -0
- package/dist/store-core.d.ts +4 -0
- package/dist/store-core.d.ts.map +1 -0
- package/dist/store.d.ts +22 -55
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +39 -80
- package/dist/store.js.map +1 -1
- package/dist/testing.d.ts.map +1 -1
- package/dist/transitions/vapor.d.ts +102 -0
- package/dist/transitions/vapor.d.ts.map +1 -0
- package/dist/transitions/vapor.js +107 -0
- package/dist/transitions/vapor.js.map +1 -0
- package/dist/transitions.d.ts +8 -1
- package/dist/transitions.d.ts.map +1 -1
- package/dist/transitions2.js +2 -1
- package/dist/transitions2.js.map +1 -1
- package/dist/transports.d.ts +18 -7
- package/dist/transports.d.ts.map +1 -1
- package/dist/transports2.js +100 -97
- package/dist/transports2.js.map +1 -1
- package/dist/vapor-chamber-core.iife.js +553 -301
- package/dist/vapor-chamber-core.iife.js.map +1 -1
- package/dist/vapor-chamber-core.iife.min.js +1 -1
- package/dist/vapor-chamber-elements.iife.js +601 -336
- package/dist/vapor-chamber-elements.iife.js.map +1 -1
- package/dist/vapor-chamber-elements.iife.min.js +1 -1
- package/dist/vapor-chamber.iife.js +815 -451
- package/dist/vapor-chamber.iife.js.map +1 -1
- package/dist/vapor-chamber.iife.min.js +1 -1
- package/dist/vapor.d.ts +15 -1
- package/dist/vapor.d.ts.map +1 -1
- package/dist/vapor.js +6 -3
- package/dist/vapor.js.map +1 -1
- package/dist/vite-hmr.d.ts +33 -5
- package/dist/vite-hmr.d.ts.map +1 -1
- package/dist/vite-hmr.js +14 -7
- package/dist/vite-hmr.js.map +1 -1
- package/dist/vitest.d.ts.map +1 -1
- package/dist/vitest.js +1 -0
- package/dist/vitest.js.map +1 -1
- package/dist/vue.js +1 -1
- package/package.json +25 -8
- package/scripts/bench-ratios-reporter.mjs +130 -91
- package/scripts/browser-channel.mjs +34 -0
- package/scripts/build.mjs +8 -0
- package/scripts/check-ascii.mjs +1 -1
- package/scripts/check-size.mjs +103 -3
- package/scripts/gate.mjs +7 -6
- package/scripts/generate-errors-doc.mjs +60 -0
- package/scripts/stamp-docs.mjs +13 -8
- package/scripts/test-counts-reporter.mjs +3 -2
- package/src/chamber-vapor.ts +15 -2
- package/src/chamber.ts +331 -227
- package/src/command-bus.ts +335 -269
- package/src/dict.ts +1 -1
- package/src/directives.ts +22 -3
- package/src/failure.ts +178 -0
- package/src/fast-lane.ts +56 -47
- package/src/form.ts +23 -13
- package/src/http-cache.ts +68 -24
- package/src/http-errors.ts +44 -8
- package/src/http.ts +269 -146
- package/src/iife-elements.ts +6 -0
- package/src/index.ts +11 -2
- package/src/ledger.ts +32 -6
- package/src/library-names.ts +22 -0
- package/src/mcp.ts +1 -1
- package/src/outbox.ts +74 -39
- package/src/plugins-core.ts +26 -22
- package/src/plugins-extra.ts +16 -24
- package/src/plugins-io.ts +8 -1
- package/src/plugins-schema.ts +6 -8
- package/src/poll-with.ts +94 -0
- package/src/router/blade.ts +11 -17
- package/src/router/composables.ts +1 -1
- package/src/router/dom.ts +17 -14
- package/src/router/engine.ts +147 -65
- package/src/router/errors.ts +67 -61
- package/src/router/index.ts +36 -25
- package/src/router/loaders.ts +15 -5
- package/src/router/menu.ts +3 -3
- package/src/router/outlet.ts +1 -1
- package/src/router/revalidate.ts +2 -2
- package/src/router/table.ts +7 -6
- package/src/router/types.ts +2 -1
- package/src/router/vapor.ts +47 -22
- package/src/router-fetch/index.ts +6 -2
- package/src/schema.ts +71 -20
- package/src/settled.ts +4 -1
- package/src/shared-bus.ts +70 -0
- package/src/ssr.ts +12 -10
- package/src/store/core.ts +45 -0
- package/src/store-base.ts +427 -0
- package/src/store.ts +46 -206
- package/src/testing.ts +35 -29
- package/src/transitions/vapor.ts +210 -0
- package/src/transitions.ts +8 -1
- package/src/transports.ts +124 -117
- package/src/vapor.ts +33 -2
- package/src/vite-hmr.ts +54 -12
- package/src/vitest.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,1125 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## v1.26.0 - 2026-10-04
|
|
6
|
+
|
|
7
|
+
### Fixed (the 1.26 evaluation's bug list)
|
|
8
|
+
|
|
9
|
+
Each fix has a test that failed before it; the record is
|
|
10
|
+
`docs/rc-alignment-log.md`, section 35.42.
|
|
11
|
+
|
|
12
|
+
- **esbuild and webpack shipped all of Vue to every app using the
|
|
13
|
+
composables.** The root finds Vue at runtime through `import('vue')`, which
|
|
14
|
+
both bundlers resolve to the whole namespace, even in an app wired through
|
|
15
|
+
`vapor-chamber/vue`. That lookup now sits behind a build flag,
|
|
16
|
+
`__VC_WIRED_BUILD__`: `vaporChamberWire()` defines it in a Vite build, and an
|
|
17
|
+
esbuild or webpack app that imports its composables from `vapor-chamber/vue`
|
|
18
|
+
(or `/vapor`) adds `define: { __VC_WIRED_BUILD__: 'true' }`. With it the
|
|
19
|
+
lookup leaves the bundle, and so does webpack's "Critical dependency"
|
|
20
|
+
warning. Without it nothing changes, which keeps a root-only Vue app working
|
|
21
|
+
on the Vite dev server, esbuild and webpack. `tests/root-probe-builds.test.ts`.
|
|
22
|
+
- **`getCommandBus()` no longer loads Vue in a Vue-less app.** The shared bus
|
|
23
|
+
(`getCommandBus`, `setCommandBus`, `resetCommandBus`) moved to a module
|
|
24
|
+
without the lookup; every import path is unchanged.
|
|
25
|
+
- **`router.destroy()` cancels the navigation in flight.** A navigation held
|
|
26
|
+
on a lazy component or a guard used to commit after the router was
|
|
27
|
+
destroyed, writing the history; it now answers `cancelled`, `isLoading`
|
|
28
|
+
returns to false, and a cancelled Back navigation walks no history.
|
|
29
|
+
`tests/router/destroy-cancels-navigation.test.ts`.
|
|
30
|
+
- **The HTTP client's cache respects writes and invalidations.** A read on the
|
|
31
|
+
wire across `invalidateCache()` or `clearCache()` is returned but not
|
|
32
|
+
stored, and a later read fetches instead of joining it; a successful
|
|
33
|
+
`post` / `put` / `patch` / `delete` drops the cached entry for its URL.
|
|
34
|
+
`tests/http-cache-writes.test.ts`.
|
|
35
|
+
- **`revalidateRoutes` re-reads past the loader cache.** With
|
|
36
|
+
`fetchLoaders({ cache })` a refresh after a command put the pre-command
|
|
37
|
+
data back on the page. The loader context gains `refresh` (true for a
|
|
38
|
+
refresh, false on a navigation), and `fetchLoaders` reads past its cache
|
|
39
|
+
then. `tests/router-fetch/revalidate-past-cache.test.ts`.
|
|
40
|
+
- **`useSharedCommandState().isLoading` across a route change, and on a
|
|
41
|
+
sealed bus.** A key in flight when the last holder left read false for the
|
|
42
|
+
next one; it now reads lit until the command settles. On a sealed bus,
|
|
43
|
+
`isLoading()` threw `core:refused:bus` whenever tracking had to start after
|
|
44
|
+
`seal()`; it now works there, and the bus stays sealed.
|
|
45
|
+
`tests/shared-state-handover.test.ts`.
|
|
46
|
+
- **`Retry-After` is read by RFC 9110's grammar**: digits, or an HTTP-date.
|
|
47
|
+
`-5`, `1.5`, `1e1`, `0x10` and `+3` were read as waits (-5,000 ms for
|
|
48
|
+
`-5`); they are ignored now. `tests/retry-after-grammar.test.ts`.
|
|
49
|
+
- **`persist()` without `getState` throws a `TypeError` at setup.** It used to
|
|
50
|
+
build a plugin that failed every save and stored nothing.
|
|
51
|
+
`tests/persist-requires-getstate.test.ts`.
|
|
52
|
+
|
|
53
|
+
The IIFE size budgets were raised to the measured sizes (owner, 2026-10-01);
|
|
54
|
+
the per-fix ledger is in `scripts/check-size.mjs`.
|
|
55
|
+
|
|
56
|
+
### Faster, and two more fixes (the perf-1.26 audit)
|
|
57
|
+
|
|
58
|
+
Every speed figure is a process-replicated A/B of the built `dist/` (10
|
|
59
|
+
processes per comparison, a per-function A/A gate), measured with
|
|
60
|
+
`npm run ab` at two call lengths and recorded in `docs/rc-alignment-log.md`,
|
|
61
|
+
sections 35.44 and 35.53; the method is `docs/V8-RULES.md`, rules 12 to 18.
|
|
62
|
+
Against v1.25.0 no measured path is slower.
|
|
63
|
+
|
|
64
|
+
- **`useCommand().dispatch` and `useCommandQuery().query` allocate no
|
|
65
|
+
closure per call**: 0.915-0.934x, 2.7-3.9 ns less per dispatch.
|
|
66
|
+
- **A scoped plugin's `actions` filter allocates nothing per dispatch**
|
|
67
|
+
(`cache`, `circuitBreaker`, `rateLimit`, `metrics`, `serialize`,
|
|
68
|
+
`idempotent`, `supersede`, the outbox): 0.979-0.986x on a chain of four
|
|
69
|
+
scoped plugins and a listener, 4.6-6.6 ns.
|
|
70
|
+
- **Per-key `isLoading` tracking is about a quarter cheaper per dispatch**
|
|
71
|
+
(about 192 -> 146 ns): a key gets its reactive flag only when `isLoading()`
|
|
72
|
+
reads it (0.969-0.982x, 3.6-6.1 ns), a dispatch is matched to its settle
|
|
73
|
+
through a `Map` (0.858-0.871x, 24-26 ns), and the keys are a map by action,
|
|
74
|
+
then by target, so a dispatch builds no key string (0.892-0.905x, 15-18 ns;
|
|
75
|
+
log section 35.60).
|
|
76
|
+
- **Fixed: `isLoading` keys could collide across the `:`.** The action
|
|
77
|
+
`'a:b'` with target `'c'` and the action `'a'` with target `'b:c'` shared a
|
|
78
|
+
flag. They no longer do; every other key equality is unchanged (`1` and
|
|
79
|
+
`'1'` still share one, as in `commandKey`). `tests/isloading-keys.test.ts`.
|
|
80
|
+
- **A build profile, and `isLoading` keeps idle slots for reuse in it.**
|
|
81
|
+
`vaporChamberWire({ profile: 'lean' })` trades speed for memory; the
|
|
82
|
+
default, `'performance'`, trades memory for speed. It is ONE build-time
|
|
83
|
+
switch for every such trade, and it costs nothing per call: the plugin
|
|
84
|
+
defines `__VC_LEAN__`, so the choice folds out of the build. **The plugin's
|
|
85
|
+
output changes for every user:** with no option it now also defines
|
|
86
|
+
`__VC_LEAN__: 'false'`, in a build and under the dev server, unless the
|
|
87
|
+
app's config defines `__VC_LEAN__` itself (an esbuild or webpack app can
|
|
88
|
+
define it with one line). The IIFE builds are performance. The first trade:
|
|
89
|
+
in performance, a key nobody reads keeps its slot when it settles while its
|
|
90
|
+
action holds 256 or fewer, so its next dispatch allocates nothing
|
|
91
|
+
(0.848-0.865x, 21-22 ns per tracked dispatch, log section 35.61); lean drops
|
|
92
|
+
it at once, as before. `tests/isloading-profile.test.ts`.
|
|
93
|
+
- **A tracked dispatch on a sync bus is faster again: 0.645x, 44 ns less.**
|
|
94
|
+
`isLoading` matched each dispatch's start to its settle through a Map keyed
|
|
95
|
+
by the command; on a sync bus, where commands settle innermost first, it is
|
|
96
|
+
a stack now, still matched by identity, so a settle with no start (a query,
|
|
97
|
+
an emit) takes nothing. In every profile (log section 35.66). The async bus
|
|
98
|
+
keeps the Map. `tests/isloading-nesting.test.ts`.
|
|
99
|
+
- **Composable creation is back to v1.25.0's speed.** The build-flag check
|
|
100
|
+
added for the Vue lookup above ran on every creation, 7-9% of it, in any
|
|
101
|
+
build that does not define the flag; a cheap test now comes first.
|
|
102
|
+
- **Fixed: `useSharedCommandState().dispose()` called twice by one holder**
|
|
103
|
+
(by hand, then by its scope) released the shared state under the next
|
|
104
|
+
holder, whose `isLoading()` then never lit; before that change it could
|
|
105
|
+
split the state in two. Each holder now counts out once.
|
|
106
|
+
`tests/shared-state-dispose-twice.test.ts`.
|
|
107
|
+
- **Fixed: the HTTP cache dropped more than it had to.** An invalidation of
|
|
108
|
+
one URL, and every successful write, kept every cacheable read then in
|
|
109
|
+
flight from being stored, whatever its URL. Only a read of the invalidated
|
|
110
|
+
URL is kept from being stored now. `tests/http-cache-unrelated-invalidation.test.ts`.
|
|
111
|
+
|
|
112
|
+
The full IIFE budget was raised to the measured size again (owner: speed over
|
|
113
|
+
size); the ledger is in `scripts/check-size.mjs`.
|
|
114
|
+
|
|
115
|
+
### Fixed: dispatch depth, and listeners changed during a dispatch
|
|
116
|
+
|
|
117
|
+
Each fix has a test that failed before it; the record is
|
|
118
|
+
`docs/rc-alignment-log.md`, section 35.67.
|
|
119
|
+
|
|
120
|
+
- **An async bus refused concurrent dispatches as if they were nested.** Its
|
|
121
|
+
depth counted dispatches in flight, so the 17th of 17 parallel, unnested
|
|
122
|
+
dispatches failed with `core:exceeded:depth`. Depth is now nesting, on both
|
|
123
|
+
buses: a dispatch is its parent's depth + 1, where the parent is the
|
|
124
|
+
dispatch whose handler, hook or listener started it. This also stops three
|
|
125
|
+
loops the old counter let run forever or overflow: a listener or after-hook
|
|
126
|
+
that re-dispatches its own action, and an async query that queries itself.
|
|
127
|
+
A loop through your own `await` is not followed.
|
|
128
|
+
- **A sync `query()` that queried itself overflowed the stack.** Queries now
|
|
129
|
+
count on the same depth as dispatches and stop at 16. A query costs about
|
|
130
|
+
1.5 ns more for it, as a dispatch already does.
|
|
131
|
+
- **A listener could be skipped, or run twice, when listeners changed during
|
|
132
|
+
a dispatch.** One that removed an earlier peer and added one skipped the
|
|
133
|
+
next; one that removed itself and a later peer re-ran the one before it.
|
|
134
|
+
A dispatch, `emit`, the fast lane's `'live'` emit and `createTestBus()` now
|
|
135
|
+
call the listeners that existed when they started: one removed during it
|
|
136
|
+
does not run, one added during it runs from the next (the rule of DOM's
|
|
137
|
+
`EventTarget`). **This changes one behaviour:** a listener added during a
|
|
138
|
+
dispatch used to run in that same dispatch. Listener fan-out measured
|
|
139
|
+
0.964-0.974x.
|
|
140
|
+
|
|
141
|
+
The IIFE budgets and the Blade-consumer ceiling were raised to the measured
|
|
142
|
+
sizes (owner: correctness first); the per-fix ledger is in
|
|
143
|
+
`scripts/check-size.mjs` and `tests/esm-treeshake.test.ts`.
|
|
144
|
+
|
|
145
|
+
### Fixed: a deduped GET obeyed another caller's signal
|
|
146
|
+
|
|
147
|
+
A GET joining one already in flight for the same URL (the client's `dedupe`,
|
|
148
|
+
on by default) shared the first caller's request with its abort: the first
|
|
149
|
+
caller's `signal` rejected every caller, and a joining caller's own `signal`
|
|
150
|
+
was ignored. With `supersede` and a handler passing `cmd.signal` to
|
|
151
|
+
`http.get()`, the newest dispatch joined the request the plugin had just
|
|
152
|
+
aborted and failed as well. Each caller's signal now rejects its own promise
|
|
153
|
+
only; the fetch is cancelled once every caller holding it has aborted (a
|
|
154
|
+
caller without a signal holds it until it lands), and a GET made after that
|
|
155
|
+
starts a fresh request. A GET with no signal costs what it cost.
|
|
156
|
+
`dist/http.js` +1,407 B raw / +266 brotli, in no IIFE.
|
|
157
|
+
`tests/http-dedupe-signal.test.ts`; log section 35.46.
|
|
158
|
+
|
|
159
|
+
### Fixed: a store's `$reset()` was seen by no plugin
|
|
160
|
+
|
|
161
|
+
`$reset()` wrote the store's state directly, so `persist` never saved it and
|
|
162
|
+
the next reload brought back the state from before the reset; `history`, the
|
|
163
|
+
devtools and every listener missed it too. It now dispatches `<id>$reset`
|
|
164
|
+
(`cart$reset`), registered beside the store's actions, and returns the
|
|
165
|
+
dispatch result. Changed with it: on an async bus the reset lands when the
|
|
166
|
+
returned promise settles, not at the call; a plugin can refuse it;
|
|
167
|
+
`inspectBus().actions` lists it. Action names containing `$` are reserved for
|
|
168
|
+
the library, and a bus's `naming` rule no longer checks them (the check runs
|
|
169
|
+
only once a name has failed the pattern). `dist/store.js` +147 B raw / +36
|
|
170
|
+
brotli; the IIFEs +17 raw and +3 to +15 brotli, from the naming check.
|
|
171
|
+
`tests/store-reset-command.test.ts`; log section 35.70. `$reset()` goes back
|
|
172
|
+
to what `state()` returns, so a `state()` that reads storage resets to the
|
|
173
|
+
saved record; `docs/store.md` shows loading it through an action instead (log
|
|
174
|
+
section 35.72).
|
|
175
|
+
|
|
176
|
+
### Added: a development warning when the shared bus is replaced after use
|
|
177
|
+
|
|
178
|
+
`getCommandBus()` still creates a bus when none was set, the normal path of
|
|
179
|
+
a sync app. `setCommandBus()` replacing that bus after it was handed out now
|
|
180
|
+
warns, once, in development: code that already called `getCommandBus()`
|
|
181
|
+
keeps the old bus, so dispatches and listeners split between two buses with
|
|
182
|
+
no error. Setting the same bus again (the HMR shim) or setting after
|
|
183
|
+
`resetCommandBus()` stays quiet, and the `vapor-chamber/vitest` setup file
|
|
184
|
+
now resets before it installs each test's bus. No IIFE byte changes (the
|
|
185
|
+
warning folds out); `dist/shared-bus.js` +602 B raw / +202 brotli before a
|
|
186
|
+
consumer's production build drops it. `tests/shared-bus-fallback-warning.test.ts`;
|
|
187
|
+
log section 35.73.
|
|
188
|
+
|
|
189
|
+
### Added: `ok`, `err` and `countOption` for app plugins
|
|
190
|
+
|
|
191
|
+
A plugin that answers without calling `next()` had to build its own
|
|
192
|
+
`{ ok: false, error }`, a different hidden class from the bus's results, so
|
|
193
|
+
every `result.ok` site that saw both went polymorphic. The root now exports
|
|
194
|
+
the bus's own two factories as `ok(value)` and `err(error)` (the same
|
|
195
|
+
functions, not copies), and a refusal reads `err(fail(code, message))`, with
|
|
196
|
+
`fail`, the plugin's third argument, minting the owner from its `id`. It also
|
|
197
|
+
exports `countOption(value, fallback, min?, max?)`, the library's rule for a
|
|
198
|
+
numeric option: NaN falls back to the default, negatives clamp, fractions
|
|
199
|
+
truncate, `Infinity` passes. `examples/custom-plugins.ts` uses them. No IIFE
|
|
200
|
+
byte changes; `dist/index.js` +47 B raw / +18 brotli.
|
|
201
|
+
`tests/plugin-result-factories.test.ts`; log section 35.74.
|
|
202
|
+
|
|
203
|
+
### Fixed: two failures built a result of their own shape
|
|
204
|
+
|
|
205
|
+
`schemaValidator`'s rejection and `rehydrate()`'s refusal of an async bus
|
|
206
|
+
returned `{ ok: false, error }`, two fields, a different hidden class from
|
|
207
|
+
every other result, so a `result.ok` site that saw them went polymorphic.
|
|
208
|
+
Both now come from the bus's own factory (same error, same message; the
|
|
209
|
+
result gains `value: undefined` like every other failure). The check that
|
|
210
|
+
guards this read one line at a time and missed a literal written over
|
|
211
|
+
several lines; it now reads both (it found these two and a third,
|
|
212
|
+
`validateSchemas`, whose three fields already matched). No IIFE byte changes;
|
|
213
|
+
`dist/ssr2.js` -21 B brotli, `dist/plugins-schema.js` -4.
|
|
214
|
+
`tests/v8-shapes.test.ts`; log section 35.76.
|
|
215
|
+
|
|
216
|
+
### Changed: every built-in plugin names itself in its failures
|
|
217
|
+
|
|
218
|
+
A built-in plugin that threw was reported as `plugin:failed:plugin`, which
|
|
219
|
+
does not say which one. Every bus-plugin factory now declares its id, the
|
|
220
|
+
owner of the failures it reports: a throw reads `serialize:failed:plugin`,
|
|
221
|
+
`logger:failed:plugin` and so on. The three bridges are `transport`, like
|
|
222
|
+
their other failures (`transport:lost:reply`), and both schema validators
|
|
223
|
+
`validateSchemas`. A plugin of yours without an `id` still reads
|
|
224
|
+
`plugin:failed:plugin`. Refusal codes do not change. `getErrorEntry()` and
|
|
225
|
+
`isRetryableCode()` answer for any `<id>:failed:plugin` with the
|
|
226
|
+
`plugin:failed:plugin` entry (before, a named plugin's throw had none).
|
|
227
|
+
`tests/plugin-ids.test.ts` holds the 25 ids; log section 35.77.
|
|
228
|
+
|
|
229
|
+
### Changed: four failures that carried no code now do
|
|
230
|
+
|
|
231
|
+
Four failures the library builds were a plain `Error` with no code, so they
|
|
232
|
+
read as condition `failed`, a bug: `schemaValidator`'s rejection is now
|
|
233
|
+
`schemaValidator:invalid:payload`, `rehydrate()` given an async bus
|
|
234
|
+
`ssr:invalid:bus`, `synthesize()` without an adapter
|
|
235
|
+
`schema:missing:adapter`, and a `v-vc-command` dispatch that outlives its
|
|
236
|
+
timeout `directive:timeout:dispatch` (a transient condition). Messages are
|
|
237
|
+
unchanged, and each code is in `ERROR_CODE_REGISTRY`, as is
|
|
238
|
+
`throttle:limited:handler`, which `throttle()` raised but the registry
|
|
239
|
+
lacked. An outbox `isRetryable` rule can now tell a schema rejection
|
|
240
|
+
(`invalid`) from a bug. No IIFE byte changes. `tests/errors-match-catalogue.test.ts`
|
|
241
|
+
(which also fails on any new uncoded failure); log section 35.78.
|
|
242
|
+
|
|
243
|
+
### Changed: two new conditions, `unauthenticated` and `conflict`
|
|
244
|
+
|
|
245
|
+
Two distinctions gRPC's canonical codes draw were hidden in our vocabulary.
|
|
246
|
+
A 401 or 419 (an expired session or CSRF token) now reads `unauthenticated`
|
|
247
|
+
instead of `refused`, which keeps 403 alone; a 409 or 412 (the target's state
|
|
248
|
+
is not the one the request assumed) reads `conflict` instead of `already`. **A
|
|
249
|
+
backend failure's code changes with it** (`remote:already:in_progress` is now
|
|
250
|
+
`remote:conflict:in_progress`, `remote:refused:...` from a 401 is
|
|
251
|
+
`remote:unauthenticated:...`): a rule that matches those strings must follow.
|
|
252
|
+
Behaviour is the same: both are final for the bus's retry; the outbox still
|
|
253
|
+
drops a 409/412 (a verdict) and keeps a 401/419 (replayed after sign-in),
|
|
254
|
+
now read from the condition instead of a status check (`dist/outbox.js` -36 B
|
|
255
|
+
brotli). The conditions read 1:1 against gRPC's codes in
|
|
256
|
+
`docs/plan-failures-and-contract.md` 4.2. IIFEs +27 B raw, brotli +11 core,
|
|
257
|
+
-2 full and elements. Log section 35.79.
|
|
258
|
+
|
|
259
|
+
### Changed: a failure on the wire carries an RFC 9457 `type`
|
|
260
|
+
|
|
261
|
+
A failure serialized as a problem (`toJSON()`, what crosses a worker, a
|
|
262
|
+
channel, storage or a server) left `type` implicit, `about:blank`, which RFC
|
|
263
|
+
9457 reads as "nothing beyond the HTTP status"; and an RFC client must ignore
|
|
264
|
+
extensions it does not know, so it dropped `code`, the only identity. `type`
|
|
265
|
+
is now the URI of the failure's condition,
|
|
266
|
+
`https://github.com/lucianofedericopereira/vapor-chamber/blob/main/docs/errors.md#<condition>`,
|
|
267
|
+
which resolves for every failure, the library's, your plugins' and your
|
|
268
|
+
backend's alike. `code` stays the exact identity; a backend's own `type` is
|
|
269
|
+
kept. `docs/errors.md` is new and generated from `ERROR_CODE_REGISTRY` by
|
|
270
|
+
`npm run docs`: one section per condition, its meaning, its retry default,
|
|
271
|
+
its gRPC reading and the codes under it. IIFEs +75 to +80 B brotli; the
|
|
272
|
+
Blade bundle +93. `tests/problem-type.test.ts`; log section 35.80.
|
|
273
|
+
|
|
274
|
+
### Faster: a sync dispatch with listeners and no plugin
|
|
275
|
+
|
|
276
|
+
A bus with listeners or hooks but no plugin called its handler through the
|
|
277
|
+
empty plugin runner, two closures a dispatch. It now calls it directly, as the
|
|
278
|
+
bare path does. With `useSharedCommandState` and `isLoading`: 235 -> 207 ns a
|
|
279
|
+
dispatch. Log 35.143.
|
|
280
|
+
|
|
281
|
+
### Changed (breaking): a plugin declares its `actions`; the bus skips it elsewhere
|
|
282
|
+
|
|
283
|
+
Every scoped plugin (`cache`, `circuitBreaker`, `rateLimit`, `metrics`,
|
|
284
|
+
`serialize`, `idempotent`, `supersede`, `debounce`, `throttle`,
|
|
285
|
+
`optimisticUndo`, the outbox, the bridges, `pollWith`) declares `actions`, and
|
|
286
|
+
the bus runs it only on those, one chain per action: a plugin that does not
|
|
287
|
+
match costs nothing (four that miss: 232 -> 114 ns a dispatch). A plugin of
|
|
288
|
+
your own declares `actions` the same way. `debounce`, `throttle` and
|
|
289
|
+
`optimisticUndo` take patterns now, as the others. A plugin called outside a
|
|
290
|
+
bus no longer filters. `tests/plugin-manifest.test.ts`; log 35.141.
|
|
291
|
+
|
|
292
|
+
### Added: `pollWith`, a 202 Accepted followed to its end
|
|
293
|
+
|
|
294
|
+
`bus.use(pollWith({ bus }))`: a command answered 202 with a `Location`
|
|
295
|
+
resolves at once; `pollWith` polls the status monitor (each `Retry-After`) and
|
|
296
|
+
emits `<action>$done` with `{ command, result }`. The reference controller
|
|
297
|
+
answers 202 for an action returning `['accepted' => ...]`.
|
|
298
|
+
`tests/poll-with.test.ts`; log 35.140.
|
|
299
|
+
|
|
300
|
+
### Added: `meta.request`, headers a plugin adds to the request
|
|
301
|
+
|
|
302
|
+
A bus plugin sets `cmd.meta.request = { headers }` (names lowercase) and the
|
|
303
|
+
HTTP bridges send them: a tracing plugin writes a W3C `traceparent` with
|
|
304
|
+
`bus.use()`, no bridge option. A batch merges its commands' headers in queue
|
|
305
|
+
order. `tests/meta-request.test.ts`; log 35.139.
|
|
306
|
+
|
|
307
|
+
### Changed (breaking): one command envelope on every wire
|
|
308
|
+
|
|
309
|
+
`{ id?, command, target, payload?, meta? }` on a single request, a batch item
|
|
310
|
+
and a WebSocket frame: `id` where answers are multiplexed, `meta` with
|
|
311
|
+
`idempotencyKey`, `correlationId` and `causationId` when set. A backend reads
|
|
312
|
+
the key from `meta` (the batch's top-level `idempotencyKey` is gone); a single
|
|
313
|
+
request still sends the `Idempotency-Key` header. The WebSocket bridge now
|
|
314
|
+
sends the key. `tests/wire-envelope.test.ts`; log 35.138.
|
|
315
|
+
|
|
316
|
+
### Removed (breaking): `CommandsOf` and the `window.__VUE__` fallback
|
|
317
|
+
|
|
318
|
+
`CommandsOf` was a second name for `InferMap`; use `InferMap`. A Vue namespace
|
|
319
|
+
is read from `window.__VAPOR_CHAMBER_VUE__` (or `configureVue()`), no longer
|
|
320
|
+
from `window.__VUE__`, which is Vue's key. Log 35.137.
|
|
321
|
+
|
|
322
|
+
### Added: `meta.response`, the response a bridged command's reply arrived in
|
|
323
|
+
|
|
324
|
+
The HTTP bridges kept a reply's status, headers, `url` and `redirected` only on
|
|
325
|
+
failure. Now `cmd.meta.response` holds them for the 2xx a reply arrived in
|
|
326
|
+
(Fetch's member names, header names lowercase); `undefined` for a local
|
|
327
|
+
handler; a non-2xx answer stays the error's. A 202 is readable as accepted,
|
|
328
|
+
not finished (RFC 9110 15.3.3). `tests/meta-response.test.ts`; log 35.136.
|
|
329
|
+
|
|
330
|
+
### Changed (breaking): the HTTP client's failures are the core's `BusError`
|
|
331
|
+
|
|
332
|
+
The client and the bridges give the same failure for the same answer, retried
|
|
333
|
+
by the async bus's rule (`retryClass`). `HttpError` is removed.
|
|
334
|
+
|
|
335
|
+
| Before | Now |
|
|
336
|
+
| --- | --- |
|
|
337
|
+
| `HttpError` for a non-2xx | `remote:<condition of the status>:<code>` (`http` when the body has no code) |
|
|
338
|
+
| `error.response.status`, `error.response.data` | `error.context.status`, `problemOf(error)` |
|
|
339
|
+
| `TimeoutError` | `transport:timeout:reply` |
|
|
340
|
+
| `AbortError` (also a deduped read's caller) | `transport:aborted:request` |
|
|
341
|
+
| a network failure (`TypeError`) | `transport:lost:reply`, the `TypeError` as `cause` |
|
|
342
|
+
| a body that is not the JSON it declared (`SyntaxError`) | `remote:unexpected:json` |
|
|
343
|
+
| no CSRF token after the 419 refresh | `transport:missing:csrf` |
|
|
344
|
+
| `error.silent` | `error.context.silent` |
|
|
345
|
+
| `http.safe.*` `error`: the problem object | the `BusError`; `problemOf(error)` reads its problem |
|
|
346
|
+
| a POST with no response re-sent | sent once, unless it carries an `Idempotency-Key` |
|
|
347
|
+
| a non-2xx whose body is not its declared JSON: the client's `SyntaxError` | the status decides on every path: `remote:<condition>:http` |
|
|
348
|
+
| a 2xx that is not the JSON envelope (an HTML login page, an array) on a bridge: a success | `remote:unexpected:json`, the command fails |
|
|
349
|
+
| an empty 2xx body | no data on every path, a success (unchanged) |
|
|
350
|
+
|
|
351
|
+
`tests/http-one-shape.test.ts`; log sections 35.131-35.133.
|
|
352
|
+
|
|
353
|
+
### Fixed: `history` could not undo a store action; `undo: true` makes it
|
|
354
|
+
|
|
355
|
+
`history` reverses a command only through an inverse registered with it, and
|
|
356
|
+
a store registered its actions with none: `undo()` left the state as it was
|
|
357
|
+
and `redo()` ran the action again. The docs, and the v1.18.0 entry ("`history`
|
|
358
|
+
undoes it"), said otherwise. `defineChamberStore(id, { ..., undo: true })`
|
|
359
|
+
registers each action and `$reset` with an inverse that puts back the state
|
|
360
|
+
the action replaced, run as the `<action>$undo` command like every undo (entry
|
|
361
|
+
below), so persist stores the undone state and a reload keeps it; history does
|
|
362
|
+
not record it. Off by default, and a store without it registers and runs
|
|
363
|
+
exactly as before. `tests/store-undo.test.ts`; log section 35.112.
|
|
364
|
+
|
|
365
|
+
### Added: `vapor-chamber/store/core`, the store with no Vue
|
|
366
|
+
|
|
367
|
+
The store, for an app on the bus without Vue: the same implementation as
|
|
368
|
+
`vapor-chamber/store` (actions as commands, `$reset`, undo, `$onField`,
|
|
369
|
+
`share`, the coded refusals) over the library's `signal()`, which is a plain
|
|
370
|
+
`{ value }` or, with `configureAlienSignals()`, a reactive signal. No
|
|
371
|
+
component scope, so the caller owns `$dispose()`; `fieldRef` stays in the Vue
|
|
372
|
+
entry. The built entry imports no Vue. `tests/store-core.test.ts`; log section
|
|
373
|
+
35.130.
|
|
374
|
+
|
|
375
|
+
### Added: a store shared across tabs, `share`
|
|
376
|
+
|
|
377
|
+
`defineChamberStore(id, { ..., share: lane })`, with the lane bridged by
|
|
378
|
+
`createChannel({ lane, events: ['<id>$state'] })`: each local write sends the
|
|
379
|
+
new state with a version, and other tabs apply a newer one through the
|
|
380
|
+
`<id>$sync` command (origin `'sync'`, which `history` and `useCommandHistory`
|
|
381
|
+
now do not record). Every tab ends on the same state, concurrent writes
|
|
382
|
+
included; a received state is not sent back. It shares state, not commands,
|
|
383
|
+
so a non-deterministic handler cannot make tabs disagree (the reason the old
|
|
384
|
+
sync plugin was removed). A store without it writes exactly as before.
|
|
385
|
+
`tests/store-share.test.ts`; log section 35.129.
|
|
386
|
+
|
|
387
|
+
### Added: a store's field events, `$onField` and `fieldRef`
|
|
388
|
+
|
|
389
|
+
`store.$onField(key, fn)` calls `fn` with a field's new value each time a write
|
|
390
|
+
changes it; `fieldRef(store, key)` (from `vapor-chamber/store`) is that field as
|
|
391
|
+
a read-only Vue ref, so a component reading one field re-renders only when it
|
|
392
|
+
changes. A store nobody subscribes to writes exactly as before. With several
|
|
393
|
+
readers it is cheaper on every write than a `computed` per field; with one, a
|
|
394
|
+
`computed` is slightly cheaper (docs/store.md). `$onField` is a store member an
|
|
395
|
+
action cannot take. `tests/store-field-events.test.ts`; log section 35.125.
|
|
396
|
+
|
|
397
|
+
### Removed: `csrf: 'inertia'` on the HTTP bridges
|
|
398
|
+
|
|
399
|
+
It sent no CSRF token at all: the bridges make their own requests, which no
|
|
400
|
+
Axios interceptor of the app touches, and Inertia 3 ships no Axios. On Laravel
|
|
401
|
+
13 a tokenless POST passes only from the same origin. An Inertia app uses
|
|
402
|
+
`csrf: true`, which reads the `XSRF-TOKEN` cookie Laravel sets when the page
|
|
403
|
+
has no meta tag. `csrf` is now a boolean; passing `'inertia'` is a type error.
|
|
404
|
+
`tests/csrf-option.test-d.ts`; log section 35.124.
|
|
405
|
+
|
|
406
|
+
### Fixed: `vaporChamberHMR()` no longer trips the shared-bus warning
|
|
407
|
+
|
|
408
|
+
On first load its shim read the shared bus, which created one, so an app
|
|
409
|
+
calling `setCommandBus()` in its setup got the "replaced the bus
|
|
410
|
+
getCommandBus() had created" warning on every dev server start. It now reads
|
|
411
|
+
no bus until a hot update stores it. `tests/hmr-shim-first-load.test.ts`;
|
|
412
|
+
log section 35.123.
|
|
413
|
+
|
|
414
|
+
### Added: `problemOf(error)`, the backend's problem behind a failure
|
|
415
|
+
|
|
416
|
+
A loader failure is `router:failed:loader` with the http client's error as
|
|
417
|
+
its cause, and an app dug through `cause` for the backend's RFC 9457 problem.
|
|
418
|
+
`problemOf(error)` (root export) walks the cause chain and returns that
|
|
419
|
+
problem: a `remote:` failure, from a bridge or the http client (entry above),
|
|
420
|
+
gives its members. `undefined` when no backend answered. Plan 8d.2;
|
|
421
|
+
`tests/problem-of.test.ts`; log section 35.121.
|
|
422
|
+
|
|
423
|
+
### Changed: a query change wins over a page navigation still in flight
|
|
424
|
+
|
|
425
|
+
On a page, `push('/list')` held by a slow guard or loader, then
|
|
426
|
+
`setQuery({ q: 'x' })` (or a query-only `push`): the query committed, then
|
|
427
|
+
`/list` committed over it and the change was lost, its history entry left
|
|
428
|
+
behind. The query change now supersedes the pending navigation, as a newer
|
|
429
|
+
navigation does: the query commits on the page on screen and the pending
|
|
430
|
+
`push` answers `router:aborted:navigation` (normal flow, not `onError`). A
|
|
431
|
+
query refetch still never aborts a page navigation.
|
|
432
|
+
`tests/router/query-during-navigation.test.ts`; log section 35.120.
|
|
433
|
+
|
|
434
|
+
### Changed: `authGuard` refuses with `authGuard:unauthenticated:action`
|
|
435
|
+
|
|
436
|
+
No session is `unauthenticated` (sign in, then the same request), not
|
|
437
|
+
`refused` (a rule said no): the code was `authGuard:refused:action`. A rule
|
|
438
|
+
matching the old code must follow. `tests/plugins.test.ts`; log section 35.119.
|
|
439
|
+
|
|
440
|
+
### Changed: an app's `$` name is refused; the naming check is coded; `router:failed:history`
|
|
441
|
+
|
|
442
|
+
Names with `$` are the library's (`<id>$reset`, `<action>$undo`), and the
|
|
443
|
+
bridges keep them local, so an app's would have been silently never sent.
|
|
444
|
+
`register()` now refuses one (`core:invalid:name`), whatever the naming rule,
|
|
445
|
+
and so does `createTestBus`; a store refuses a `$` in its id or an action key
|
|
446
|
+
when defined (`store:invalid:name`). The naming check's violation in `throw`
|
|
447
|
+
mode is the same `core:invalid:name` (it was a plain `Error`). A history the
|
|
448
|
+
app supplies that throws writing the URL is `router:failed:history` (it was
|
|
449
|
+
reported as a component failure, which hard-navigated). A store with
|
|
450
|
+
`undo: true` keeps its last 256 steps in a ring instead of weak maps: its
|
|
451
|
+
action cost drops from about +800 ns to about +60 ns. `createTestBus` registers
|
|
452
|
+
`<action>$undo` like the real bus. `tests/library-names.test.ts`; log sections
|
|
453
|
+
35.117, 35.118.
|
|
454
|
+
|
|
455
|
+
### Changed: reads answer plain JSON; `unwrapRoutesPayload` is `readRoutesPayload`
|
|
456
|
+
|
|
457
|
+
A read (a loader, the routes table) answers the body itself, and a failure
|
|
458
|
+
is a non-2xx `application/problem+json`; the command envelope (`{ state }`, a
|
|
459
|
+
`{ problem }` on a 2xx) is not a read's shape. The loader preset already read
|
|
460
|
+
plain JSON, but the routes reader also accepted `{ state }` and a 2xx
|
|
461
|
+
`{ problem }`, guessing between two shapes. It now reads the bare
|
|
462
|
+
`{ routes, base? }` only: an envelope is `router:unexpected:routes`, a failed
|
|
463
|
+
fetch `router:failed:routes` with the client's error as its cause. The
|
|
464
|
+
export is renamed to say what it does. A backend that wrapped its routes table
|
|
465
|
+
in `{ state }` returns the table itself. `tests/router/routes-plain-json.test.ts`;
|
|
466
|
+
log section 35.115.
|
|
467
|
+
|
|
468
|
+
### Changed: every undo is a command, `<action>$undo`
|
|
469
|
+
|
|
470
|
+
`register(action, handler, { undo })` also registers `<action>$undo`, and every
|
|
471
|
+
undo runs through it: `history` / `useCommandHistory`, `optimisticUndo`'s
|
|
472
|
+
rollback and a transactional batch's rollback. Before, each called the inverse
|
|
473
|
+
directly, so no listener, devtools timeline or persist heard an undo. The
|
|
474
|
+
command's target is the command it reverses (the inverse still receives that
|
|
475
|
+
command), its origin `'undo'`, its `causationId` that command's id. A plugin
|
|
476
|
+
can refuse it like any command: history then keeps the step, and a refused
|
|
477
|
+
rollback is reported (`rollbacks`, `onRollbackError`). `$undo` names show in
|
|
478
|
+
`registeredActions()` and `inspectBus().actions`. On the async bus an undo is
|
|
479
|
+
now async like every dispatch, so a `redo()` in the same tick as an `undo()`
|
|
480
|
+
does nothing (one undo or redo in flight at a time, as before). The library's
|
|
481
|
+
`$` commands (`$undo`, `$reset`) are local: no bridge sends them and the outbox
|
|
482
|
+
never queues them, whatever their `actions` patterns. Log section 35.114.
|
|
483
|
+
|
|
484
|
+
### Added: `canUndo` beside `undo` in `register()`
|
|
485
|
+
|
|
486
|
+
`register(action, handler, { undo, canUndo })`: `canUndo(cmd)` says whether
|
|
487
|
+
the inverse can still run for that recorded command. `history().getState()`
|
|
488
|
+
and `useCommandHistory()` report `canUndo` from it for the command on top, and
|
|
489
|
+
`undo()` does nothing while it is false. Without it, `canUndo` is "the history
|
|
490
|
+
is not empty", as before. A store defined with `undo: true` uses it: an undo
|
|
491
|
+
waits while a change the history did not record has moved the state.
|
|
492
|
+
`tests/undo-check.test.ts`, `tests/store-undo.test.ts`; log section 35.113.
|
|
493
|
+
|
|
494
|
+
### Changed: the router follows the core's failure model
|
|
495
|
+
|
|
496
|
+
The entries above gave every failure the library raises one shape: a
|
|
497
|
+
`BusError` coded `owner:condition:subject`, listed in `ERROR_CODE_REGISTRY`,
|
|
498
|
+
read by `conditionOf` / `failureCondition`, a stack only for `failed`, an RFC
|
|
499
|
+
9457 problem at a boundary. The router was the one part left outside it: its
|
|
500
|
+
failure was a plain `Error` named `RouterError` with a snake_case code that
|
|
501
|
+
no registry listed and no core reader understood (`failureCondition` read
|
|
502
|
+
every one as `failed`). It now uses the same model, owner `router`: nothing
|
|
503
|
+
here is a router feature, each change below is what the core's rules give
|
|
504
|
+
it. **Every code changes**, and a handler that matches them must follow:
|
|
505
|
+
|
|
506
|
+
| before | now |
|
|
507
|
+
|---|---|
|
|
508
|
+
| `unmatched` | `router:missing:route` |
|
|
509
|
+
| `aborted` (a guard returned false) | `router:refused:guard` |
|
|
510
|
+
| `cancelled` (superseded, destroy) | `router:aborted:navigation` |
|
|
511
|
+
| `not_ready` | `router:missing:routes` |
|
|
512
|
+
| `unknown_route_name` | `router:missing:record` |
|
|
513
|
+
| `redirect_loop` | `router:exceeded:redirects` |
|
|
514
|
+
| `duplicate_route` | `router:already:route` |
|
|
515
|
+
| `unknown_parent` | `router:missing:parent` |
|
|
516
|
+
| `cyclic_parent` | `router:invalid:parent` |
|
|
517
|
+
| `missing_param` | `router:missing:param` |
|
|
518
|
+
| `bad_menu_row` | `router:invalid:menu` |
|
|
519
|
+
| `invalid_path` | `router:invalid:path` |
|
|
520
|
+
| `invalid_routes_payload` | `router:unexpected:routes` |
|
|
521
|
+
| `routes_load_failed` | `router:failed:routes`; `reload()` without a `{ url }` source: `router:missing:url` |
|
|
522
|
+
| `inline_routes_missing` | `router:missing:inline` |
|
|
523
|
+
| `component_missing` | `router:missing:component` |
|
|
524
|
+
| `component_load_failed` | `router:failed:component`; a guard that throws: `router:failed:guard` |
|
|
525
|
+
| `load_failed` | `router:failed:loader`; a `load` no handler serves: `router:missing:loader` |
|
|
526
|
+
| `blade_unconfigured` | `router:missing:fetchBlade` |
|
|
527
|
+
| `http_unconfigured` | `router:missing:http` |
|
|
528
|
+
| `blade_fetch_failed` | `router:failed:blade` |
|
|
529
|
+
| `mode_mismatch` | `router:invalid:component` |
|
|
530
|
+
| `no_router` | `router:missing:router` |
|
|
531
|
+
|
|
532
|
+
Also changed: `isRouterError(error, code)` takes the full code; `error.name`
|
|
533
|
+
is `BusError`; the navigation target is `error.context.to` (was `error.to`);
|
|
534
|
+
the message has no `[vapor-chamber-router]` prefix (the code names the
|
|
535
|
+
source); only a `failed` code captures a stack, so a superseded navigation or
|
|
536
|
+
a guard's refusal no longer pays for one; `toJSON()` gives the RFC 9457
|
|
537
|
+
problem every failure gives. Read as conditions, three router codes were two
|
|
538
|
+
failures each, and are split: `router:missing:url`, `router:missing:loader`
|
|
539
|
+
(nothing was tried, so not `failed`) and `router:failed:guard`.
|
|
540
|
+
`HARD_NAV_CODES` follows the plan's rule, a route, component or server HTML
|
|
541
|
+
that is missing or failed, and so gains `router:missing:component`: an
|
|
542
|
+
unregistered component key now hands the URL to the server, as an unmatched
|
|
543
|
+
one does. A guard that throws no longer hard-navigates: it was reported as a
|
|
544
|
+
component load failure, and a reload cannot fix a guard bug. `router` +0.3 KB
|
|
545
|
+
brotli, `router/vdom` and `router/vapor` +0.3 KB each (they now carry
|
|
546
|
+
`BusError`), `router-fetch` +0.3 KB, the root entry +1.4 KB brotli for the 26
|
|
547
|
+
registry rows (tree-shaken unless you read the registry).
|
|
548
|
+
`tests/router/router-codes.test.ts`; log section 35.108.
|
|
549
|
+
|
|
550
|
+
### Changed: Vue's async Vapor code only in apps that use it (`vaporChamberWire({ entry: 'vapor' })`)
|
|
551
|
+
|
|
552
|
+
`vapor-chamber/vapor` handed Vue's `defineVaporAsyncComponent` to the
|
|
553
|
+
registry so the root's wrapper could reach it, which kept Vue's async
|
|
554
|
+
component code in every `/vapor` app, whether it defined an async component
|
|
555
|
+
or not. In a Vite build with `vaporChamberWire({ entry: 'vapor' })` the root's
|
|
556
|
+
`defineVaporAsyncComponent` is now `/vapor`'s, which calls Vue's directly, and
|
|
557
|
+
the registry is not seeded: an app that never defines an async component
|
|
558
|
+
ships none of Vue's async code, and one that does gets a component in
|
|
559
|
+
production as before. Every other build (esbuild, webpack, Vite without the
|
|
560
|
+
plugin) keeps the seed and today's behaviour. `vapor-chamber/vapor`'s
|
|
561
|
+
`defineVaporAsyncComponent` is its own function now (it never returns null),
|
|
562
|
+
no longer the root's. `./vite` +0.1 KB brotli, `./vapor` unchanged in brotli.
|
|
563
|
+
`tests/vapor-async-wire.test.ts`, `tests/vapor-async-seed.test.ts`; log
|
|
564
|
+
section 35.109.
|
|
565
|
+
|
|
566
|
+
### Faster for keyed commands: `meta.idempotencyKey` is always there
|
|
567
|
+
|
|
568
|
+
A command's `meta` gained `idempotencyKey` only when `idempotent()`, the outbox
|
|
569
|
+
or the async retry stamped one, which moved every keyed command onto a second
|
|
570
|
+
hidden class. It is now a slot of every `meta`, `undefined` until stamped, so
|
|
571
|
+
keyed and plain commands share one shape (`'idempotencyKey' in cmd.meta` is
|
|
572
|
+
now always true; JSON still drops it while undefined). A plain dispatch
|
|
573
|
+
measured unchanged (B/A 1.000-1.008, ~22 ns). IIFEs +2 to +9 B brotli.
|
|
574
|
+
`tests/v8-shapes.test.ts`; log section 35.81.
|
|
575
|
+
|
|
576
|
+
### Added: `HttpResponse.url` and `redirected`
|
|
577
|
+
|
|
578
|
+
The HTTP client dropped two members Fetch gives every response, so an app
|
|
579
|
+
its server redirected could not learn where it ended up. `createHttpClient()`
|
|
580
|
+
and `postCommand()` now set `url` (the final URL, after redirects) and
|
|
581
|
+
`redirected`. Both are optional in the type, for an app that passes its own
|
|
582
|
+
`httpClient` to a bridge. IIFEs +8 to +11 B brotli.
|
|
583
|
+
`tests/http-response-url.test.ts` (a real local server: a stub has no url);
|
|
584
|
+
log section 35.82.
|
|
585
|
+
|
|
586
|
+
### Added: `resolveNotModified`, a 304 that resolves; fixed: a GET joined to another with different headers
|
|
587
|
+
|
|
588
|
+
A 304 answers a conditional request the app sent itself (`If-None-Match`,
|
|
589
|
+
`If-Modified-Since`), and the client threw it like any non-2xx; no plugin or
|
|
590
|
+
interceptor could change that. With `resolveNotModified: true`, per request
|
|
591
|
+
or per client (and on `postCommand`), it resolves as it is: `status` 304,
|
|
592
|
+
`ok` false, the empty body. Off by default, so code catching it is unchanged.
|
|
593
|
+
|
|
594
|
+
Fixed with it: two GETs of one URL shared one request whatever their
|
|
595
|
+
headers, so a plain read that started while a conditional one was in flight
|
|
596
|
+
received that read's 304 (and a read with another `Authorization` or
|
|
597
|
+
`Accept-Language` got the first one's answer). A GET now joins one in flight
|
|
598
|
+
only when its headers match, and an opted-in read never joins one that is
|
|
599
|
+
not. IIFEs +32 to +38 B brotli. `tests/http-not-modified.test.ts` (a real
|
|
600
|
+
local server); log section 35.83.
|
|
601
|
+
|
|
602
|
+
### Fixed: the router re-review
|
|
603
|
+
|
|
604
|
+
Each fix has a test that failed before it; the record is
|
|
605
|
+
`docs/rc-alignment-log.md`, sections 35.90 onward.
|
|
606
|
+
|
|
607
|
+
- **Two quick Backs during a slow page ended a page off.** A navigation the
|
|
608
|
+
user superseded walked the history back by its own step, which the router
|
|
609
|
+
then read as a third navigation: the page settled on the first Back's page
|
|
610
|
+
with the address bar elsewhere. A cancelled navigation now walks nothing
|
|
611
|
+
(vue-router does the same); a refused or failed one still walks back.
|
|
612
|
+
`tests/router/superseded-navigation-history.test.ts`; log section 35.90.
|
|
613
|
+
- **A navigation to the page on screen let a pending one commit after it.**
|
|
614
|
+
On `/`, a click to a slow `/list` and then a click back to `/` ended on
|
|
615
|
+
`/list`, with `push('/')` having answered null; Forward back to the page
|
|
616
|
+
during a held Back committed the Back's page under the Forward's address.
|
|
617
|
+
The page on screen, asked for again, now supersedes the navigation in
|
|
618
|
+
flight like any newer one (it answers `cancelled`, its loaders abort);
|
|
619
|
+
with nothing in flight it still changes nothing.
|
|
620
|
+
`tests/router/navigation-to-current-page.test.ts`, and both cases in real
|
|
621
|
+
Chromium, `tests/browser/router-history.browser.test.ts`; log section 35.91.
|
|
622
|
+
- **`router.destroy()` during `start()` left a live router behind.** With a
|
|
623
|
+
`{ url }` table, a destroy while the table loaded let start() go on to
|
|
624
|
+
register its history, link and announcer listeners and commit the first
|
|
625
|
+
navigation after the teardowns had run, so a link click still navigated
|
|
626
|
+
it; a destroy during the first navigation left the idle preheat's window
|
|
627
|
+
listeners in place. start() now stops at both points.
|
|
628
|
+
`tests/router/destroy-during-start.test.ts`; log section 35.92.
|
|
629
|
+
- **Inline routes that were not JSON failed with a plain `SyntaxError`.** The
|
|
630
|
+
one inline failure with no code; it is now `invalid_routes_payload`, the
|
|
631
|
+
parse error as its `cause`. `tests/router/inline-routes-json.test.ts`; log
|
|
632
|
+
section 35.93.
|
|
633
|
+
- **One hidden class for every `RouterError`.** `to` and `cause` were added
|
|
634
|
+
only when given, so the errors the navigation path builds came in up to
|
|
635
|
+
four shapes; both are now always own properties, `undefined` when not
|
|
636
|
+
given (`'to' in error` is now always true; reading either is unchanged).
|
|
637
|
+
`tests/router/router-error-shape.test.ts`; log section 35.94.
|
|
638
|
+
- **Docs corrected to the code.** The `vapor-chamber/router/vapor` row of the
|
|
639
|
+
retained-bindings tables (README, docs/router.md) now lists `createIf`;
|
|
640
|
+
`RouteLocation.path` is documented as the path as in the URL
|
|
641
|
+
(percent-encoded, params decoded), not "decoded"; an untested "~20us per
|
|
642
|
+
navigation" figure is gone from docs/router.md.
|
|
643
|
+
`tests/router/location-path-encoding.test.ts`,
|
|
644
|
+
`tests/router/vapor-boundary.test.ts`; log section 35.95.
|
|
645
|
+
### Changed: `OutboxRecord.queuedAt` is an RFC 3339 string
|
|
646
|
+
|
|
647
|
+
The outbox stamped each queued record with `Date.now()`, an epoch number
|
|
648
|
+
that says neither its unit nor its zone, on a record that leaves the
|
|
649
|
+
process (storage, or a backend a custom `OutboxStorage` syncs to). It is now
|
|
650
|
+
`new Date().toISOString()`, RFC 3339 in UTC with milliseconds
|
|
651
|
+
(`2026-10-02T17:41:19.250Z`), and the type is `string`.
|
|
652
|
+
`Date.parse(record.queuedAt)` gives the milliseconds back. The outbox never
|
|
653
|
+
reads the field, so replay is unchanged; a record persisted with a number
|
|
654
|
+
still hydrates and replays. `dist/outbox.js` +17 B brotli, no IIFE change.
|
|
655
|
+
`tests/outbox.test.ts`; log section 35.84.
|
|
656
|
+
|
|
657
|
+
### Changed: the outbox drops a record a validator rejects at replay
|
|
658
|
+
|
|
659
|
+
The outbox's default `isRetryable` kept every failure the library or a
|
|
660
|
+
plugin raised, so a queued command that `validator()` or `schemaValidator()`
|
|
661
|
+
rejected stayed at the head of the queue for good: the outbox captures before
|
|
662
|
+
any validator runs, the same record gets the same `invalid` answer on every
|
|
663
|
+
flush, and every record behind it waited for ever. Such a failure (condition
|
|
664
|
+
`invalid`) is now final: the record is dropped, `'outboxRejected'` reports it
|
|
665
|
+
with its error, and the flush moves on, as a live dispatch would have been
|
|
666
|
+
refused. Every other failure the library raised is still kept: a handler not
|
|
667
|
+
registered yet, a plugin that threw, an `authGuard` refusal (replayed after
|
|
668
|
+
sign-in). A rule of your own (`isRetryable`) is unchanged. `dist/outbox.js`
|
|
669
|
+
+13 B brotli, no IIFE change. `tests/outbox.test.ts`; log section 35.85.
|
|
670
|
+
|
|
671
|
+
### Fixed: two outbox defects around `Retry-After`
|
|
672
|
+
|
|
673
|
+
Both have a test in `tests/wire-contract.test.ts` that failed before.
|
|
674
|
+
|
|
675
|
+
- **`Retry-After: 0` looped.** A kept record is flushed again after the wait
|
|
676
|
+
its answer declared. With a wait of zero the outbox re-flushed at once, got
|
|
677
|
+
the same answer, and re-flushed again: 500 requests with no time passing
|
|
678
|
+
between them, in the test. A scheduled re-flush now waits at least one
|
|
679
|
+
second. The record still drains by itself when the backend recovers.
|
|
680
|
+
- **A `409` that declared a `Retry-After` was dropped as a verdict.** A
|
|
681
|
+
`409` for a request still in progress, sent with `Retry-After`, says when
|
|
682
|
+
to come back. Any answer that declares a `Retry-After` now keeps the
|
|
683
|
+
record, and the outbox flushes again then. A `409` with no `Retry-After` is
|
|
684
|
+
rejected as before (`remote:conflict:...`).
|
|
685
|
+
|
|
686
|
+
`autoFlush`'s documentation said "No timers"; the `Retry-After` wait is one,
|
|
687
|
+
and the text says so. `dist/outbox.js` +22 B brotli, no IIFE change. Log
|
|
688
|
+
section 35.86.
|
|
689
|
+
|
|
690
|
+
### Changed: the outbox backs off while a backend keeps answering `Retry-After`
|
|
691
|
+
|
|
692
|
+
A backend that answered every replay with `Retry-After: 1` was asked once a
|
|
693
|
+
second for as long as it stayed busy: 601 requests over a 10-minute outage.
|
|
694
|
+
The outbox's scheduled re-flush now doubles its wait while the answer
|
|
695
|
+
repeats (1 s, 2 s, 4 s, ...) and stops growing at 30 s: 24 requests over the
|
|
696
|
+
same outage, and the record drains a second after the backend recovers in
|
|
697
|
+
that run, always within 30 s. A declared wait longer than the schedule wins
|
|
698
|
+
(`Retry-After` is a minimum). A successful replay starts it over at 1 s. A
|
|
699
|
+
`flush()` or an `'online'` event during a wait replaces that wait instead of
|
|
700
|
+
starting a second timer. `dist/outbox.js` +36 B brotli, no IIFE change.
|
|
701
|
+
`tests/wire-contract.test.ts`; log section 35.87.
|
|
702
|
+
|
|
703
|
+
### Fixed: three ways a store broke without a word
|
|
704
|
+
|
|
705
|
+
From a re-review of `vapor-chamber/store`, each with a test that failed before
|
|
706
|
+
(`tests/store-review.test.ts`):
|
|
707
|
+
|
|
708
|
+
- **An action named like a store member replaced it.** `actions: { state }`
|
|
709
|
+
left `store.state` a function, so `store.state.value` read `undefined`;
|
|
710
|
+
`actions: { $reset }` was overwritten by the library's own reset.
|
|
711
|
+
`defineChamberStore` now throws for an action key `$id`, `state`, `url`,
|
|
712
|
+
`$reset` or `$dispose`, naming it.
|
|
713
|
+
- **A store with `url` fields called without a router left its handlers on
|
|
714
|
+
the bus.** The error came after they were registered, so nothing could
|
|
715
|
+
unregister them, and the next call (with the router) registered over them
|
|
716
|
+
with a warning per action. The check now comes first.
|
|
717
|
+
- **A stale `$dispose()` removed the newer store.** After a store was
|
|
718
|
+
disposed and its id taken again, a second `$dispose()` of the old one, or a
|
|
719
|
+
scope that held the old one ending late, deleted the new store from the
|
|
720
|
+
registry: the next `useStore` built a third store over its handlers, and
|
|
721
|
+
the second one's state stopped changing. `$dispose()` now removes only its
|
|
722
|
+
own entry.
|
|
723
|
+
|
|
724
|
+
`dist/store.js` +90 B brotli, no IIFE change. Log section 35.88.
|
|
725
|
+
|
|
726
|
+
### Changed: the store's refusals carry codes
|
|
727
|
+
|
|
728
|
+
Every failure the library raises is `owner:condition:subject`; the store's
|
|
729
|
+
three setup refusals were plain `Error`s. They are now `store:missing:bus`
|
|
730
|
+
(`useStore()` with no bus), `store:missing:router` (url fields, no router) and
|
|
731
|
+
`store:already:member` (an action named like a store member), each in
|
|
732
|
+
`ERROR_CODE_REGISTRY` and `docs/errors.md`; the messages are unchanged.
|
|
733
|
+
`dist/store.js` now imports `./command-bus.js` beside `vue` (no probe, and
|
|
734
|
+
every store user loads it already for the bus). `dist/store.js` +51 B
|
|
735
|
+
brotli, `dist/index.js` +146 (the registry entries); no IIFE change.
|
|
736
|
+
`tests/store-review.test.ts`; log section 35.88b.
|
|
737
|
+
|
|
738
|
+
### Fixed: a `Retry-After` over 30 seconds was ignored, and a long one was cut short
|
|
739
|
+
|
|
740
|
+
`Retry-After` is the minimum time to wait (RFC 9110). The parser dropped any
|
|
741
|
+
value over 30 s, a policy of the HTTP client living where every caller reads,
|
|
742
|
+
so: the HTTP client retried a `Retry-After: 120` at once on its own backoff;
|
|
743
|
+
the outbox saw no wait, and a `409` sent with `Retry-After: 120` was rejected
|
|
744
|
+
as a verdict; and the async bus re-sent a declared wait longer than its
|
|
745
|
+
`maxDelay` (20 s) at `maxDelay`, sooner than asked. The parser now reads what
|
|
746
|
+
was declared (ignoring only a wait no timer can hold, over about 24.8 days),
|
|
747
|
+
and each caller keeps its own policy: the HTTP client does not sleep a
|
|
748
|
+
declared wait over 30 s inside one request (the request fails, the header on
|
|
749
|
+
the failure), the async bus returns a declared wait longer than `maxDelay`
|
|
750
|
+
instead of re-sending early, and the outbox waits it out. IIFEs brotli -6
|
|
751
|
+
full, +12 core, +23 elements. `tests/retry-after-long.test.ts`; log section
|
|
752
|
+
35.89.
|
|
753
|
+
|
|
754
|
+
### Fixed: a `Retry-After` date is read as GMT, and only an HTTP-date is read
|
|
755
|
+
|
|
756
|
+
`Retry-After` may carry a date instead of seconds, and RFC 9110 names three
|
|
757
|
+
forms for it, all in GMT. The client read the date with `Date.parse`, which
|
|
758
|
+
takes the oldest of them (`Sun Nov 6 08:49:37 1994`, no zone written) as
|
|
759
|
+
LOCAL time - three hours late on a machine at UTC-3 - and takes any other
|
|
760
|
+
date it understands too (`2026-10-01T12:00:10Z`, `10/1/2026`, a numeric
|
|
761
|
+
offset, lowercase or made-up day names). It now reads the three HTTP-date
|
|
762
|
+
forms only, each as GMT, and ignores anything else, as it already ignored a
|
|
763
|
+
malformed number of seconds. A two-digit year (the obsolete second form)
|
|
764
|
+
reads as its next occurrence. IIFEs +250 to +286 B brotli, the Blade bundle
|
|
765
|
+
+272. `tests/retry-after-grammar.test.ts`; log section 35.100.
|
|
766
|
+
|
|
767
|
+
### Fixed: a store defined again takes over, as a handler registered again does
|
|
768
|
+
|
|
769
|
+
A second `defineChamberStore` with an id a store still held got the first
|
|
770
|
+
store back: without its own actions, running the old reducers, with no
|
|
771
|
+
word. A store created when its module loads kept its first definition
|
|
772
|
+
through every hot reload, so edited reducers never applied. Now a different
|
|
773
|
+
definition takes over, the rule `register()` follows for a handler: it builds
|
|
774
|
+
its own store and registers its actions over the old ones (in development,
|
|
775
|
+
`register()` warns for each one, which is how two modules claiming one id
|
|
776
|
+
show), and the old store's `$dispose()` removes only what it still owns. The
|
|
777
|
+
same definition called again still returns the one store. A component that
|
|
778
|
+
holds a store was not affected: Vue disposes it before a reload. `dist/store.js`
|
|
779
|
+
+16 B brotli, no IIFE change. `tests/store-redefine.test.ts`,
|
|
780
|
+
`tests/vapor/store-hmr.test.ts`; log section 35.101.
|
|
781
|
+
|
|
782
|
+
### Changed: the store's refusals read like the core's
|
|
783
|
+
|
|
784
|
+
A store's three setup refusals (`store:already:member`, `store:missing:bus`,
|
|
785
|
+
`store:missing:router`) now have the shape every core failure has: the
|
|
786
|
+
message states the fact, every value in it is also in `error.context`
|
|
787
|
+
(`id`, the `key`, the url `fields`), and the advice on what to do next is
|
|
788
|
+
printed in development only - production carries the fact, and the fix lives
|
|
789
|
+
in the catalogue (`getErrorEntry(code).fix`). The `[vapor-chamber]` prefix
|
|
790
|
+
went with it; no core failure carries one. Codes unchanged. A consumer's
|
|
791
|
+
production build of `vapor-chamber/store` is 108 B brotli smaller.
|
|
792
|
+
`tests/store-review.test.ts`; log section 35.102.
|
|
793
|
+
|
|
794
|
+
### Docs: loading saved state into a store, from storage or from the server
|
|
795
|
+
|
|
796
|
+
`docs/store.md` has one pattern for it: a store has no setter, so saved state
|
|
797
|
+
enters through an action the store declares (`load: (s, saved) => saved ?? s`),
|
|
798
|
+
called with `persist().load()` or with the state the server put in the page.
|
|
799
|
+
From the server, send the state rather than the commands that built it:
|
|
800
|
+
replaying commands re-runs every reducer on the client, and one that is not
|
|
801
|
+
deterministic ends somewhere else. `__origin: 'replay'` in the payload marks
|
|
802
|
+
the load as restored state for listeners. No code change.
|
|
803
|
+
`tests/store-load-saved.test.ts`; log section 35.103.
|
|
804
|
+
|
|
805
|
+
### Changed: development dependencies updated within their ranges
|
|
806
|
+
|
|
807
|
+
No major version: vite 8.3.2, vitest 5.0.3 (with its coverage and browser
|
|
808
|
+
packages), @vitejs/plugin-vue 6.0.9, vue-tsc 3.3.12, astro 7.3.5 (the
|
|
809
|
+
exo-astro example), biome 2.5.15, happy-dom 20.14.5, @types/node 26.6.4.
|
|
810
|
+
`npm audit fix` cleared `devalue`; `http-cache-semantics`, reached only
|
|
811
|
+
through the exo-astro example's astro, has no fixed version. The library's
|
|
812
|
+
own dependencies are unchanged (it has none at run time). The full IIFE is 3 B
|
|
813
|
+
brotli larger from the minifier's output; no library code changed.
|
|
814
|
+
|
|
815
|
+
### Docs: the whitepaper rewritten
|
|
816
|
+
|
|
817
|
+
`docs/whitepaper.md` now describes the library as it is: 17 sections and a new
|
|
818
|
+
one, Performance and V8 (how a speed claim is measured here, and the findings
|
|
819
|
+
in ns and ratios). The origins, the Vue 3.6 alignment log (beta.8 to rc.10)
|
|
820
|
+
and the passages in which the document corrected itself moved verbatim to an
|
|
821
|
+
appendix, "Notes and history". Six wrong statements were corrected (among
|
|
822
|
+
them: `retry` listed as a plugin, naming called "enforced", an example passing
|
|
823
|
+
a `plugins` option `createCommandBus` does not have) and seven unbacked
|
|
824
|
+
figures dropped or replaced by stamped ones; the record is
|
|
825
|
+
`docs/rc-alignment-log.md`, section 35.68.
|
|
826
|
+
|
|
827
|
+
### Docs: the peer bench ratios restamped
|
|
828
|
+
|
|
829
|
+
The 16 ratios against mitt and nanoevents in `docs/performance.md`, the
|
|
830
|
+
migration guides and the ROADMAP had drifted with Node and the peers'
|
|
831
|
+
versions, not with this library's code. Each is now the median of 5
|
|
832
|
+
`npm run bench` runs from a clean tree (Node 24.21, mitt 3.0.1, nanoevents
|
|
833
|
+
9.1.0). The default fast lane's fan-out no longer reads ahead of nanoevents:
|
|
834
|
+
it is about level (0.91-1.03 across runs), and the guide says so. Log section
|
|
835
|
+
35.56.
|
|
836
|
+
|
|
837
|
+
Then nanoevents left the docs, the bench and the dev dependencies (owner). Its
|
|
838
|
+
lead came from a micro-loop emitting one constant name, which V8 specialises:
|
|
839
|
+
with four varying names it emits at about half this library's speed with no
|
|
840
|
+
listeners, and its plain-object table throws on `emit('constructor')`. The docs
|
|
841
|
+
compare this library with itself and with mitt, which has a migration guide.
|
|
842
|
+
Log sections 35.57 and 35.58.
|
|
843
|
+
|
|
844
|
+
Then a ratio against another library became a range, not one value: `npm run
|
|
845
|
+
bench:bands` runs the bench N times and stamps each peer ratio as its min-max,
|
|
846
|
+
with the Node and peer versions, and each of this library's own ratios as the
|
|
847
|
+
median; a single `npm run bench` no longer moves a doc. eventemitter3, the
|
|
848
|
+
EventEmitter guide's peer, is now compared too, and the docs say what the
|
|
849
|
+
bench says: the general bus's `emit` is behind it on fan-out (0.78-0.82x),
|
|
850
|
+
the fast lane level with it (0.94-1.04x). Two hand-typed figures are now
|
|
851
|
+
stamped: `bus.emit` on fan-out, 1.44-1.54x mitt ("~1.5x" held), and mitt's
|
|
852
|
+
emit against `bus.dispatch`, 1.64-2.04x (it said "~3x"). Log section 35.69.
|
|
853
|
+
|
|
854
|
+
### CI: a speed check that checks
|
|
855
|
+
|
|
856
|
+
The job named "bench smoke (no regression vs baseline)" compared against
|
|
857
|
+
nothing; it is now "bench (report only)". A new job, `perf-ab`, builds the
|
|
858
|
+
pushed commit and its base and compares them with the repository's A/B tool
|
|
859
|
+
(`npm run ab:ci`) over five workloads: it fails on a measured regression, and
|
|
860
|
+
on a run too noisy to have measured anything. Verified locally: identical
|
|
861
|
+
builds pass, and a seeded +37-41 ns per dispatch fails on every row of the
|
|
862
|
+
dispatch workload. Log section 35.59.
|
|
863
|
+
|
|
864
|
+
With no base to build, `perf-ab` now skips and says why, where it failed with
|
|
865
|
+
no message: an all-zero base (the push that creates the branch) or one a
|
|
866
|
+
force-push dropped from the checkout. `scripts/ab/build-dists.sh` also prints
|
|
867
|
+
git's error when it cannot check a commit out. Log section 35.71.
|
|
868
|
+
|
|
869
|
+
### `VcTransition`: a transition for Vapor, driven by the bus (new subpath, experimental)
|
|
870
|
+
|
|
871
|
+
`vapor-chamber/transitions/vapor` exports `VcTransition`. It imports nothing of
|
|
872
|
+
Vue's transition. The component owns the condition: the content is rendered
|
|
873
|
+
while `show` is true, and when `show` turns false it stays in the DOM until the
|
|
874
|
+
`<namespace>Leave` command has answered. The look comes from command handlers;
|
|
875
|
+
there are no CSS classes.
|
|
876
|
+
|
|
877
|
+
```vue
|
|
878
|
+
<VcTransition :show="open" namespace="modal" :bus="bus">
|
|
879
|
+
<Panel />
|
|
880
|
+
</VcTransition>
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
- **The same six actions as `createTransitionBridge`**, under the same names:
|
|
884
|
+
`modalBeforeEnter`, `modalEnter`, `modalAfterEnter`, `modalBeforeLeave`,
|
|
885
|
+
`modalLeave`, `modalAfterLeave` (`beforeEnter` and so on with no namespace).
|
|
886
|
+
The target is the first element of the content. `AfterLeave` is dispatched
|
|
887
|
+
once the element is out of the document.
|
|
888
|
+
- **What is awaited is the dispatch**, as in the bridge. An async bus returns
|
|
889
|
+
a promise and the transition waits for it, for at most `timeout`
|
|
890
|
+
milliseconds (default 30,000): a handler that never settles cannot strand
|
|
891
|
+
the element, and one that rejects, or a missing one, does not stop it either.
|
|
892
|
+
A sync bus answers at once, whatever its handler returned, so there the
|
|
893
|
+
element leaves at once.
|
|
894
|
+
- **`:bus` is optional**; without it the shared bus is used. The shared bus is
|
|
895
|
+
a sync bus: for a leave that must be waited for, pass an async bus.
|
|
896
|
+
- **Reopening during a leave cancels the leave.** `<namespace>LeaveCancelled`
|
|
897
|
+
is dispatched, the same element enters again, and the older answer is
|
|
898
|
+
ignored. Closing during an enter dispatches `<namespace>EnterCancelled` and
|
|
899
|
+
then the leave commands. Those are the bridge's two cancelled actions, in the
|
|
900
|
+
order Vue calls them for a `v-show`; a sync bus never produces one, since
|
|
901
|
+
nothing is ever waiting on it.
|
|
902
|
+
- **Mounting dispatches nothing**, unless `appear` is set: then content that
|
|
903
|
+
is there at mount gets the three enter commands.
|
|
904
|
+
- Vapor only, Vue >= 3.6, like `vapor-chamber/router/vapor`. It is named by the
|
|
905
|
+
rule now recorded in `docs/decisions.md`: `Vc` + the name of the Vue piece it
|
|
906
|
+
stands in for, opt-in, never a swap of Vue's own name.
|
|
907
|
+
|
|
908
|
+
Why it exists, measured on Vue 3.6.0-rc.10 in a Vite production bundle: Vue's
|
|
909
|
+
`VaporTransition` adds 4,400 B brotli to a Vapor app, 1,127 B of it CSS class
|
|
910
|
+
machinery that a command-driven transition never uses and no exported piece
|
|
911
|
+
lets a bundle leave out; with the bridge it is 4,737 B. `VcTransition` adds
|
|
912
|
+
428 B brotli (1,106 B minified) to an app that already uses the shared bus and
|
|
913
|
+
the five Vue helpers it is built from (`createIf`, `shallowRef`, `watch`,
|
|
914
|
+
`nextTick`, `onScopeDispose`), and 2,534 B to one that uses none of the five.
|
|
915
|
+
A show and hide cycle on a sync bus takes 0.39 of the time
|
|
916
|
+
`<Transition :css="false">` with the bridge takes (7.4 against 19.1
|
|
917
|
+
microseconds, production build, 15 interleaved rounds).
|
|
918
|
+
|
|
919
|
+
`createTransitionBridge` and `useTransitionCommand` are unchanged: they remain
|
|
920
|
+
the way to drive Vue's own `<Transition>` from the bus, on Vapor and on vDOM.
|
|
921
|
+
|
|
922
|
+
`tests/vapor/vc-transition.test.ts` runs every behaviour above twice, on the
|
|
923
|
+
dev build and against an executed production bundle, from templates compiled on
|
|
924
|
+
the installed Vue. `tests/vapor/vc-transition-helpers.test.ts` pins what the
|
|
925
|
+
module takes from Vue.
|
|
926
|
+
|
|
927
|
+
### Vue 3.6.0-rc.10 alignment
|
|
928
|
+
|
|
929
|
+
85 commits in `v3.6.0-rc.9..v3.6.0-rc.10`, the full log. Eighty-three were read
|
|
930
|
+
in full at source; the other two are the Vitest 5 chore and the release commit,
|
|
931
|
+
closed on their touched files. The peer range narrows to
|
|
932
|
+
`>=3.5.0 || >=3.6.0-rc.10` for `vue` and `@vue/reactivity`; both Vapor examples
|
|
933
|
+
are repinned and rebuilt. The alignment itself changed nothing in `src` except
|
|
934
|
+
two comments; the outlet fix below is separate. The cycle record is
|
|
935
|
+
`docs/rc-alignment-log.md`, section 35.
|
|
936
|
+
|
|
937
|
+
**What the bump gives you, each pinned by a test that fails on rc.9.**
|
|
938
|
+
|
|
939
|
+
- **`createDirectivePlugin()` on a Vapor child of a vDOM parent.** With
|
|
940
|
+
`vaporInteropPlugin`, `<VaporChild v-vc-command="act" />` kept its listener on
|
|
941
|
+
the child's first root. When a root `v-if` / `v-else` replaced that element,
|
|
942
|
+
the new one dispatched nothing, the detached one still did, and a `.delegate`
|
|
943
|
+
registration never released its document listener. On rc.10 the directive
|
|
944
|
+
follows the root. `tests/interop-vdom-directive-fixture.test.ts`.
|
|
945
|
+
- **A function set on a widget property arrives as the function.**
|
|
946
|
+
`el.onSave = fn` on a `defineWidget` element was called as a prop getter on
|
|
947
|
+
rc.9, and `setup(props)` received what it returned.
|
|
948
|
+
`tests/vapor/widget-shape.test.ts`.
|
|
949
|
+
- **`:onClick="fn"` beside `v-vc-command` in a Vapor template is a listener.**
|
|
950
|
+
On rc.9 it compiled to a property write and never ran. It now runs before the
|
|
951
|
+
directive and can veto the dispatch the way `@click` does.
|
|
952
|
+
`tests/directives-vapor-fixture.test.ts`.
|
|
953
|
+
|
|
954
|
+
**Upgrade notes.**
|
|
955
|
+
|
|
956
|
+
- **Server and client on the same RC.** rc.10 changes what the server renders:
|
|
957
|
+
a slot outlet showing its fallback is wrapped in `<!--(-->` ... `<!--)-->`,
|
|
958
|
+
and a Vapor `<TransitionGroup>` keeps its children's fragment markers. An
|
|
959
|
+
rc.9 client does not know the new marker. This is Vue's format, not this
|
|
960
|
+
library's: `vapor-chamber/ssr` holds no DOM and is unchanged.
|
|
961
|
+
- **Rows that register one action.** When rows join a keyed list that already
|
|
962
|
+
has rows, rc.10 creates them in source order; rc.9 created them last to
|
|
963
|
+
first. `register()` is last-wins, so if several rows register the same action
|
|
964
|
+
the owner is now the LAST inserted row, and `on()` listeners run in source
|
|
965
|
+
order. One row per action is unaffected.
|
|
966
|
+
- **A function as a widget property** is no longer called for you. If you
|
|
967
|
+
relied on `el.value = () => compute()` being evaluated, pass the value.
|
|
968
|
+
- **A function as a ROOT prop is still a getter**, on both RCs:
|
|
969
|
+
`createVaporChamberApp(App, { onSave: fn })` calls `fn`. Pass
|
|
970
|
+
`{ onSave: () => fn }`.
|
|
971
|
+
|
|
972
|
+
**Measured.** `npm run ab:vue -- 3.6.0-rc.9`, five runs: scope create and
|
|
973
|
+
dispose 1.027x under a 4.0% floor, ref write 0.997x under a 1.5% floor. Both
|
|
974
|
+
read SAME. Vue's Vapor runtime grew: the hand-wired `createVaporApp` minimum
|
|
975
|
+
went from 15.1 to 16.0 KB brotli, `+ vaporInteropPlugin` from 26.4 to 28.0, and
|
|
976
|
+
the Vapor outlet's own machinery from 4.13 to 4.58 against a ceiling of 5.0.
|
|
977
|
+
Every vapor-chamber export row is unchanged.
|
|
978
|
+
|
|
979
|
+
**Found and not fixed**, recorded with a reproduction in log section 35.10: vDOM
|
|
980
|
+
`<Transition mode="out-in">` throws when `done()` is called synchronously. A
|
|
981
|
+
plain `onLeave(el, done) { done() }` does it with no bridge involved; the
|
|
982
|
+
transition bridge on a sync bus is one way to reach it. It is on rc.9 as well,
|
|
983
|
+
and on Vue 3.5.43, in real Chromium as under happy-dom (log section 35.16).
|
|
984
|
+
Reported upstream as vuejs/core#15727; a fix is proposed in vuejs/core#11824,
|
|
985
|
+
and on that pull request's own build every arm of the reproduction renders,
|
|
986
|
+
while the default mode and `in-out` are unchanged (log section 35.39). The
|
|
987
|
+
limit is now documented on `createTransitionBridge`: with the bridge, vDOM
|
|
988
|
+
`out-in` needs an async bus until a Vue release carries the fix.
|
|
989
|
+
|
|
990
|
+
### Fixed: the Vapor `RouterOutlet`'s default slot went dead after a child route
|
|
991
|
+
|
|
992
|
+
`vapor-chamber/router/vapor`. A default slot is what the outlet shows when no
|
|
993
|
+
route matches at its depth. If that slot held anything reactive (a `{{ }}`, a
|
|
994
|
+
`v-if`), it stopped updating once a child route had been shown and the outlet
|
|
995
|
+
returned to the slot: it came back with the DOM it had when it left, and no
|
|
996
|
+
later change reached it. A static slot was unaffected, which is why the fixture
|
|
997
|
+
never saw it.
|
|
998
|
+
|
|
999
|
+
The outlet built the slot once in `setup` and handed the same block back. Vue
|
|
1000
|
+
stops a fragment's scope when it removes it, so the second time there was no
|
|
1001
|
+
effect behind the nodes. The slot is now created each time the no-match branch
|
|
1002
|
+
is entered, through `createIf`, which is the call compiler-vapor emits for
|
|
1003
|
+
`<component :is="c" v-if="c" /><slot v-else />`. An outlet with no default
|
|
1004
|
+
slot is unchanged and still leaves one anchor node.
|
|
1005
|
+
|
|
1006
|
+
Cost, measured: the module alone grows 49 bytes minified, 30 brotli, and its
|
|
1007
|
+
`BUNDLE-SIZES.md` row does not move at the table's resolution (0.4 KB brotli).
|
|
1008
|
+
In an app that has no `v-if` of its own, `createIf` joins the bundle and the
|
|
1009
|
+
outlet's own machinery goes from 4.58 to 4.74 KB brotli against the 5.0
|
|
1010
|
+
ceiling; an app that already uses `v-if` pays only the module's 30 bytes.
|
|
1011
|
+
`tests/vapor/vapor-outlet.test.ts` has the reactive-slot arm, on the dev build
|
|
1012
|
+
and on an executed production build; it failed on both before the fix.
|
|
1013
|
+
|
|
1014
|
+
### Fixed: a conditional default slot on the Vapor `RouterOutlet` never appeared if it started off
|
|
1015
|
+
|
|
1016
|
+
`vapor-chamber/router/vapor`. With
|
|
1017
|
+
|
|
1018
|
+
```vue
|
|
1019
|
+
<RouterOutlet>
|
|
1020
|
+
<template v-if="ready" #default>No page here</template>
|
|
1021
|
+
</RouterOutlet>
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
and `ready` false when the outlet was created, the fallback never showed: not
|
|
1025
|
+
when `ready` turned true, and not after a child route had come and gone. The
|
|
1026
|
+
same template with `ready` true at the start worked, and so did an
|
|
1027
|
+
unconditional slot. It is on v1.25.0 and Vue 3.6.0-rc.9 as well.
|
|
1028
|
+
|
|
1029
|
+
The outlet read `slots.default` once, in `setup`, to decide whether it had a
|
|
1030
|
+
fallback. A conditional slot is a dynamic slot source, and while its condition
|
|
1031
|
+
is off there is no `slots.default`, so the outlet took the no-slot path for its
|
|
1032
|
+
whole life. It now takes the slot path whenever the slots it was given carry a
|
|
1033
|
+
dynamic source, and `createSlot` follows the source from there. An outlet with
|
|
1034
|
+
no slot at all is unchanged.
|
|
1035
|
+
|
|
1036
|
+
Cost, measured: 18 bytes brotli in the module (471 to 489); its
|
|
1037
|
+
`BUNDLE-SIZES.md` row and the outlet guard's own-arm number do not move (0.4 and
|
|
1038
|
+
4.74 KB). `tests/vapor/vapor-outlet.test.ts` has the conditional-slot arm, on
|
|
1039
|
+
the dev build and on an executed production build; it failed on both before the
|
|
1040
|
+
fix. The outlet now reads one more property off what Vapor hands `setup`
|
|
1041
|
+
(`rawSlots`, beside `slots`); `tests/vapor/vapor-outlet-helpers.test.ts` pins
|
|
1042
|
+
it on the installed Vue, and if it is ever absent the outlet falls back to the
|
|
1043
|
+
old behaviour and does not throw.
|
|
1044
|
+
|
|
1045
|
+
### Documented: a function as a root prop is a getter
|
|
1046
|
+
|
|
1047
|
+
`createVaporChamberApp(App, rootProps)` passes `rootProps` to Vue as raw props,
|
|
1048
|
+
where a function is a getter. `{ onSave: fn }` calls `fn` and hands the
|
|
1049
|
+
component its return value; write `{ onSave: () => fn }`. The docblock now says
|
|
1050
|
+
so, with a test (`tests/vapor/vapor-subpath-wiring.test.ts`). Nothing changed
|
|
1051
|
+
in behaviour.
|
|
1052
|
+
|
|
1053
|
+
### Fixed: a throwing sync subscriber left `useCommand`'s `loading` stuck on true
|
|
1054
|
+
|
|
1055
|
+
`useCommand().dispatch` and `useCommandQuery().query` open by writing
|
|
1056
|
+
`loading = true` and `lastError = null`. A synchronous subscriber of either
|
|
1057
|
+
signal that throws (a raw `effect`, a `flush: 'sync'` watcher) throws out of
|
|
1058
|
+
that write, and the two writes sat before the `try`. The error escaped
|
|
1059
|
+
`dispatch`, the handler never ran, and `loading` stayed true until the next
|
|
1060
|
+
dispatch.
|
|
1061
|
+
|
|
1062
|
+
Both writes are now inside the `try`. The subscriber's error comes back as a
|
|
1063
|
+
failed `CommandResult`, `lastError` holds it, `loading` ends false, and the
|
|
1064
|
+
handler still does not run. A dispatch with no throwing subscriber is
|
|
1065
|
+
unchanged. `tests/rundispatch-throwing-subscriber.test.ts` has one test per
|
|
1066
|
+
write; both failed before. Cost: `dist/chamber.js` +4 bytes raw, -1 brotli;
|
|
1067
|
+
39.9 against 38.8 ns per call in an interleaved A/B, no slower.
|
|
1068
|
+
|
|
1069
|
+
### Fixed: the same throwing subscriber, at the other sites that write a signal before their bookkeeping
|
|
1070
|
+
|
|
1071
|
+
The shape above was found at five more sites. Each fix is the same move, the
|
|
1072
|
+
write goes inside the existing `try` or after the bookkeeping, and each has a
|
|
1073
|
+
test on real Vue `effect`s that failed first. A path with no throwing
|
|
1074
|
+
subscriber is unchanged at every one: same writes, same order.
|
|
1075
|
+
|
|
1076
|
+
- **`useVaporAsyncCommand().dispatch`.** The promise rejected with the
|
|
1077
|
+
subscriber's error and `loading` stayed true. It now resolves a failed
|
|
1078
|
+
`CommandResult`, `lastError` holds the error and `loading` ends false.
|
|
1079
|
+
`tests/vapor-async-throwing-subscriber.test.ts`. `dist/chamber-vapor.js`
|
|
1080
|
+
+4 bytes raw, +2 brotli; 120.4 against 120.4 ns per awaited call.
|
|
1081
|
+
- **`createFormBus`, `set()`.** A subscriber throwing on `values` left
|
|
1082
|
+
`errors`, `isDirty` and `isValid` unwritten: an invalid value read as valid.
|
|
1083
|
+
The three writes now happen whatever the `values` subscriber does. Still
|
|
1084
|
+
open: a subscriber that throws on `errors` or on `isDirty` skips the writes
|
|
1085
|
+
after it; covering those measured up to 5% slower per `set` and was not
|
|
1086
|
+
landed.
|
|
1087
|
+
- **`createFormBus`, `submit()`.** A subscriber throwing when `isValidating` or
|
|
1088
|
+
`isSubmitting` turned true left that flag true for good. `submit()` still
|
|
1089
|
+
rejects with the subscriber's error; the flag and `isBusy` now end false and
|
|
1090
|
+
the next `submit()` runs. `tests/form-throwing-subscriber.test.ts`.
|
|
1091
|
+
`dist/index.js` +64 bytes raw, +19 brotli.
|
|
1092
|
+
- **The router's commit.** A `currentRoute` subscriber that threw made a
|
|
1093
|
+
COMMITTED `push()` resolve `component_load_failed`, fire `onError` and skip
|
|
1094
|
+
`afterEach`, with the URL moved and the snapshot already the new page. The
|
|
1095
|
+
push now resolves `null`, `onCommit` and `afterEach` run, and the
|
|
1096
|
+
subscriber's error is logged as a post-commit hook's is.
|
|
1097
|
+
- **The router's background lanes.** A subscriber throwing when `isLoading`
|
|
1098
|
+
turned true on a query-only change, or when `isRevalidating` turned true,
|
|
1099
|
+
left that flag true for the life of the router, and the refetch or the
|
|
1100
|
+
refresh never ran. Both now run and lower their flag. The caller that
|
|
1101
|
+
triggered the write still gets the subscriber's error.
|
|
1102
|
+
`tests/router/engine-throwing-subscriber.test.ts`. `dist/router.js` +168
|
|
1103
|
+
bytes raw, +29 brotli; the commit statements measure the same, 37.5 against
|
|
1104
|
+
37.3 ns, inside a push of about 6,500 ns.
|
|
1105
|
+
- **`createWsBridge`.** A subscriber of `connected` that threw skipped what
|
|
1106
|
+
followed the write: on open the queue was not flushed and `onConnect` did not
|
|
1107
|
+
run; on close `onDisconnect` did not run and the bridge never reconnected; in
|
|
1108
|
+
`disconnect()` in-flight requests were not failed and the socket was left
|
|
1109
|
+
open. All three now do their bookkeeping whatever the subscriber does; its
|
|
1110
|
+
error still leaves the handler. `tests/ws-bridge-throwing-subscriber.test.ts`.
|
|
1111
|
+
The full IIFE is 20 bytes SMALLER than before these fixes (39,483 raw,
|
|
1112
|
+
12,234 brotli): two unreachable guards and a one-caller function in the same
|
|
1113
|
+
file paid for it.
|
|
1114
|
+
- **`useCommandHistory().undo()` and `redo()`, and the `history()` plugin.** A
|
|
1115
|
+
subscriber of `past`, `future`, `canUndo` or `canRedo` that threw while the
|
|
1116
|
+
stacks moved threw out of `undo()`: the undo handler never ran, the ledger
|
|
1117
|
+
had already moved the command to the redo stack, and the four signals were
|
|
1118
|
+
left half written. The history read "undone" over an application that was
|
|
1119
|
+
not. The move is now inside the `try` that guards the call, so a throw there
|
|
1120
|
+
is an undo that did not land: the stacks move back, all four signals agree,
|
|
1121
|
+
and the error is logged as a throwing undo handler's is.
|
|
1122
|
+
`tests/history-throwing-subscriber.test.ts`. `dist/settled.js` +2 bytes.
|
|
1123
|
+
|
|
5
1124
|
## v1.25.0 - 2026-09-29
|
|
6
1125
|
|
|
7
1126
|
### Changed (breaking): a failure's code is `owner:condition:subject`, and the owner comes from the wiring
|
|
@@ -221,8 +1340,8 @@ A custom `isRetryable` reads the same condition the default rule does
|
|
|
221
1340
|
(`failureCondition(error)`: a library failure's own condition, an HTTP
|
|
222
1341
|
response's status through the status table, a timeout, an abort, `lost`),
|
|
223
1342
|
instead of matching statuses inside `error.message`, which is what
|
|
224
|
-
`examples/feature-retry.ts` taught; it now
|
|
225
|
-
|
|
1343
|
+
`examples/feature-retry.ts` taught; it now reads the condition (`conditionOf`)
|
|
1344
|
+
and leaves the re-sending to the async bus.
|
|
226
1345
|
|
|
227
1346
|
### Changed (breaking, reference controller): an `Idempotency-Key` that is not a String is ignored
|
|
228
1347
|
|