vapor-chamber 1.23.0 → 1.25.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 +649 -0
- package/README.md +115 -82
- package/ROADMAP.md +3 -3
- package/dist/LICENSE.txt +6 -3
- package/dist/a11y.d.ts +29 -0
- package/dist/a11y.d.ts.map +1 -0
- package/dist/a11y.js +43 -0
- package/dist/a11y.js.map +1 -0
- package/dist/chamber-vapor.d.ts +8 -19
- package/dist/chamber-vapor.d.ts.map +1 -1
- package/dist/chamber-vapor.js +2 -2
- package/dist/chamber-vapor.js.map +1 -1
- package/dist/chamber.d.ts +22 -59
- package/dist/chamber.d.ts.map +1 -1
- package/dist/chamber.js +53 -71
- package/dist/chamber.js.map +1 -1
- package/dist/command-bus.d.ts +153 -131
- package/dist/command-bus.d.ts.map +1 -1
- package/dist/command-bus.js +269 -187
- 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 +42 -78
- package/dist/directives.d.ts.map +1 -1
- package/dist/directives2.js +61 -3
- package/dist/directives2.js.map +1 -1
- package/dist/form.d.ts +30 -1
- package/dist/form.d.ts.map +1 -1
- package/dist/http-cache.d.ts +6 -9
- package/dist/http-cache.d.ts.map +1 -1
- package/dist/http-errors.d.ts +28 -20
- package/dist/http-errors.d.ts.map +1 -1
- package/dist/http.d.ts +19 -54
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +43 -29
- package/dist/http.js.map +1 -1
- package/dist/iife-core.d.ts +8 -10
- package/dist/iife-core.d.ts.map +1 -1
- package/dist/iife-core.js +13 -10
- package/dist/iife-core.js.map +1 -1
- package/dist/iife-elements.d.ts +9 -14
- package/dist/iife-elements.d.ts.map +1 -1
- package/dist/iife-elements.js +12 -9
- package/dist/iife-elements.js.map +1 -1
- package/dist/iife.d.ts +7 -9
- package/dist/iife.d.ts.map +1 -1
- package/dist/iife.js +13 -9
- package/dist/iife.js.map +1 -1
- package/dist/index.d.ts +9 -37
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +189 -124
- package/dist/index.js.map +1 -1
- package/dist/keys.js.map +1 -1
- package/dist/ledger.d.ts +29 -0
- package/dist/ledger.d.ts.map +1 -0
- package/dist/ledger.js +59 -0
- package/dist/ledger.js.map +1 -0
- package/dist/mcp.d.ts +5 -25
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +4 -7
- package/dist/mcp.js.map +1 -1
- package/dist/outbox.d.ts +4 -3
- package/dist/outbox.d.ts.map +1 -1
- package/dist/outbox.js +18 -9
- package/dist/outbox.js.map +1 -1
- package/dist/plugins-core.d.ts +8 -18
- package/dist/plugins-core.d.ts.map +1 -1
- package/dist/plugins-core.js +197 -0
- package/dist/plugins-core.js.map +1 -0
- package/dist/plugins-extra.d.ts.map +1 -1
- package/dist/plugins-io.d.ts +7 -83
- package/dist/plugins-io.d.ts.map +1 -1
- package/dist/plugins-io.js +4 -289
- package/dist/plugins-io.js.map +1 -1
- package/dist/plugins-schema.d.ts +1 -1
- package/dist/plugins-schema.d.ts.map +1 -1
- package/dist/plugins-schema.js +3 -5
- package/dist/plugins-schema.js.map +1 -1
- package/dist/plugins.d.ts +2 -2
- package/dist/plugins.d.ts.map +1 -1
- package/dist/router/announce.d.ts +14 -0
- package/dist/router/announce.d.ts.map +1 -0
- package/dist/router/composables.d.ts +2 -3
- package/dist/router/composables.d.ts.map +1 -1
- package/dist/router/dom.d.ts +5 -0
- package/dist/router/dom.d.ts.map +1 -1
- package/dist/router/engine.d.ts +7 -8
- package/dist/router/engine.d.ts.map +1 -1
- package/dist/router/errors.d.ts +4 -4
- package/dist/router/errors.d.ts.map +1 -1
- package/dist/router/index.d.ts +14 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/loaders.d.ts.map +1 -1
- package/dist/router/menu.d.ts +7 -2
- package/dist/router/menu.d.ts.map +1 -1
- package/dist/router/remote.d.ts +8 -13
- package/dist/router/remote.d.ts.map +1 -1
- package/dist/router/remote.js.map +1 -1
- package/dist/router/revalidate.d.ts +9 -16
- package/dist/router/revalidate.d.ts.map +1 -1
- package/dist/router/table.d.ts +4 -3
- package/dist/router/table.d.ts.map +1 -1
- package/dist/router/types.d.ts +5 -2
- package/dist/router/types.d.ts.map +1 -1
- package/dist/router/url.d.ts.map +1 -1
- package/dist/router/vapor.d.ts +6 -8
- package/dist/router/vapor.d.ts.map +1 -1
- package/dist/router/vapor.js.map +1 -1
- package/dist/router/vdom.js.map +1 -1
- package/dist/router.js +104 -29
- package/dist/router.js.map +1 -1
- package/dist/scheduler.d.ts +31 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/schema.d.ts +30 -9
- package/dist/schema.d.ts.map +1 -1
- package/dist/settled.d.ts +28 -55
- package/dist/settled.d.ts.map +1 -1
- package/dist/settled.js +24 -1
- package/dist/settled.js.map +1 -1
- package/dist/signal.d.ts.map +1 -1
- package/dist/signal.js +3 -5
- package/dist/signal.js.map +1 -1
- package/dist/ssr.d.ts +8 -40
- package/dist/ssr.d.ts.map +1 -1
- package/dist/ssr2.js +2 -2
- package/dist/ssr2.js.map +1 -1
- package/dist/store.d.ts +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +5 -2
- package/dist/store.js.map +1 -1
- package/dist/stream-parser.d.ts +2 -4
- package/dist/stream-parser.d.ts.map +1 -1
- package/dist/stream-parser.js +2 -4
- package/dist/stream-parser.js.map +1 -1
- package/dist/testing.d.ts +14 -5
- package/dist/testing.d.ts.map +1 -1
- package/dist/transitions.d.ts +14 -36
- package/dist/transitions.d.ts.map +1 -1
- package/dist/transitions2.js.map +1 -1
- package/dist/transports.d.ts +27 -41
- package/dist/transports.d.ts.map +1 -1
- package/dist/transports.js +1 -414
- package/dist/transports2.js +415 -0
- package/dist/transports2.js.map +1 -0
- package/dist/utilities.d.ts +1 -1
- package/dist/utilities.d.ts.map +1 -1
- package/dist/vapor-chamber-core.iife.js +625 -762
- 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 +643 -790
- 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 +928 -949
- package/dist/vapor-chamber.iife.js.map +1 -1
- package/dist/vapor-chamber.iife.min.js +1 -1
- package/dist/vapor.d.ts +3 -3
- package/dist/vapor.js.map +1 -1
- package/dist/vite-hmr.d.ts +33 -91
- package/dist/vite-hmr.d.ts.map +1 -1
- package/dist/vite-hmr.js +3 -4
- package/dist/vite-hmr.js.map +1 -1
- package/dist/vitest-mcp.js +3 -3
- package/dist/vitest-mcp.js.map +1 -1
- package/dist/vitest-pure.d.ts +10 -20
- package/dist/vitest-pure.d.ts.map +1 -1
- package/dist/vitest-pure.js +11 -21
- package/dist/vitest-pure.js.map +1 -1
- package/dist/vitest.d.ts.map +1 -1
- package/dist/vitest.js.map +1 -1
- package/dist/vue.d.ts +9 -18
- package/dist/vue.d.ts.map +1 -1
- package/dist/vue.js.map +1 -1
- package/package.json +5 -2
- package/scripts/build.mjs +11 -13
- package/scripts/check-ascii.mjs +8 -17
- package/scripts/check-example-directive.mjs +7 -6
- package/scripts/check-size.mjs +119 -3
- package/scripts/gate.mjs +0 -3
- package/scripts/measure-size.mjs +14 -22
- package/scripts/serve-docs.mjs +4 -5
- package/scripts/stamp-docs.mjs +7 -12
- package/src/a11y.ts +67 -0
- package/src/chamber-vapor.ts +12 -22
- package/src/chamber.ts +101 -185
- package/src/command-bus.ts +504 -427
- package/src/dict.ts +1 -1
- package/src/directives.ts +186 -156
- package/src/form.ts +112 -4
- package/src/http-cache.ts +6 -9
- package/src/http-errors.ts +26 -20
- package/src/http.ts +66 -103
- package/src/iife-core.ts +13 -18
- package/src/iife-elements.ts +14 -22
- package/src/iife.ts +11 -16
- package/src/index.ts +39 -49
- package/src/ledger.ts +115 -0
- package/src/mcp.ts +12 -45
- package/src/outbox.ts +31 -19
- package/src/plugins-core.ts +70 -157
- package/src/plugins-extra.ts +32 -54
- package/src/plugins-io.ts +18 -230
- package/src/plugins-schema.ts +6 -6
- package/src/plugins.ts +1 -3
- package/src/router/announce.ts +88 -0
- package/src/router/composables.ts +25 -23
- package/src/router/dom.ts +23 -16
- package/src/router/engine.ts +32 -43
- package/src/router/errors.ts +6 -7
- package/src/router/index.ts +45 -34
- package/src/router/loaders.ts +6 -8
- package/src/router/menu.ts +7 -2
- package/src/router/outlet.ts +2 -2
- package/src/router/remote.ts +8 -13
- package/src/router/revalidate.ts +38 -40
- package/src/router/table.ts +41 -25
- package/src/router/types.ts +5 -2
- package/src/router/url.ts +9 -12
- package/src/router/vapor.ts +8 -11
- package/src/scheduler.ts +70 -0
- package/src/schema.ts +75 -54
- package/src/settled.ts +61 -59
- package/src/signal.ts +7 -10
- package/src/ssr.ts +14 -58
- package/src/store.ts +21 -10
- package/src/stream-parser.ts +6 -10
- package/src/testing.ts +43 -30
- package/src/transitions.ts +27 -64
- package/src/transports.ts +140 -216
- package/src/utilities.ts +10 -15
- package/src/vapor.ts +3 -3
- package/src/vite-hmr.ts +50 -124
- package/src/vitest-mcp.ts +4 -4
- package/src/vitest-pure.ts +27 -37
- package/src/vitest.ts +6 -2
- package/src/vue.ts +10 -19
- package/dist/http-errors.js +0 -11
- package/dist/http-errors.js.map +0 -1
- package/dist/transports.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,655 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## v1.25.0 - 2026-09-29
|
|
6
|
+
|
|
7
|
+
### Changed (breaking): a failure's code is `owner:condition:subject`, and the owner comes from the wiring
|
|
8
|
+
|
|
9
|
+
Every failure the library, a plugin or a transport raises is a `BusError` whose
|
|
10
|
+
`code` says who raised it, what went wrong from a closed vocabulary (`missing`,
|
|
11
|
+
`already`, `invalid`, `refused`, `limited`, `timeout`, `lost`, `aborted`,
|
|
12
|
+
`exceeded`, `failed`, `unexpected`, `unknown`), and what it is about. The owner
|
|
13
|
+
is never typed at a site: the bus hands each party a `fail` bound to its owner
|
|
14
|
+
(`core` inside the bus, a plugin's declared `id`, `transport`, `remote` for a
|
|
15
|
+
backend), so no party can raise under another's name. Designed from RFC 3463's
|
|
16
|
+
`class.subject.detail`, AIP-193's (domain, reason), OpenTelemetry's scope bound
|
|
17
|
+
at `getTracer(name)` and Temporal's runtime-only failure kinds
|
|
18
|
+
(docs/plan-failures-and-contract.md 4.5).
|
|
19
|
+
|
|
20
|
+
| Was | Is |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `VC_CORE_NO_HANDLER` | `core:missing:handler` |
|
|
23
|
+
| `VC_CORE_THROTTLED` | `core:limited:handler` (`register({ throttle })`), `throttle:limited:handler` (the plugin) |
|
|
24
|
+
| `VC_CORE_REQUEST_TIMEOUT` | `core:timeout:request` |
|
|
25
|
+
| `VC_CORE_MAX_DEPTH` / `VC_CORE_SEALED` / `VC_CORE_ABORTED` | `core:exceeded:depth` / `core:refused:bus` / `core:aborted:dispatch` |
|
|
26
|
+
| `VC_CORE_BEFORE_CANCEL` | `core:refused:hook` |
|
|
27
|
+
| `VC_PLUGIN_THREW` | `<plugin id>:failed:plugin` (`plugin:failed:plugin` when it declares none) |
|
|
28
|
+
| `VC_PLUGIN_CIRCUIT_OPEN` / `VC_PLUGIN_RATE_LIMITED` | `circuitBreaker:limited:action` / `rateLimit:limited:action` |
|
|
29
|
+
| `VC_VALIDATION_FAILED` | `validator:invalid:payload`, `validateSchemas:invalid:payload` |
|
|
30
|
+
| `VC_TRANSPORT_TIMEOUT` / `_CLOSED` / `_QUEUE_FULL` | `transport:timeout:reply` / `transport:lost:reply` / `transport:lost:command` |
|
|
31
|
+
| `VC_TRANSPORT_REDIRECT` / `_PROTOCOL` | `transport:refused:redirect` / `transport:lost:result` |
|
|
32
|
+
| `VC_UNKNOWN` | `core:unknown:error`, no longer retryable |
|
|
33
|
+
| `VC_TEST_*` (seven) | `test:missing:tap`, `test:already:instance`, `test:unexpected:version`, `test:lost:tap`, `test:invalid:files`, `test:missing:run`, `test:missing:coverage` |
|
|
34
|
+
| a backend's failure (any path) | `remote:<condition of its status>:<its code>` (next entry) |
|
|
35
|
+
|
|
36
|
+
**Migrating:** compare the new strings. `RETRYABLE_CODES` is
|
|
37
|
+
`RETRYABLE_CONDITIONS` (`limited`, `timeout`); new `ownerOf(e)` and
|
|
38
|
+
`conditionOf(e)` read the parts, `failureCondition(e)` any failure's.
|
|
39
|
+
`BusError.severity`, `BusError.emitter` and the `BusEmitter` type are gone:
|
|
40
|
+
whoever logs a failure decides its level (the catalogue's `severity` is the
|
|
41
|
+
suggestion), and the owner is in the code. A hand-built `new BusError('limited:quota', ...)`
|
|
42
|
+
is the app's (`app:limited:quota`). A plugin declares its owner as `id`
|
|
43
|
+
(`Object.assign(fn, { id: 'myPlugin' })`) and receives its `fail` as a third
|
|
44
|
+
argument; a plugin that calls another passes its own `fail` on; a plugin called
|
|
45
|
+
outside a bus in a test is wrapped with the new `wired(plugin)`, exported
|
|
46
|
+
from `vapor-chamber` beside `createTestBus` (not from `vapor-chamber/vitest`,
|
|
47
|
+
whose pure entry imports nothing from the library at runtime).
|
|
48
|
+
|
|
49
|
+
**What it closes.** A backend could make a permanent refusal retryable by
|
|
50
|
+
sending one of six library code strings in a 2xx body (the residue stated in
|
|
51
|
+
v1.23.0). The verdict is now the failure's condition: a backend's code is the
|
|
52
|
+
subject, after the owner and the condition, so no string it sends can reach
|
|
53
|
+
the verdict. `code` is read-only (a private field with a getter), so neither
|
|
54
|
+
owner nor condition can be rewritten after the fact.
|
|
55
|
+
|
|
56
|
+
**What else changed with it.**
|
|
57
|
+
- `toJSON()` is the failure as an RFC 9457 problem with the members it needs:
|
|
58
|
+
`detail`, `code`, `action`, the context as extensions; `type` implicit. The
|
|
59
|
+
transports' reader reads that shape back, so a failure crossing a worker, a
|
|
60
|
+
channel or storage has one shape both ways.
|
|
61
|
+
- The stack is decided once, by the constructor: only `failed` (a bug) captures
|
|
62
|
+
one; every expected refusal is control flow and skips it.
|
|
63
|
+
- The catalogue lost its `emitter` column (the owner is the code's first part);
|
|
64
|
+
its `retryable` column is an outcome of the condition, asserted. The registry
|
|
65
|
+
sweep now asks which site MINTS each code: every minted code has a row, and
|
|
66
|
+
the eight declared-but-never-raised codes are listed with their reasons.
|
|
67
|
+
- The test double mints as `core`, so a test reads the codes production does.
|
|
68
|
+
|
|
69
|
+
**Speed** (three-arm A/B, production, 11 rotated rounds, against the previous
|
|
70
|
+
tree; the self-control arm is the noise band): the happy path is unchanged,
|
|
71
|
+
0.99-1.03x on bare, three-plugin, async, throttle/circuit/rateLimit passing and
|
|
72
|
+
`retry` passing. Failures that used to capture a stack are 4-12x faster: no
|
|
73
|
+
handler 0.086x, circuit open 0.097x, rate limit 0.101x, `retry` on a permanent
|
|
74
|
+
refusal 0.26x. Refusals that already skipped it are within noise
|
|
75
|
+
(1.02-1.07x). Locking `code` by `freeze` or a non-writable property cost 10-30%
|
|
76
|
+
on those refusals and was not shipped.
|
|
77
|
+
|
|
78
|
+
**Size**, measured on the new IIFE target (next entry), against the old code on
|
|
79
|
+
that same target: full +79 / +57, core +12 / +58, elements +12 / +46 raw /
|
|
80
|
+
brotli; the budgets are set to the new, lower numbers. The Blade consumer
|
|
81
|
+
bundle (ESM) measures 6,328 brotli against 6,260; its ceiling moves to 6,340,
|
|
82
|
+
with the steps in `tests/esm-treeshake.test.ts`.
|
|
83
|
+
|
|
84
|
+
### Changed (breaking): one wire contract, and retries by it
|
|
85
|
+
|
|
86
|
+
The answer to a command is the same shape on every wire
|
|
87
|
+
(docs/plan-failures-and-contract.md 4.4): `{ state }` on success, `{ redirect }`
|
|
88
|
+
for a navigation, an RFC 9457 problem on failure, with only the members it
|
|
89
|
+
uses: `{ status, code, detail, errors? }`. A single command's failure is its
|
|
90
|
+
non-2xx response; a batched result or a WebSocket frame is `{ id, ... }`. The
|
|
91
|
+
client reads a problem as `remote:<condition of its status>:<code>`, the
|
|
92
|
+
condition from a status table that says only what RFC 9110 says of a status,
|
|
93
|
+
`detail` the message, everything else `error.context`. Removed, on both sides:
|
|
94
|
+
`{ ok: false, error, code }`, the `ok` flag, `error` and `message` as message
|
|
95
|
+
fields, `type` and `title`; `FormBus.setErrors` takes only
|
|
96
|
+
`[{ pointer: '/payload/<field>', detail }]`, which the reference controller now
|
|
97
|
+
sends for a `ValidationException` (it sent no field errors at all).
|
|
98
|
+
|
|
99
|
+
Retries read the same contract (`failureCondition(error)`); who retries, and
|
|
100
|
+
what, is the next section. A redirect (`transport:refused:redirect`) is never
|
|
101
|
+
re-sent, and a batch that answers without a command's result
|
|
102
|
+
(`transport:lost:result`, it may have run) is re-sent only with a key - 1.24
|
|
103
|
+
re-sent neither, and `tests/wire-contract.test.ts` pins both. The bridges'
|
|
104
|
+
`retry` and `noRetry` options are gone (a declaration of "safe to send again"
|
|
105
|
+
replaces them). The outbox keeps a record whose answer declared a
|
|
106
|
+
`Retry-After` and flushes again when that wait is over; `dispose()` ends the
|
|
107
|
+
wait. `serialize()`'s lanes, the retry's waits and the outbox's wait are one
|
|
108
|
+
module (`src/scheduler.ts`).
|
|
109
|
+
|
|
110
|
+
No external consumers; the reference controller, the example servers, the
|
|
111
|
+
guides and the tests moved with it (`tests/wire-contract.test.ts`).
|
|
112
|
+
|
|
113
|
+
### Changed (breaking): the async bus retries, by class, and `retry()` / `retrying()` are gone
|
|
114
|
+
|
|
115
|
+
The async bus re-sends the call that produced the outcome - a handler, or a
|
|
116
|
+
plugin declaring `transport: true` (the three bridges do) - so the plugins
|
|
117
|
+
outside see one dispatch (docs/plan-shape.md 4). On by default:
|
|
118
|
+
|
|
119
|
+
- **By class.** Transient, always: `limited`, `timeout`, any failure declaring
|
|
120
|
+
`retryIn` (a `Retry-After`), after that wait. Uncertain - `lost`, `failed`
|
|
121
|
+
(a 500, a handler's throw), `unexpected`, `unknown`: the first attempt may
|
|
122
|
+
have landed - only for an action declared idempotent or a command carrying
|
|
123
|
+
an idempotency key. Never: a verdict, an abort, a depth bound, a library
|
|
124
|
+
bug, the bus's own `register({ throttle })` refusal (a throttle drops
|
|
125
|
+
repeats), an outbox replay (its scheduler owns it).
|
|
126
|
+
- **Bounded.** 3 attempts; full jitter under `200ms * 2^n`, capped at 20s; a
|
|
127
|
+
declared wait clamped to the same cap. `dispose()` and the dispatch's own
|
|
128
|
+
signal end a wait (`core:aborted:dispatch`). A failure sent more than once
|
|
129
|
+
carries `context.attempts`.
|
|
130
|
+
- **Budgeted per bus** (gRPC A6): 10 tokens, a failure costs one, a success
|
|
131
|
+
refunds a tenth, no retry below half.
|
|
132
|
+
- **Declared.** `createAsyncCommandBus({ retry: { maxAttempts, baseDelay,
|
|
133
|
+
maxDelay, actions: { 'cart*': 'idempotent', orderPay: false, searchRun: 2 } } })`;
|
|
134
|
+
`retry: false` turns it off; `retry` on a schema action joins
|
|
135
|
+
`retry.actions`. An action declared idempotent gets its dispatch id as its
|
|
136
|
+
`Idempotency-Key`, one for every attempt.
|
|
137
|
+
|
|
138
|
+
Migration: `bus.use(retry(opts))` and `bus.use(retrying(bridge))` go; use the
|
|
139
|
+
bridge alone and declare on the bus. 1.24's `retry()` re-sent a 500 and a
|
|
140
|
+
handler's plain throw by default; now declare the action `'idempotent'` for
|
|
141
|
+
that. `RETRYABLE_CONDITIONS` is `limited` and `timeout`, and the registry's
|
|
142
|
+
`retryable` column follows it (`core:unknown:error` reads false). The IIFEs'
|
|
143
|
+
`connect()` and `createApp()` take the bus's `retry` option in place of
|
|
144
|
+
`retry: n`. `conditionOfStatus` and `failureCondition` moved into the core
|
|
145
|
+
(still exported from the root; no longer from `http-errors`). New types from the root: `AsyncCommandBusOptions`,
|
|
146
|
+
`RetryOptions`, `RetryDeclaration`, `AsyncSchemaCommandBusOptions`. Size: the IIFEs
|
|
147
|
+
+114 to +136 B brotli, the policy now inside the async bus they create.
|
|
148
|
+
The reference controller answers its `409 in_progress` (a key still running)
|
|
149
|
+
with `Retry-After: 1`, so the bus's re-send comes back for the finished answer
|
|
150
|
+
instead of settling as a conflict; the examples, guides and READMEs moved
|
|
151
|
+
with it. `tests/retry-policy.test.ts`.
|
|
152
|
+
|
|
153
|
+
### Changed: the IIFE bundles target 2022 browsers
|
|
154
|
+
|
|
155
|
+
The `<script>`-tag builds target Chrome 100, Firefox 100 and Safari 16 (was
|
|
156
|
+
Chrome 80, Firefox 75, Safari 13). Below that floor `?.`, `??`, class fields and
|
|
157
|
+
private fields were lowered into helpers in every bundle; above it all three are
|
|
158
|
+
native. Core IIFE 27,164 -> 25,923 raw, 7,992 -> 7,727 brotli, with the change
|
|
159
|
+
above included. A page on an older browser keeps working with the previous
|
|
160
|
+
release's IIFE. The ESM build (what apps bundle) was already es2022.
|
|
161
|
+
|
|
162
|
+
### Changed: the async bus's `request()` settles one promise
|
|
163
|
+
|
|
164
|
+
It raced three (the dispatch, the timer, the caller's abort); it now settles
|
|
165
|
+
one through a single `done`, as the sync bus's `request()` does, and removes
|
|
166
|
+
its dedup entry only while that entry is still its own. Measured on a request
|
|
167
|
+
whose responder answers at once: 2,958 -> 2,293 ns. Settled results are
|
|
168
|
+
unchanged (`tests/request-dispose-ab.test.ts`).
|
|
169
|
+
|
|
170
|
+
### Changed (breaking): the HTTP client's `safe` helpers return the contract's problem
|
|
171
|
+
|
|
172
|
+
`http.safe.*` resolved `error` to the response body as it came, or to
|
|
173
|
+
`{ message, code }` built for the occasion. It is now the RFC 9457 problem the
|
|
174
|
+
wire contract uses (`ProblemDetails`, now exported from `http-errors`): the
|
|
175
|
+
backend's body with its `status` added, `detail` its sentence or
|
|
176
|
+
`HTTP <status>`; with no response (a timeout, an abort, the network)
|
|
177
|
+
`{ detail }` only. Read `error.detail` where you read `error.message`.
|
|
178
|
+
|
|
179
|
+
`HttpError.status` is gone: it always equalled `response.status`, which the
|
|
180
|
+
HttpClient contract already guarantees a failure carries. Read
|
|
181
|
+
`error.response?.status`; `failureCondition` and the outbox's verdict read it
|
|
182
|
+
there too, so a custom client's error is judged by its `response`, not by a
|
|
183
|
+
bare `status` field.
|
|
184
|
+
|
|
185
|
+
### Changed (breaking, types only): `Plugin` is typed for either bus
|
|
186
|
+
|
|
187
|
+
`Plugin` declared `next: () => CommandResult` while installing on the async
|
|
188
|
+
bus, where `next()` may be a promise; the built-in plugins escaped it with
|
|
189
|
+
`as unknown as Plugin` and `const plugin: any`. It is now generic:
|
|
190
|
+
`<R extends CommandResult | Promise<CommandResult>>(cmd, next: () => R, fail) => R | CommandResult`,
|
|
191
|
+
a sync plugin on the sync bus and an async one on the async bus. A plugin that
|
|
192
|
+
reads the result settles it through `onSettled` (now exported, with
|
|
193
|
+
`MaybeAsyncResult`); reading `.ok` straight off `next()` no longer compiles as
|
|
194
|
+
a `Plugin`, which is the defect it would be on the async bus. A plugin for one
|
|
195
|
+
bus only is a `SyncPlugin` or an `AsyncPlugin`; `PluginParts` is what every
|
|
196
|
+
plugin may carry (`dispose`, `id`). The async bus has one `use(AsyncPlugin)`
|
|
197
|
+
(a `Plugin` fits it), `BaseBus.use` takes a `Plugin` rather than `any`, and
|
|
198
|
+
the IIFEs' `connect({ plugins })` takes `AsyncPlugin[]`. No cast is left in the
|
|
199
|
+
built-in plugins. It found the defect in two example plugins and in the
|
|
200
|
+
README's timing example, all corrected.
|
|
201
|
+
|
|
202
|
+
### Changed: every `limited` refusal declares when to come back
|
|
203
|
+
|
|
204
|
+
`circuitBreaker()` and `rateLimit()` refuse with `context.retryIn`, as a
|
|
205
|
+
throttle and a `Retry-After` already did: the time left until the circuit goes
|
|
206
|
+
half-open, and until the oldest dispatch leaves the window. `retry()` waits
|
|
207
|
+
that time instead of spending its attempts on refusals, and the outbox flushes
|
|
208
|
+
again when it is over, whatever order the plugins are installed in. No new
|
|
209
|
+
option and no shared state: the wait travels on the failure, where the
|
|
210
|
+
contract already carries it.
|
|
211
|
+
|
|
212
|
+
### Removed (breaking): `agentOrigin()` from `vapor-chamber/mcp`
|
|
213
|
+
|
|
214
|
+
A no-op since v1.12.0, deprecated and "kept for one release": the core stamps
|
|
215
|
+
`meta.origin === 'agent'` on MCP-driven dispatches itself. Delete the
|
|
216
|
+
`bus.use(agentOrigin(), ...)` line. Its tests now assert the core's stamp.
|
|
217
|
+
|
|
218
|
+
### Added: `failureCondition` from the package root
|
|
219
|
+
|
|
220
|
+
A custom `isRetryable` reads the same condition the default rule does
|
|
221
|
+
(`failureCondition(error)`: a library failure's own condition, an HTTP
|
|
222
|
+
response's status through the status table, a timeout, an abort, `lost`),
|
|
223
|
+
instead of matching statuses inside `error.message`, which is what
|
|
224
|
+
`examples/feature-retry.ts` taught; it now uses it, and `retrying()` for the
|
|
225
|
+
wire.
|
|
226
|
+
|
|
227
|
+
### Changed (breaking, reference controller): an `Idempotency-Key` that is not a String is ignored
|
|
228
|
+
|
|
229
|
+
The PHP controller accepted a raw, unquoted key "from a client that predates
|
|
230
|
+
the quoting". A value that is not a Structured Field String fails to parse,
|
|
231
|
+
and RFC 9651 ignores a field that fails to parse: the request runs with no
|
|
232
|
+
key.
|
|
233
|
+
|
|
234
|
+
### Fixed: the examples teach the current shapes
|
|
235
|
+
|
|
236
|
+
Read in full, every example and companion: the Laravel app and backend, the
|
|
237
|
+
Astro, island-cart, Vapor SFC and router demos, the patterns and the feature
|
|
238
|
+
snippets. Old shapes replaced (`{ ok }` answers, `quantity`, a composable from
|
|
239
|
+
the package root in a Vue component, `useCommand` inside React, statuses read
|
|
240
|
+
out of `error.message`, a hand-rolled retry, uncoded plugin refusals, the
|
|
241
|
+
PHP actions validating the target so a field error pointed at a payload field
|
|
242
|
+
that did not exist); pressed buttons use `aria-disabled` instead of dropping
|
|
243
|
+
keyboard focus with `disabled`; outcomes reach a live region or `announce()`;
|
|
244
|
+
text colours meet 4.5:1 (WCAG 1.4.3); the static example server no longer
|
|
245
|
+
crashes on a malformed URL.
|
|
246
|
+
|
|
247
|
+
### Fixed: the sprinkled-Blade example is accessible
|
|
248
|
+
|
|
249
|
+
It disabled the focused button while busy, which sends a keyboard user's focus
|
|
250
|
+
to `<body>` (the defect `v-vc-command` avoids), wrote the backend's message
|
|
251
|
+
into `innerHTML`, and its status line was not a live region. It now marks the
|
|
252
|
+
button `aria-disabled` and ignores a press in flight, writes text, and
|
|
253
|
+
announces the outcome (`role="status"`).
|
|
254
|
+
|
|
255
|
+
### Changed: source comments carry the current reason, not the release history
|
|
256
|
+
|
|
257
|
+
Release-by-release logs in module headers, "used to" narratives and version
|
|
258
|
+
tags on comments are rewritten as the rule each one established, keeping the
|
|
259
|
+
measurement or the test that pins it; the history stays in this file.
|
|
260
|
+
|
|
261
|
+
### Fixed (router): typed params match only their type
|
|
262
|
+
|
|
263
|
+
A row's `params: { id: 'int' }` compiled to `[^/]+` and cast with `parseInt`,
|
|
264
|
+
so `/products/7x` and `/products/7.5` resolved product 7 and `/products/abc`
|
|
265
|
+
handed an `int` param over as the string `'abc'`; a `bool` read anything but
|
|
266
|
+
`1`/`true` as false. A typed param with no regex of its own now matches only
|
|
267
|
+
its type (`int`: `-?\d+`, `bool`: `1|0|true|false`, case-insensitive like the
|
|
268
|
+
rest of matching), so a URL that does not fit falls through to the next row.
|
|
269
|
+
|
|
270
|
+
### Fixed (router): query params and pagination hand out values, not the location
|
|
271
|
+
|
|
272
|
+
- `decodeQueryParam` returned a present array param by reference, so a
|
|
273
|
+
component's `tags.value.push(...)` rewrote the committed location's query
|
|
274
|
+
while the URL stayed as it was. It returns a copy, as it already did for a
|
|
275
|
+
declared default.
|
|
276
|
+
- `usePagination()`'s `page` is a number even on a route that declares no
|
|
277
|
+
`page` param (it read `?page=2` as `'2'`); the route's own declaration still
|
|
278
|
+
wins.
|
|
279
|
+
|
|
280
|
+
### Fixed (router): `revalidateRoutes` never fails the command it follows
|
|
281
|
+
|
|
282
|
+
A revalidation map naming a record outside the current chain threw inside the
|
|
283
|
+
plugin after the mutation had succeeded: on a sync bus the command came back
|
|
284
|
+
as `plugin:failed:plugin`, on an async bus it was an unhandled rejection. The
|
|
285
|
+
coded `unknown_route_name` error now goes to `onError` (default
|
|
286
|
+
`console.error`) and the command's result passes through. The plugin declares
|
|
287
|
+
`id: 'revalidateRoutes'`.
|
|
288
|
+
|
|
289
|
+
### Changed (breaking, router): a routes endpoint answers by the wire contract
|
|
290
|
+
|
|
291
|
+
`unwrapRoutesPayload` (the `{ url }` and `{ inline }` route sources) read the
|
|
292
|
+
old `{ ok, state, error }` envelope. It accepts a bare payload, `{ state }`,
|
|
293
|
+
or `{ problem }` - the last as a `routes_load_failed` carrying the problem's
|
|
294
|
+
`detail`.
|
|
295
|
+
|
|
296
|
+
### Fixed: `authGuard` refuses with a code
|
|
297
|
+
|
|
298
|
+
Its refusal was a plain `Error`, the one library refusal without a code, so
|
|
299
|
+
`retry()` re-sent an unauthenticated command and a circuit breaker counted it
|
|
300
|
+
as a failing server. It is now `authGuard:refused:action`, raised through the
|
|
301
|
+
plugin's own `fail` (`id: 'authGuard'`), and in `ERROR_CODE_REGISTRY`. A
|
|
302
|
+
protected prefix matched the exact action name twice over; once is enough.
|
|
303
|
+
|
|
304
|
+
### Fixed: a backend's failure opens the circuit breaker
|
|
305
|
+
|
|
306
|
+
`circuitBreaker()` skipped every `failed` condition to ignore a plugin that
|
|
307
|
+
threw, so a backend's 5xx (`remote:failed:<code>`) never counted and the
|
|
308
|
+
circuit never opened on the failure it exists for. It now skips only a party's
|
|
309
|
+
own throw, by the rule `retry()` already used (one internal predicate for
|
|
310
|
+
both). A plugin that throws still neither counts nor resets the run.
|
|
311
|
+
|
|
312
|
+
### Fixed: a history cleared while an undo is in flight stays cleared
|
|
313
|
+
|
|
314
|
+
If the server refused an undo that was still pending when `clear()` ran, the
|
|
315
|
+
revert pushed the command back into the undo stack of a history the user had
|
|
316
|
+
just cleared. A revert now moves a command only if it is still where the undo
|
|
317
|
+
put it. Found by the coverage pass; `tests/history-undo-lands.test.ts` (failed
|
|
318
|
+
on the old code).
|
|
319
|
+
|
|
320
|
+
### Fixed: `retry()` no longer builds an Error on every successful dispatch
|
|
321
|
+
|
|
322
|
+
It opened each dispatch with `_errResult(new Error('No attempts made'))`, a
|
|
323
|
+
stack capture on every call, success included, for a result that could never
|
|
324
|
+
be returned (at least one attempt is guaranteed). Measured: `retry` alone
|
|
325
|
+
48,857 ns per successful dispatch against 5,635 without it, 6,031 once fixed; a
|
|
326
|
+
realistic chain (validator, idempotent, serialize, circuitBreaker, retry)
|
|
327
|
+
halved, 69,001 to 34,135 ns. `tests/retry-happy-path.test.ts`.
|
|
328
|
+
|
|
329
|
+
### Fixed: `register()`'s cleanup removes only what it registered
|
|
330
|
+
|
|
331
|
+
It deleted whatever handler (and undo handler) held the action name when it
|
|
332
|
+
ran. KeepAlive with a `max` sets up the new page and THEN evicts the old one,
|
|
333
|
+
so the evicted page's cleanup deleted the handler of the page on screen:
|
|
334
|
+
`dispatch` answered `core:missing:handler`. The whitepaper's rule ("the newest
|
|
335
|
+
handler always wins regardless of teardown") was drawn from HMR, where the
|
|
336
|
+
order is the other way round. `createTestBus()` had the same cleanup and now
|
|
337
|
+
matches. `tests/register-ownership.test.ts` (5, all failed on the old code,
|
|
338
|
+
including a real Vapor KeepAlive).
|
|
339
|
+
|
|
340
|
+
### Fixed: a store held outside any scope is no longer disposed under its owner
|
|
341
|
+
|
|
342
|
+
`docs/store.md` says the last holder out disposes and an unscoped caller owns
|
|
343
|
+
`$dispose()`, but only scoped holders were counted: a module-level store was
|
|
344
|
+
disposed by the first component that joined and left, its actions answered
|
|
345
|
+
`ok: false`, and the next `useStore` built a fresh store with its state reset.
|
|
346
|
+
A call outside any scope now pins the store until its owner disposes it.
|
|
347
|
+
`tests/store-unscoped-holder.test.ts`.
|
|
348
|
+
|
|
349
|
+
### Fixed: `useCommand().register` follows KeepAlive
|
|
350
|
+
|
|
351
|
+
`useCommandHistory` and `useCommandError` paused for a KeepAlive deactivation;
|
|
352
|
+
`useCommand`, the composable that registers handlers, did not, so after A -> B
|
|
353
|
+
-> A the cached B's handler answered while A was on screen. A deactivated page
|
|
354
|
+
now releases its handlers and takes them back when activated (register()'s
|
|
355
|
+
cleanup removes only its own, above). Listeners (`on()`) are unchanged: whether
|
|
356
|
+
a cached page keeps listening is still an open decision.
|
|
357
|
+
`tests/usecommand-keepalive.test.ts`.
|
|
358
|
+
|
|
359
|
+
### Fixed: errors match the library's own catalogue
|
|
360
|
+
|
|
361
|
+
`validator()` refused with a plain `Error` (no code) although the catalogue
|
|
362
|
+
declares a validation code for per-action validation; it now refuses with
|
|
363
|
+
`validator:invalid:payload`, the message still the rule's own. One consequence: `retry()`'s
|
|
364
|
+
default rule treated the uncoded error as retryable (no status), so a refused
|
|
365
|
+
validation was retried; a coded permanent refusal is not. And three sites built
|
|
366
|
+
the throttled and aborted refusals with a severity the catalogue contradicted
|
|
367
|
+
(severity has since left the failure object; see the first entry). `tests/errors-match-catalogue.test.ts`.
|
|
368
|
+
|
|
369
|
+
### Fixed: the `Idempotency-Key` header is a Structured Field String
|
|
370
|
+
|
|
371
|
+
The draft this bridge follows makes the value a quoted, escaped String (RFC
|
|
372
|
+
9651); it was sent raw. The key is the command plus its target as JSON, so it
|
|
373
|
+
was unquoted and unescaped, and a target with any character outside Latin-1
|
|
374
|
+
(an emoji, CJK text) made a value `Headers` refuses outright: the request never
|
|
375
|
+
left. Now the key is percent-encoded (the web's own encoding, UTF-8) and
|
|
376
|
+
quoted: only unreserved ASCII and `%XX`, so an sf-string with nothing left to
|
|
377
|
+
escape, reversible, so no two keys collide. The reference controller unquotes
|
|
378
|
+
and `rawurldecode`s it back to the exact key, the same one the batching bridge
|
|
379
|
+
sends in the body, and still accepts a raw key from an older client. Two
|
|
380
|
+
encodings were measured and dropped first: hand escaping plus `%uXXXX` (+63 B
|
|
381
|
+
brotli on the Blade consumer bundle), and the same in one regex pass (+87).
|
|
382
|
+
**Upgrade order: the backend first**, or a quoted key misses a cache written
|
|
383
|
+
under the raw one for up to the replay window. `tests/idempotency-header.test.ts`;
|
|
384
|
+
`docs/integrations/laravel.md` updated, and its "line for line" snippet
|
|
385
|
+
corrected where it had drifted from the controller (`fail` -> `problem`, the
|
|
386
|
+
redirect-aware success body).
|
|
387
|
+
|
|
388
|
+
### Changed: error advice is DEV-only; one throttle gate
|
|
389
|
+
|
|
390
|
+
Production error messages state the fact; the fix sentence is DEV's, and the
|
|
391
|
+
catalogue (`ERROR_CODE_REGISTRY`, each code's `fix`) is where a production
|
|
392
|
+
reader finds it (docs/plan-failures-and-contract.md, settled item 5). Touched:
|
|
393
|
+
`core:refused:bus`, `core:exceeded:depth`, `core:timeout:request`,
|
|
394
|
+
`core:missing:handler` (production is `No handler registered for "x".`), the
|
|
395
|
+
naming-pattern violation, `createVaporChamberApp`'s VDOM tail and `persist`'s
|
|
396
|
+
"stale after a deploy". Match on `.code`, never on `.message`; development
|
|
397
|
+
builds read as before. `vueDetectionHint` still ships: it is a diagnosis, and a
|
|
398
|
+
`<script>`-tag page only ever runs a production IIFE.
|
|
399
|
+
|
|
400
|
+
`register(action, h, { throttle })` and the `throttle()` plugin were the same
|
|
401
|
+
gate written twice; both use one now, so the plugin also skips the stack
|
|
402
|
+
capture on its rejected path, the hot one. The refusal reads `"x" throttled.
|
|
403
|
+
Retry in Nms.` from either (it said `Handler "x"` or `Action "x"`).
|
|
404
|
+
|
|
405
|
+
These paid for this release's fixes without a raise (measured before the
|
|
406
|
+
failure shape and the IIFE target in the first two entries): all three IIFE
|
|
407
|
+
budgets went DOWN (full 41,172 / 12,303, core 27,164 / 7,992, elements 28,928 / 8,499
|
|
408
|
+
raw / brotli) and the Blade consumer bundle measures 6,260 brotli, below
|
|
409
|
+
v1.24.0's 6,363; its ceiling locks at 6,280.
|
|
410
|
+
|
|
411
|
+
### Added: `announce()` - status messages for assistive technology; `vc-error` made semantic
|
|
412
|
+
|
|
413
|
+
`announce(message, { assertive })` says something to a screen reader without
|
|
414
|
+
moving focus (WCAG 4.1.3), through ONE shared pair of live regions per document
|
|
415
|
+
(polite, assertive), created on first use: the pattern of Angular CDK's
|
|
416
|
+
LiveAnnouncer and React Aria's `announce()`. A repeated message is heard again.
|
|
417
|
+
`setAnnouncer(fn)` lets an app take the announcing over (its own status bar, its
|
|
418
|
+
own words); `setAnnouncer(null)` hands it back. Exported from the root.
|
|
419
|
+
|
|
420
|
+
Two features now speak through it instead of on their own:
|
|
421
|
+
|
|
422
|
+
- **`v-vc-command` failures.** `vc-error` stays for styling, but a class is
|
|
423
|
+
invisible to a screen reader. A failed dispatch now also announces the
|
|
424
|
+
failure's message assertively, focus left where it is. The words are the
|
|
425
|
+
failure's own: since 1.24 a backend problem's `detail`, already localized.
|
|
426
|
+
An app that wants other words uses `setAnnouncer`.
|
|
427
|
+
- **The router's page changes** use the shared assertive region; the router no
|
|
428
|
+
longer creates a region of its own.
|
|
429
|
+
|
|
430
|
+
`tests/a11y-announce.test.ts` (7), `tests/browser/command-errors.browser.test.ts`
|
|
431
|
+
(3; 2 failed on the old code). Size: `./directives` +0.2 KB brotli, `./router`
|
|
432
|
+
unchanged, core unchanged, IIFEs unchanged.
|
|
433
|
+
|
|
434
|
+
### Fixed: an undo the server refuses no longer reads as "undone"
|
|
435
|
+
|
|
436
|
+
`useCommandHistory()` and the `history()` plugin moved their stacks before an
|
|
437
|
+
undo or redo and never looked at the outcome. On an async bus, an undo the
|
|
438
|
+
server refused left the history saying "undone": the command was no longer
|
|
439
|
+
undoable, and a redo was offered for a change that never happened. For a
|
|
440
|
+
screen-reader user, who cannot see that nothing changed, that is
|
|
441
|
+
indistinguishable from success (WCAG 3.3.4).
|
|
442
|
+
|
|
443
|
+
Now the stacks still move first, so anything observing the call sees what it
|
|
444
|
+
always saw, and move BACK when the undo or redo does not land: a throw, a
|
|
445
|
+
rejection, or a failed `CommandResult`, returned or resolved. While one is in
|
|
446
|
+
flight, another press does nothing. The API is unchanged: `undo()` and
|
|
447
|
+
`redo()` return the command at once, and a synchronous local undo behaves
|
|
448
|
+
exactly as before. `tests/history-undo-lands.test.ts` (8; 6 failed on the old
|
|
449
|
+
code). A first cut that moved the stacks only AFTER the call broke the pinned
|
|
450
|
+
island-cart wiring (a listener on a redo's re-dispatch reads the redone state),
|
|
451
|
+
which is why the order is move-then-revert.
|
|
452
|
+
|
|
453
|
+
**One implementation.** The two histories each carried their own stacks,
|
|
454
|
+
recording rule, undo and redo, so this defect existed twice. They now wrap one
|
|
455
|
+
internal ledger (`src/ledger.ts`) and differ only where they genuinely differ:
|
|
456
|
+
where a command is recorded, what else is skipped, how the stacks are exposed.
|
|
457
|
+
The `history()` plugin's `bus` option is now typed `CommandBus |
|
|
458
|
+
AsyncCommandBus`, what it already worked with at runtime.
|
|
459
|
+
|
|
460
|
+
**Size:** full IIFE +787 raw / +278 brotli (budget raised, measured steps in
|
|
461
|
+
`scripts/check-size.mjs`); core and elements IIFEs unchanged; ESM import
|
|
462
|
+
everything 24.9 KB brotli either way. `tests/history-redo-ab.test.ts` now derives
|
|
463
|
+
a ledger per arm, so its reverted baseline still reaches the code it times.
|
|
464
|
+
|
|
465
|
+
### Added: FormBus takes the server's errors, and says what an element needs to make them accessible
|
|
466
|
+
|
|
467
|
+
Most real form errors come from the server, not from client rules, and a form
|
|
468
|
+
had no way to show them: `onSubmit` throwing a 422 made `submit()` reject, and
|
|
469
|
+
its field errors were lost. Now:
|
|
470
|
+
|
|
471
|
+
- `form.setErrors(errors)` takes an RFC 9457 problem's `errors` list
|
|
472
|
+
(`{ pointer, detail }`; `/email`, `#/payload/email` and `/address/city` all
|
|
473
|
+
resolve to their field) or a Laravel field map (`{ email: [...] }`). Each
|
|
474
|
+
error shows at once and clears when its own field changes; rule errors keep
|
|
475
|
+
working; `reset()` clears them; `submit()` clears them before validating,
|
|
476
|
+
since the server decides again.
|
|
477
|
+
- `submit()` lands a field-carrying failure itself: when `onSubmit` throws an
|
|
478
|
+
error whose problem has `errors`, the fields are marked and it resolves
|
|
479
|
+
`false`. Any other failure still rejects, as before.
|
|
480
|
+
- `form.aria(field)` gives `aria-invalid` and `aria-describedby`, set only while
|
|
481
|
+
the error is shown; `form.errorId(field)` is the message element's id, unique
|
|
482
|
+
per form (two forms on a page cannot produce duplicate ids);
|
|
483
|
+
`form.firstInvalid()` names the field to focus after a failed submit.
|
|
484
|
+
|
|
485
|
+
The messages are the backend's, already localized; the form holds no strings.
|
|
486
|
+
`tests/form-a11y.test.ts` (11; 10 failed on the old code), and in real Chromium
|
|
487
|
+
`tests/browser/form-errors.browser.test.ts`: Playwright's role engine reads the
|
|
488
|
+
field as invalid, described by the server's message.
|
|
489
|
+
|
|
490
|
+
### Fixed: a `role="button"` element with `v-vc-command` works from the keyboard
|
|
491
|
+
|
|
492
|
+
A `<div role="button">` carrying the directive answered pointer clicks only:
|
|
493
|
+
not focusable, and Enter and Space did nothing (WCAG 2.1.1). A native
|
|
494
|
+
`<button>` gets both from the platform; a role does not. The directive now gives
|
|
495
|
+
a non-native `role="button"` element what the ARIA Authoring Practices ask of a
|
|
496
|
+
button: `tabindex="0"` when it has none (removed again at teardown; a tabindex
|
|
497
|
+
the app set is kept), Enter activates on key down, Space on key up with its page
|
|
498
|
+
scroll prevented. Activation is `el.click()`, so it takes the same path as a
|
|
499
|
+
pointer press. A key the app already handled (`defaultPrevented`) is left
|
|
500
|
+
alone, and a native button still dispatches once per key.
|
|
501
|
+
`tests/browser/command-keyboard.browser.test.ts` (6, real key presses through
|
|
502
|
+
Playwright; 3 failed on the old code).
|
|
503
|
+
|
|
504
|
+
### Added: route changes reach assistive technology
|
|
505
|
+
|
|
506
|
+
A full page load announces the new page to a screen reader and restarts focus;
|
|
507
|
+
a client-side navigation did neither. The router now announces each one in an
|
|
508
|
+
assertive live region (`document.title`, else the first `<h1>`, else the path),
|
|
509
|
+
skipping the initial load and query-only changes, and removes the region on
|
|
510
|
+
`destroy()`. New options: `announce` (default `true`; `false`, or a function
|
|
511
|
+
returning the text) and `focusOnNavigate` (a selector for a small element the
|
|
512
|
+
app provides, focused after each navigation; made focusable only if it is not).
|
|
513
|
+
Rules from Next.js's route announcer and Gatsby's user testing with disabled
|
|
514
|
+
users. `tests/router/announce.test.ts` (7), and in real Chromium
|
|
515
|
+
`tests/browser/route-announce.browser.test.ts`.
|
|
516
|
+
|
|
517
|
+
**Behaviour change:** after the first navigation, the document gains the shared
|
|
518
|
+
visually hidden live regions (`<div data-vc-announcer="assertive">`, see "Added:
|
|
519
|
+
`announce()`" below); the router no longer creates a region of its own. Pass
|
|
520
|
+
`announce: false` to opt out.
|
|
521
|
+
|
|
522
|
+
### Added: `aria-current="page"` on the current link
|
|
523
|
+
|
|
524
|
+
`stampActiveLinks` now sets `aria-current="page"` on the exactly-active anchor
|
|
525
|
+
and removes it when the route moves on; an `aria-current` the page set for
|
|
526
|
+
another purpose (`"step"`) is left alone. Section links that are only a prefix
|
|
527
|
+
match keep `data-active` and are not marked, since ARIA asks for one current
|
|
528
|
+
element per set. `MenuItem.exactActive` and `Breadcrumb.current` now say they
|
|
529
|
+
are the fields to bind `aria-current` to; `MenuItem.active` says it is not.
|
|
530
|
+
|
|
531
|
+
### Fixed: `v-vc-command` no longer takes keyboard focus away
|
|
532
|
+
|
|
533
|
+
While a dispatch was in flight the directive set `disabled` on its button.
|
|
534
|
+
HTML's focus fixup rule answers that by moving focus to `<body>`, so a keyboard
|
|
535
|
+
user who pressed Enter on a command button was sent back to the top of the page
|
|
536
|
+
on every dispatch, and focus did not return when the button was re-enabled.
|
|
537
|
+
Measured in real Chromium, production build, with the new browser runner
|
|
538
|
+
(`npm run test:browser`, `tests/browser/command-focus.browser.test.ts`): during
|
|
539
|
+
the dispatch `{ disabled: true, focused: false }`, after it
|
|
540
|
+
`{ disabled: false, focused: false }`. A zero-delay timeout read the opposite,
|
|
541
|
+
because the rule runs during the next rendering update.
|
|
542
|
+
|
|
543
|
+
Now a button in flight (a `<button>`, or any element with `role="button"`) gets
|
|
544
|
+
`aria-disabled="true"`, restored to its previous value when it lands. A
|
|
545
|
+
`role="button"` element used to get nothing at all in flight. Links are left as
|
|
546
|
+
they were: their press is navigation. Not `aria-busy`: it tells assistive technology to hold off
|
|
547
|
+
on the element's content, and a button's content is its accessible name. What `disabled` also did is
|
|
548
|
+
kept by hand and pinned by the same file, each measured on the old code first:
|
|
549
|
+
|
|
550
|
+
- the press that starts the dispatch does not submit a form (`preventDefault`);
|
|
551
|
+
- no further press reaches any listener, the app's own included, until the
|
|
552
|
+
dispatch lands (a capture listener, attached only while in flight, so
|
|
553
|
+
`.delegate` still attaches nothing per element at rest);
|
|
554
|
+
- the button works again once the dispatch lands.
|
|
555
|
+
|
|
556
|
+
**Behaviour changes to check:**
|
|
557
|
+
|
|
558
|
+
- CSS keyed on `:disabled` to show a button working stops matching during a
|
|
559
|
+
dispatch. Use `.vc-loading` (unchanged). Announcing progress in words is the
|
|
560
|
+
app's (a live region, in its own language).
|
|
561
|
+
- A `disabled` the app sets is no longer cleared when a dispatch lands: the old
|
|
562
|
+
`finally` set `disabled = false` whatever the app had set.
|
|
563
|
+
- Do not bind `:aria-disabled` on the element; `:disabled` is
|
|
564
|
+
now safe to bind (whitepaper, "A bound update overwrites what `v-vc-command`
|
|
565
|
+
writes by hand").
|
|
566
|
+
|
|
567
|
+
`tests/directives-vapor-fixture.test.ts`'s identity test now also asserts the
|
|
568
|
+
in-flight guard comes off after every dispatch.
|
|
569
|
+
|
|
570
|
+
## v1.24.0 - 2026-09-24
|
|
571
|
+
|
|
572
|
+
### Added: RFC 9457 problem documents are read on every path
|
|
573
|
+
|
|
574
|
+
A backend that answers a failure as `application/problem+json` -
|
|
575
|
+
`{ type, title, status, detail, code }`, `code` as an extension member - lost it
|
|
576
|
+
three ways. Measured with only `fetch` stubbed, then against the reference
|
|
577
|
+
controller running under Laravel 13:
|
|
578
|
+
|
|
579
|
+
- `createHttpClient` parsed a body only when its content type contained
|
|
580
|
+
`application/json`, which `application/problem+json` does not. The problem
|
|
581
|
+
stayed a string, so `HttpError.code` read `undefined` - on reads, in
|
|
582
|
+
`router-fetch`, and in any bridge handed an `httpClient`. `postCommand` was
|
|
583
|
+
never affected: its fetch calls `raw.json()` whatever the type.
|
|
584
|
+
- Every bridge took its message from `error ?? message`, never `detail`, so the
|
|
585
|
+
backend's sentence arrived as `HTTP 404`.
|
|
586
|
+
- A batched result, or a WebSocket frame, carrying `problem` resolved as a
|
|
587
|
+
SUCCESS whose value was `undefined`.
|
|
588
|
+
|
|
589
|
+
Now:
|
|
590
|
+
|
|
591
|
+
- The client parses `application/json` and any `+json` structured suffix
|
|
592
|
+
(RFC 6839), parameters allowed. A type that only contains "json"
|
|
593
|
+
(`application/json-seq`) stays text - it used to be `JSON.parse`d, which
|
|
594
|
+
throws on a real JSON text sequence.
|
|
595
|
+
- `Accept` is `application/json, application/problem+json`.
|
|
596
|
+
- A problem's `detail` is the error's message, read once, in `HttpError`
|
|
597
|
+
itself - so the client, `router-fetch` and every bridge agree. A body without
|
|
598
|
+
`detail` keeps `HTTP <status>`, and a body with the older `error`/`message`
|
|
599
|
+
keeps what the bridges read from it.
|
|
600
|
+
- A result `{ id, problem }` is a failure, batched or over WebSocket: message
|
|
601
|
+
from `detail`, then `title`; code from `problem.code`, then the result's
|
|
602
|
+
`code`. Both paths now share one reading, `resultFailure()` in
|
|
603
|
+
`transports.ts`, where each had its own `ok === false` branch. The single
|
|
604
|
+
bridge keeps its own: there a problem is the error response and arrives as
|
|
605
|
+
an HttpError, and routing it through the helper cost core +128 B raw for a
|
|
606
|
+
shape its 2xx never carries.
|
|
607
|
+
- `ProblemDetails` is exported, and `BackendResponse` gains `detail` and
|
|
608
|
+
`problem`.
|
|
609
|
+
|
|
610
|
+
**What is deliberately not read.** `type` is an opaque URI: nothing derives a
|
|
611
|
+
code from it, so a backend sends `code` as an extension member. The `status`
|
|
612
|
+
inside a batched problem is data, not the response's status, and changes no
|
|
613
|
+
retry decision.
|
|
614
|
+
|
|
615
|
+
**The reference controller** (`examples/laravel-backend/VaporChamberController.php`)
|
|
616
|
+
now answers every failure as `application/problem+json`, with an absolute `type`
|
|
617
|
+
(`url('/problems/<code>')`), the status's reason phrase as `title`, and `code`.
|
|
618
|
+
On `batch()` a failed command's problem rides on its own result inside the 200,
|
|
619
|
+
beside `ok: false`. An exception's own `render()` contributes its `code` member
|
|
620
|
+
when it has one, the last segment of its `type` otherwise.
|
|
621
|
+
|
|
622
|
+
**Upgrade order: the client first.** A v1.23.0 client against the new
|
|
623
|
+
controller still sees every failure as a failure - `ok: false` beside `problem`
|
|
624
|
+
is there for exactly that - but reads `HTTP 422` and `Backend error` for
|
|
625
|
+
messages, and loses the code on a batched result. The previous
|
|
626
|
+
`{ ok: false, error, code }` shape is still read everywhere, so a backend can
|
|
627
|
+
move one endpoint at a time.
|
|
628
|
+
|
|
629
|
+
**Examples:** the two mock backends (`examples/static-server.mjs`,
|
|
630
|
+
`examples/sprinkled-blade/mock-server.mjs`) answer failures as problems, and
|
|
631
|
+
their 500 no longer echoes the exception's text to the client - it is logged.
|
|
632
|
+
|
|
633
|
+
**Fixture:** `tests/problem-details.test.ts` - 17 cases across the client,
|
|
634
|
+
`router-fetch`, `createHttpBridge` (through `postCommand` and through an
|
|
635
|
+
`httpClient`), `createBatchingHttpBridge` and `createWsBridge`, each with
|
|
636
|
+
today's shape as the control. 13 fail against v1.23.0; the 4 controls pass on
|
|
637
|
+
both. The stubbed backend it answers with - `reply`, `problemReply`,
|
|
638
|
+
`batchServer`, `singleServer`, `MockWebSocket` - is now one shared file,
|
|
639
|
+
`tests/backend-stubs.ts`, and `batch-redirect`, `outbox-refusal` and
|
|
640
|
+
`transports` use it instead of their own copies.
|
|
641
|
+
|
|
642
|
+
**Size:** +136 B raw / +62 B brotli on the full IIFE, which carries the batch
|
|
643
|
+
and WebSocket bridges; +21 B raw on core and elements, +3 / -1 B brotli; +7 B
|
|
644
|
+
brotli on the Blade consumer ESM bundle, under its unchanged ceiling. Squeezed
|
|
645
|
+
before landing - how is in `scripts/check-size.mjs`.
|
|
646
|
+
|
|
647
|
+
### Fixed: the last uncovered branch, back to 100% on all four axes
|
|
648
|
+
|
|
649
|
+
`runFlush` falls back to `new Error('Unknown error')` when a replay fails with
|
|
650
|
+
no `error` - the shape a hand-written plugin can return - and no test reached
|
|
651
|
+
it since v1.23.0 added `isRetryable`. `tests/outbox-edges.test.ts` now does,
|
|
652
|
+
and pins that the predicate sees that Error rather than `undefined`.
|
|
653
|
+
|
|
5
654
|
## v1.23.0 - 2026-09-23
|
|
6
655
|
|
|
7
656
|
Post-v1.22.0 work. The first four entries were written into the v1.22.0 section
|