vapor-chamber 1.22.0 → 1.23.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 +577 -0
- package/README.md +13 -13
- package/ROADMAP.md +39 -19
- package/dist/LICENSE.txt +3 -2
- package/dist/chamber-vapor.d.ts +1 -1
- package/dist/chamber-vapor.js.map +1 -1
- package/dist/command-bus.d.ts +24 -2
- package/dist/command-bus.d.ts.map +1 -1
- package/dist/command-bus.js +1 -1
- package/dist/command-bus.js.map +1 -1
- package/dist/directives.d.ts +1 -1
- package/dist/directives.d.ts.map +1 -1
- package/dist/directives2.js.map +1 -1
- package/dist/glyphs.d.ts +7 -3
- package/dist/glyphs.d.ts.map +1 -1
- package/dist/http-errors.js +11 -0
- package/dist/http-errors.js.map +1 -0
- package/dist/http.d.ts +57 -3
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +18 -24
- package/dist/http.js.map +1 -1
- package/dist/iife-core.d.ts +1 -1
- package/dist/iife-core.js +1 -1
- package/dist/iife-core.js.map +1 -1
- package/dist/iife-elements.d.ts +10 -2
- 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.d.ts +2 -2
- package/dist/iife.d.ts.map +1 -1
- package/dist/iife.js +2 -2
- package/dist/iife.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +52 -6
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +8 -8
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +2 -2
- package/dist/mcp.js.map +1 -1
- package/dist/observable.d.ts +1 -1
- package/dist/observable.js.map +1 -1
- package/dist/outbox.d.ts +39 -9
- package/dist/outbox.d.ts.map +1 -1
- package/dist/outbox.js +29 -6
- package/dist/outbox.js.map +1 -1
- package/dist/plugins-io.d.ts +19 -10
- package/dist/plugins-io.d.ts.map +1 -1
- package/dist/plugins-io.js +5 -6
- package/dist/plugins-io.js.map +1 -1
- package/dist/plugins.d.ts +2 -2
- package/dist/plugins.d.ts.map +1 -1
- package/dist/router/remote.js +1 -1
- package/dist/router-fetch/index.js +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/ssr.d.ts +1 -1
- 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.map +1 -1
- package/dist/transitions.d.ts +1 -1
- package/dist/transitions2.js.map +1 -1
- package/dist/transports.d.ts +15 -0
- package/dist/transports.d.ts.map +1 -1
- package/dist/transports.js +51 -33
- package/dist/transports.js.map +1 -1
- package/dist/vapor-chamber-core.iife.js +224 -42
- 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 +233 -43
- 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 +236 -50
- package/dist/vapor-chamber.iife.js.map +1 -1
- package/dist/vapor-chamber.iife.min.js +1 -1
- package/dist/vitest-pure.d.ts +10 -0
- package/dist/vitest-pure.d.ts.map +1 -1
- package/dist/vitest-pure.js +10 -0
- package/dist/vitest-pure.js.map +1 -1
- package/package.json +2 -2
- package/scripts/check-ascii.mjs +19 -1
- package/scripts/check-size.mjs +117 -4
- package/scripts/gate.mjs +1 -1
- package/scripts/generate-api-docs.mjs +35 -7
- package/scripts/stamp-docs.mjs +14 -6
- package/src/chamber-vapor.ts +1 -1
- package/src/command-bus.ts +48 -4
- package/src/directives.ts +18 -2
- package/src/glyphs.ts +7 -3
- package/src/http.ts +85 -22
- package/src/iife-core.ts +1 -1
- package/src/iife-elements.ts +10 -2
- package/src/iife.ts +2 -2
- package/src/index.ts +3 -3
- package/src/mcp.ts +8 -8
- package/src/observable.ts +1 -1
- package/src/outbox.ts +73 -14
- package/src/plugins-io.ts +65 -22
- package/src/plugins.ts +3 -3
- package/src/schema.ts +11 -1
- package/src/ssr.ts +1 -1
- package/src/store.ts +5 -3
- package/src/transitions.ts +1 -1
- package/src/transports.ts +145 -35
- package/src/vitest-pure.ts +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,583 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## v1.23.0 - 2026-09-23
|
|
6
|
+
|
|
7
|
+
Post-v1.22.0 work. The first four entries were written into the v1.22.0 section
|
|
8
|
+
while it was still open and are moved here unchanged - v1.22.0 shipped on
|
|
9
|
+
2026-09-21 and its text is frozen at what was published.
|
|
10
|
+
|
|
11
|
+
### Fixed: the backend's `code` survives an envelope failure
|
|
12
|
+
|
|
13
|
+
Both HTTP bridges enriched an error when the failure THREW and built a bare one
|
|
14
|
+
when the same failure arrived as DATA, twelve lines apart in the same function.
|
|
15
|
+
`postCommand` throws an `HttpError` on a non-2xx with the parsed body attached,
|
|
16
|
+
and the catch path has copied `status`, `code` and `response` off it since
|
|
17
|
+
v1.20.0. A 200 carrying `{ ok: false, code }` throws nothing, so the envelope
|
|
18
|
+
path built `new Error(body.error)` and the code was read by nobody.
|
|
19
|
+
|
|
20
|
+
Not a missing feature. `examples/laravel-backend/VaporChamberController.php`
|
|
21
|
+
sends the field and its `fail()` docblock promises it arrives as
|
|
22
|
+
`HttpError.code` - a promise that was false twice over on the batch path, since
|
|
23
|
+
`batch()` answers 200 with per-command status in the body, so nothing throws and
|
|
24
|
+
no `HttpError` is ever built. That docblock was corrected in `df6923d`; this is
|
|
25
|
+
the code half.
|
|
26
|
+
|
|
27
|
+
Four sites now carry it, through one small builder: the HTTP envelope
|
|
28
|
+
(`ok: false`), the non-2xx-that-did-not-throw branch, the batching bridge's
|
|
29
|
+
per-result branch, and the WebSocket frame. `BackendResponse` gains `code?`, and
|
|
30
|
+
because the WS frame is parsed as `{ id: string } & BackendResponse` that one
|
|
31
|
+
type covers all four. `BatchedResult` stops being a hand-written restatement of
|
|
32
|
+
`{ id } & BackendResponse` and composes it instead - the spelling the WS site
|
|
33
|
+
already used, and the reason `code` would otherwise have needed adding twice.
|
|
34
|
+
|
|
35
|
+
**What this does NOT fix**, stated plainly so nobody reads it as closed: the
|
|
36
|
+
retry hole. `defaultIsRetryable` reads `e.status ?? e.response?.status`, an
|
|
37
|
+
envelope error carries neither, so it still returns true and a batched 422 is
|
|
38
|
+
still re-sent. Closing that needs the `status` contract, which changes a
|
|
39
|
+
documented shape and is its own step with its own evidence.
|
|
40
|
+
|
|
41
|
+
**Cost, and the squeeze, MEASURED per piece:**
|
|
42
|
+
|
|
43
|
+
the fix (4 sites + one builder) raw +64 / +61 / +61 brotli +2 / +13 / +14
|
|
44
|
+
sharing the two catch blocks raw +30 / +33 / +33 NOT TAKEN
|
|
45
|
+
|
|
46
|
+
Two squeezes were attempted and both are in those numbers. Assigning `code`
|
|
47
|
+
unconditionally rather than behind a `!== undefined` guard is **-14 B raw** and
|
|
48
|
+
is also the shape-stable form `okResult` / `errResult` already use, so every
|
|
49
|
+
error the builder returns leaves with one hidden class.
|
|
50
|
+
|
|
51
|
+
The second is the interesting one. The plan for this change argued that
|
|
52
|
+
extracting the two byte-identical catch blocks into one helper would pay for the
|
|
53
|
+
fix, on the grounds that `transports.ts`'s note declining exactly that
|
|
54
|
+
("inline rather than shared with createHttpBridge, whose bundle has two bytes of
|
|
55
|
+
headroom") had expired now that the bundle has 115. Measured: the extraction
|
|
56
|
+
**costs 30 B raw** rather than saving. The abstraction is more expensive than the
|
|
57
|
+
duplication, and the headroom was never what made it so. The extraction was
|
|
58
|
+
reverted, and that note now carries the number under a `MEASURED:` prefix so the
|
|
59
|
+
next reader does not re-derive it - which cost four builds here.
|
|
60
|
+
|
|
61
|
+
Budgets raised by the remainder, which is the feature itself, with the per-piece
|
|
62
|
+
table in `scripts/check-size.mjs`.
|
|
63
|
+
|
|
64
|
+
**Fixture:** `tests/envelope-code.test.ts`, real bridges with only `fetch`
|
|
65
|
+
stubbed, verified to FAIL against the pre-fix module on both defect cases while
|
|
66
|
+
the controls pass. It carries two controls - the throw path still delivering
|
|
67
|
+
`code`, and a 5xx delivering code and status - because a negative result is only
|
|
68
|
+
evidence if the same probe fires where it must. One test was relabelled after
|
|
69
|
+
the arming run: it was written to cover the `!res.ok` branch and turned out to
|
|
70
|
+
drive the catch path, since a 503 throws. It stays as a control and says so,
|
|
71
|
+
rather than claiming coverage it does not have. And `error.status` is
|
|
72
|
+
deliberately NOT asserted on the envelope cases: on that path the response
|
|
73
|
+
really is 200, so `status` is not a field being dropped but one that does not
|
|
74
|
+
exist.
|
|
75
|
+
|
|
76
|
+
### Fixed: `.code` on a transport error had two owners
|
|
77
|
+
|
|
78
|
+
`defaultIsRetryable` read a `VC_` prefix on `.code` as proof the library minted
|
|
79
|
+
it, and its docblock said so: "a backend code is not ours to interpret, while a
|
|
80
|
+
`VC_` code is." The bridges write the BACKEND's body code into that same field,
|
|
81
|
+
so the claim was false and a backend chose which branch of the predicate ran.
|
|
82
|
+
Two directions, both counted in `tests/transport-code-owner.test.ts`:
|
|
83
|
+
|
|
84
|
+
code: 'VC_CORE_THROTTLED' on a 422 -> re-sent 3x (the HTTP layer had refused to re-send it once)
|
|
85
|
+
code: 'VC_VALIDATION_FAILED' on a 503 -> sent 1x (a retryable status, suppressed from the wire)
|
|
86
|
+
|
|
87
|
+
No hostile backend is needed. `VC_` is a convention nobody polices, and it has
|
|
88
|
+
three claimants inside this repo already: `BusError`, `VcTestError` in
|
|
89
|
+
`src/vitest-pure.ts` (a `VC_TEST_*` code family with no emitter), and whatever a
|
|
90
|
+
body carries.
|
|
91
|
+
|
|
92
|
+
**`.code` does not move, and that is the decision.** It stays the backend's on a
|
|
93
|
+
transport error. The whitepaper documents it (section 5.7), the example
|
|
94
|
+
controller's `fail()` promises it, and the catch path has delivered it since
|
|
95
|
+
v1.20.0 - a released contract. What moves is the QUESTION: provenance now comes
|
|
96
|
+
from `emitter`, which `BusError`'s constructor has always set and which no
|
|
97
|
+
transport ever copies a body field into. A field the backend can write cannot
|
|
98
|
+
answer "did the library write this".
|
|
99
|
+
|
|
100
|
+
- if (typeof code === 'string' && code.startsWith('VC_')) return RETRYABLE_CODES.has(code);
|
|
101
|
+
+ if (e.emitter !== undefined) return RETRYABLE_CODES.has(e.code as string);
|
|
102
|
+
|
|
103
|
+
That is the whole provenance fix and it is **smaller**: -25 B raw and -8 / -2 / -5
|
|
104
|
+
brotli. `emitter` is on every `BusError` because the constructor defaults it to
|
|
105
|
+
`'core'`, so nothing had to be added to make it reliable.
|
|
106
|
+
|
|
107
|
+
**The other half: seven library-minted transport failures carried no code at
|
|
108
|
+
all.** An unhandled backend redirect, a batch result that never came back, a full
|
|
109
|
+
offline queue, a queued message that expired, a dead socket, a WS request
|
|
110
|
+
timeout. Each was indistinguishable from an unclassified error, so each was
|
|
111
|
+
retried to exhaustion - and for the redirect the retry cannot change the answer,
|
|
112
|
+
it just issues `maxAttempts` requests to a backend that will redirect every time.
|
|
113
|
+
|
|
114
|
+
They are `BusError`s now, through one `transportError` builder, with
|
|
115
|
+
`emitter: 'transport'` - a `BusEmitter` member declared since v1.0 and never
|
|
116
|
+
set by anything until now. Five new codes: `VC_TRANSPORT_REDIRECT`,
|
|
117
|
+
`VC_TRANSPORT_PROTOCOL`, `VC_TRANSPORT_QUEUE_FULL`, `VC_TRANSPORT_CLOSED` and
|
|
118
|
+
`VC_TRANSPORT_TIMEOUT`. The last one replaced reusing `VC_CORE_REQUEST_TIMEOUT`
|
|
119
|
+
at the two WS timeout sites: that row says emitter `core` and "request() timed
|
|
120
|
+
out waiting for a response", and from a transport both are false. A registry row
|
|
121
|
+
costs nothing in these bundles, so the reuse traded two true rows for one
|
|
122
|
+
half-true one. Each code carries `action` and a `context` holding the
|
|
123
|
+
value a reader would inspect rather than read - the dropped envelope, the
|
|
124
|
+
unmatched batch id, the redirect URL - so it travels beside the sentence instead
|
|
125
|
+
of inside it, the rule `scripts/check-console-shape.mjs` enforces for console
|
|
126
|
+
arguments.
|
|
127
|
+
|
|
128
|
+
**The retry hole IS closed, and the previous entry predicted otherwise.** It said
|
|
129
|
+
closing it "needs the `status` contract, which changes a documented shape". It
|
|
130
|
+
does not. A refusal that arrives as `{ ok: false }` inside a 2xx means the server
|
|
131
|
+
received, processed and refused, so it is not a transient transport failure and
|
|
132
|
+
the transport is the layer that knows this. It says so by tagging the refusal
|
|
133
|
+
`emitter: 'transport'`, and then an EXISTING rule answers: a transport error is
|
|
134
|
+
retried only when its code is one of RETRYABLE_CODES' six, and a backend code
|
|
135
|
+
is not one of those. No status is stamped on a response that carried 200, no
|
|
136
|
+
`response` is attached to make a predicate find one, and the "no status means
|
|
137
|
+
retry" fallback is untouched for every non-HTTP consumer that depends on it. The
|
|
138
|
+
three rejected approaches stay rejected; this is a fourth.
|
|
139
|
+
|
|
140
|
+
**No new field, and that is the second decision.** This shipped for one commit
|
|
141
|
+
as a `retryable: false` boolean on the error, which worked. It went for two
|
|
142
|
+
reasons. It measured 19 B raw / 17 B brotli DEARER on the full IIFE (22/6 and
|
|
143
|
+
22/4 on core and elements) than letting `emitter` carry it. And it was a
|
|
144
|
+
string-keyed field on an error object built from a response body, which the
|
|
145
|
+
predicate then obeyed unconditionally - the exact precondition that broke
|
|
146
|
+
`.code`, which became a wire field additively over two releases. `emitter` is
|
|
147
|
+
not a plausible wire field: a backend has no reason to claim to be a subsystem
|
|
148
|
+
of this library, and it is written from string literals at every site in `src/`.
|
|
149
|
+
`src/transports.ts` carries the do-not.
|
|
150
|
+
|
|
151
|
+
**The residue, stated and pinned.** A 2xx refusal carries `emitter: 'transport'`
|
|
152
|
+
while its `.code` is the BACKEND's, so a backend sending one of RETRYABLE_CODES'
|
|
153
|
+
six exact strings gets that refusal retried. Bounded and one-directional, where
|
|
154
|
+
the `VC_` prefix was unbounded and went both ways - and before this every 2xx
|
|
155
|
+
refusal was retried, so it is strictly smaller than what it replaces. It has its
|
|
156
|
+
own tests rather than a comment, and they enumerate the colliding strings FROM
|
|
157
|
+
`RETRYABLE_CODES` so the size cannot drift from the set. Closing it completely is
|
|
158
|
+
what the boolean bought, and it was not worth the shape.
|
|
159
|
+
|
|
160
|
+
**Six, not seven, and that is the set getting smaller rather than the rule
|
|
161
|
+
getting cleverer.** `VC_CORE_HANDLER_THREW` was in `RETRYABLE_CODES` while being
|
|
162
|
+
minted by no site in `src/` - a dead code is still a live collision surface, and
|
|
163
|
+
that one could only ever have matched a backend's string or a code a consumer
|
|
164
|
+
built by hand. Removed with nothing able to regress, because nothing emitted it -
|
|
165
|
+
and it PAYS rather than costs: MEASURED -24 B raw in all three IIFEs and -2 / -8 /
|
|
166
|
+
-7 brotli, a set member being a string plus a comma. (An earlier draft of this
|
|
167
|
+
entry said "at zero bytes", which was a guess, and wrong in the flattering
|
|
168
|
+
direction.) Priced: a consumer throwing a `BusError` with that code from
|
|
169
|
+
a handler is no longer retried by default and must pass its own predicate, which
|
|
170
|
+
is what the corrected registry row now says. `RETRYABLE_CODES`' own docblock
|
|
171
|
+
records that adding a member widens the wire-facing surface.
|
|
172
|
+
|
|
173
|
+
**Cost, MEASURED per piece,** one build each, raw and brotli, full / core /
|
|
174
|
+
elements. The full table is in `scripts/check-size.mjs`:
|
|
175
|
+
|
|
176
|
+
1 provenance from `emitter` raw -25 / -25 / -25 brotli -8 / -2 / -5
|
|
177
|
+
2 the 7 codeless sites, 5 new codes raw +174 / +89 / +89 brotli +54 / +35 / +42
|
|
178
|
+
3 `action` + `context` on those sites raw +150 / +57 / +57 brotli +52 / +16 / +15
|
|
179
|
+
4 the 2xx refusal tagged `emitter` raw +55 / +49 / +49 brotli 0 / +10 / +12
|
|
180
|
+
|
|
181
|
+
Squeezed first; three arms measured and not taken. `transportError` without its
|
|
182
|
+
`context` parameter is -12 raw and -12 / -5 / -8 brotli, declined once the
|
|
183
|
+
parameter earned its place. Dropping the builder for one shared
|
|
184
|
+
`{ emitter: 'transport' }` const inlined at each site is a wash (full +19 raw /
|
|
185
|
+
0 brotli, core -6 / -7, elements -6 / +5), so the named function stays as the one
|
|
186
|
+
place saying which kind of error the transports mint. And row 4 as a `retryable`
|
|
187
|
+
boolean is 19 / 22 / 22 raw and 17 / 6 / 4 brotli dearer than the tag.
|
|
188
|
+
|
|
189
|
+
The five registry rows cost **nothing in these bundles**, and that scope is the
|
|
190
|
+
whole claim - verified by grep, the `fix` strings appear 0 times in all three,
|
|
191
|
+
because `/* @__PURE__ */` on `ERROR_CODE_REGISTRY`'s freeze still shakes the whole
|
|
192
|
+
table out of a build that never reads it. A consumer who imports the registry does
|
|
193
|
+
pay: the root barrel row in `docs/BUNDLE-SIZES.md` moved 23.3 -> 23.8 KB brotli
|
|
194
|
+
across this work, rows and new code strings together. The fifth budget
|
|
195
|
+
did not need raising either: the Blade consumer ESM bundle measures 6,342 against
|
|
196
|
+
its 6,380 ceiling.
|
|
197
|
+
|
|
198
|
+
Step 1 pays for itself and is the piece that fixes the defect; steps 2-4 are
|
|
199
|
+
features, taken on the trade: a transport failure now carries a code a consumer
|
|
200
|
+
can switch on and `getErrorEntry(code).fix` can explain, an emitter a logger can
|
|
201
|
+
route on, and an action.
|
|
202
|
+
|
|
203
|
+
**Fixture:** `tests/transport-code-owner.test.ts`, real bridges with only `fetch`
|
|
204
|
+
stubbed, counting HTTP requests rather than inspecting fields, because an attempt
|
|
205
|
+
count is what the defect cost. Verified to FAIL on all three defect cases against
|
|
206
|
+
`git show HEAD:` copies of every touched module. Two controls, and both PASS
|
|
207
|
+
against the pre-fix module, which is what makes the three failures evidence: with
|
|
208
|
+
no `code` the status rule still decides 503 -> 3 and 422 -> 1, and a transport
|
|
209
|
+
failure with a retryable code is still retried - so `emitter: 'transport'` cannot
|
|
210
|
+
have become a blanket "never retry" that would pass the first three assertions
|
|
211
|
+
for the wrong reason.
|
|
212
|
+
|
|
213
|
+
**Not addressed, and not from a "0 consumers" reading.** `VcTestError`
|
|
214
|
+
(`src/vitest-pure.ts`) carries a `VC_TEST_*` code and no `emitter`, so it is the
|
|
215
|
+
one library error class the new provenance rule does not recognise. No path puts
|
|
216
|
+
one in a `CommandResult` today. `ERROR_CODE_REGISTRY` calls itself the complete
|
|
217
|
+
registry of all `BusError` codes and nothing asserts that; a code added without a
|
|
218
|
+
row would make it false silently, which is the same shape as the incomplete
|
|
219
|
+
plugin catalogue found in whitepaper section 8.
|
|
220
|
+
|
|
221
|
+
### `HttpError` is a class, and four hand-assembled sites are one call
|
|
222
|
+
|
|
223
|
+
It was `Error & { ... }` since v0.4. A type cannot be called, so four places
|
|
224
|
+
built the shape by hand: the non-2xx and timeout constructors in `http.ts`, and
|
|
225
|
+
the two catch blocks in `transports.ts` that re-assemble it field by field to
|
|
226
|
+
swap in the backend's own message. Each spelled the same four assignments out and
|
|
227
|
+
each was free to forget one. That is not hypothetical - it is how the envelope
|
|
228
|
+
paths came to drop `code` while the catch path twelve lines away kept it, the
|
|
229
|
+
defect the entry above this one fixed.
|
|
230
|
+
|
|
231
|
+
Nothing about the shape moved: the same five fields under the same names, and
|
|
232
|
+
`HttpError` still names a type wherever it did, because a class declaration
|
|
233
|
+
declares one. `index.ts` re-exports it as a type only, so the constructor is not
|
|
234
|
+
new public surface and stays shakeable out of a barrel import.
|
|
235
|
+
|
|
236
|
+
**Three shapes were measured**, one build each, brotli, full / core / elements:
|
|
237
|
+
|
|
238
|
+
class (shipped) 12,011 / 8,125 / 8,638
|
|
239
|
+
plain factory function 12,030 / 8,128 / 8,645
|
|
240
|
+
one Object.assign expression 12,021 / 8,131 / 8,642
|
|
241
|
+
|
|
242
|
+
The class is smallest in all three. It is 34 B LARGER raw in all three, and raw
|
|
243
|
+
is the ceiling that absorbs toolchain drift rather than the headline number - the
|
|
244
|
+
budget comments have said brotli is the meaningful metric since the Vite 8 move.
|
|
245
|
+
|
|
246
|
+
It is also the only one of the three that can be EXTENDED: a factory returns an
|
|
247
|
+
intersection nobody can subclass, and `Object.assign` returns a shape with no
|
|
248
|
+
identity at all. That is the tie-break the bytes did not need to make here, and
|
|
249
|
+
it is the reason to prefer it when they are this close.
|
|
250
|
+
|
|
251
|
+
`instanceof` is deliberately not what this buys. Using it to tell a library error
|
|
252
|
+
from a backend one was considered and dropped - `emitter` answers provenance, and
|
|
253
|
+
answers it for a plain object too.
|
|
254
|
+
|
|
255
|
+
**Cost: +72 raw / +71 / +71 and +8 brotli / -9 / +2.** Not a saving, and it was
|
|
256
|
+
not taken as one. `core` gets smaller; `full` pays 8 bytes. What it buys is that
|
|
257
|
+
a site can no longer omit a field. The Blade consumer ESM bundle measures 6,344
|
|
258
|
+
against its 6,380 ceiling, so that budget still does not move.
|
|
259
|
+
|
|
260
|
+
The note in `transports.ts` that priced sharing the two catch blocks stays, with
|
|
261
|
+
its number intact, and now says why this is not the case it declined: that
|
|
262
|
+
measurement was for a new shared HELPER, +30 B raw. Both sites call a CONSTRUCTOR
|
|
263
|
+
the layer below had to have anyway, so the shared code is paid for once by
|
|
264
|
+
`http.ts` rather than added.
|
|
265
|
+
|
|
266
|
+
### BREAKING: `sync` is now `createChannel`
|
|
267
|
+
|
|
268
|
+
`sync(opts)` -> `createChannel(opts)`, `SyncOptions` -> `ChannelOptions`,
|
|
269
|
+
`SyncLane` -> `ChannelLane`. Same module, same behaviour, same options. The
|
|
270
|
+
section below is the change this one finishes.
|
|
271
|
+
|
|
272
|
+
**The name promised the one thing the mechanism cannot do.** That section ends
|
|
273
|
+
with a paragraph headed "What it still does not do": facts mirror but seeds do
|
|
274
|
+
not, a context opened later does not catch up, and tabs converge only when the
|
|
275
|
+
facts are absolute. A name needing a defensive paragraph is the defect. And
|
|
276
|
+
`sync` was the only bare verb among the primitives - `createCommandBus`,
|
|
277
|
+
`createFastLane`, `createChamber`, `createWorkflow`, `createHttpClient`,
|
|
278
|
+
`createOutbox`, `createTestBus` - so it read as a verb you call rather than a
|
|
279
|
+
handle you hold and close.
|
|
280
|
+
|
|
281
|
+
It also kept being mis-filed. Seven documentation sites listed it among the bus
|
|
282
|
+
plugins after it stopped being one, in README, ROADMAP, both migration guides,
|
|
283
|
+
the whitepaper's catalogue and an example, and one claimed the plugin pipeline
|
|
284
|
+
"applies automatically" to it, which it cannot.
|
|
285
|
+
|
|
286
|
+
`createChannel` pairs with `createFastLane` on the axis that matters: a lane is
|
|
287
|
+
local, a channel crosses contexts. It also uses the word the API already uses
|
|
288
|
+
for its own option (`channel`).
|
|
289
|
+
|
|
290
|
+
**Names rejected, with the reason each failed:**
|
|
291
|
+
|
|
292
|
+
- `createTabBridge` - names one example. The implementation is a bare
|
|
293
|
+
`new BroadcastChannel(...)`, which reaches every same-origin context: tabs,
|
|
294
|
+
windows, iframes and workers. Encoding "tab" in the name of a general
|
|
295
|
+
mechanism is the defect it was meant to fix.
|
|
296
|
+
- `createBroadcastBridge` - "broadcast" is redundant with "channel", and
|
|
297
|
+
"bridge" is the wrong family. All five `create*Bridge` attach to the BUS
|
|
298
|
+
(`bus.use(createHttpBridge(...))`, `realtime.install(bus)`). This takes a
|
|
299
|
+
lane and never sees a dispatch, so filing it as a bridge repeats the plugin
|
|
300
|
+
mistake one level over.
|
|
301
|
+
|
|
302
|
+
**Migration** is the call site and the import:
|
|
303
|
+
|
|
304
|
+
- import { sync } from 'vapor-chamber'
|
|
305
|
+
+ import { createChannel } from 'vapor-chamber'
|
|
306
|
+
|
|
307
|
+
- const tabSync = sync({ channel, lane, events })
|
|
308
|
+
+ const tabSync = createChannel({ channel, lane, events })
|
|
309
|
+
|
|
310
|
+
**Cost: +9 B raw / +2 B brotli, on the full IIFE only.** That is the whole of
|
|
311
|
+
it, and it is irreducible: `createChannel` appears exactly ONCE in the built
|
|
312
|
+
bundle, as the export key, so 13 characters replace 4. Verified by grep - one
|
|
313
|
+
occurrence in `vapor-chamber.iife.min.js`, zero in core and elements, which
|
|
314
|
+
stay byte-identical and serve as the control. The budget was raised rather than
|
|
315
|
+
squeezed because there is nothing to squeeze: the cost is the public name, once,
|
|
316
|
+
with no dead weight beside it to trade. The one string the rename touched, the
|
|
317
|
+
non-cloneable-payload warning, is DEV-gated and folds out of that build - its
|
|
318
|
+
wording also dropped "to other tabs", which was inaccurate for the same reason
|
|
319
|
+
`createTabBridge` was.
|
|
320
|
+
|
|
321
|
+
Also corrected while renaming: README's plugin list had "cross-tab sync" among
|
|
322
|
+
the plugins, and ROADMAP's capability list had it under Transports as a
|
|
323
|
+
"cross-tab" bridge. Neither is a plugin and neither is tab-scoped.
|
|
324
|
+
|
|
325
|
+
### `buildRunner` is `@internal`, and the reference stops advertising it
|
|
326
|
+
|
|
327
|
+
It sat in the root barrel with a full section in the generated reference, while
|
|
328
|
+
every mention of it in prose treats it as internals - `docs/performance.md` calls
|
|
329
|
+
it "buildRunner's PERF NOTE", the whitepaper twice describes it as the sync
|
|
330
|
+
runner. Nothing outside `src/` imports it: `src/testing.ts` takes it relatively
|
|
331
|
+
from `./command-bus`, and `command-bus.ts` uses it internally. No test, no
|
|
332
|
+
example, no documented consumer call.
|
|
333
|
+
|
|
334
|
+
**Not a removal.** The export stays exactly where it is, so anything that did
|
|
335
|
+
reach for it keeps working - the no-removal-on-zero-consumers rule respected
|
|
336
|
+
rather than argued around. One JSDoc tag, no code, and the `@internal` filter
|
|
337
|
+
`generate-api-docs.mjs` already applies does the rest; an existing mechanism, not
|
|
338
|
+
a new one. The reference goes from 459 to 458 exports, the root entry from 196 to
|
|
339
|
+
195. The existing PERF NOTE above the function is untouched: the new block sits
|
|
340
|
+
between it and the declaration, where TypeScript reads it.
|
|
341
|
+
|
|
342
|
+
### Fixed: the API reference published a name nobody can import
|
|
343
|
+
|
|
344
|
+
Two defects in the generated reference, both live in the output rather than stale
|
|
345
|
+
files - regenerating before the fix produced no diff.
|
|
346
|
+
|
|
347
|
+
**A name that does not compile.** `getExportsOfModule()` is mapped through
|
|
348
|
+
`resolve()` so that `@internal` and the type come from the declaration, and that
|
|
349
|
+
also renamed the symbol. A declaration's name is not always the public one:
|
|
350
|
+
`src/vitest.ts` does `export { expect }` on a binding Vitest declares as
|
|
351
|
+
`globalExpect`. So `docs/api/vitest.md` listed `globalExpect` under Variables,
|
|
352
|
+
documented its signature, and never mentioned `expect` - while that module's own
|
|
353
|
+
JSDoc tells you to write `import { expect, it } from 'vapor-chamber/vitest'`. The
|
|
354
|
+
same shape as the `v-vc:payload` drift: a documented spelling that nothing
|
|
355
|
+
compiles. Fixed by keeping the export name BESIDE the resolved symbol instead of
|
|
356
|
+
in place of it, in a map rebuilt per entry point, because one declaration can be
|
|
357
|
+
exported under different names by different entries and the program is shared.
|
|
358
|
+
|
|
359
|
+
**Every SCREAMING_SNAKE entry in a Contents list was a dead link.** `anchor()`
|
|
360
|
+
folded `_` into `-`, but GitHub's slugger keeps underscores, so
|
|
361
|
+
`ERROR_CODE_REGISTRY` anchored as `error_code_registry` while the link said
|
|
362
|
+
`error-code-registry`. Seven of them. The sections were present and correct; only
|
|
363
|
+
the navigation to them was broken.
|
|
364
|
+
|
|
365
|
+
Also here: `scripts/stamp-docs.mjs` claimed "a fresh checkout has
|
|
366
|
+
docs/metrics.json (committed)". It does not - `.gitignore` ignores it
|
|
367
|
+
deliberately, because the counts depend on what else has run and a committed copy
|
|
368
|
+
would churn. The ignore is right and the comment was its opposite, and the cost it
|
|
369
|
+
hid is worth one line: a marker whose source is absent renders exactly like a
|
|
370
|
+
current one, and `--check` reports it current having had nothing to compare
|
|
371
|
+
against. Twelve `vc:bench*` markers are that set.
|
|
372
|
+
|
|
373
|
+
### Fixed: the ascii guard raced the A/B tests' generated tree
|
|
374
|
+
|
|
375
|
+
`npm run test:run` failed intermittently on a file that is not on disk -
|
|
376
|
+
`ENOENT: tests/__ref/before-cancel/before-cancel-pre.ts`, from
|
|
377
|
+
`tests/ascii-guard.test.ts` - while the same file passed 15/15 on its own. Each
|
|
378
|
+
`tests/*-ab.test.ts` writes `tests/__ref/<name>/` while it runs and removes it
|
|
379
|
+
afterwards, and eight of them do, so a vitest run has several writers going at
|
|
380
|
+
once. The guard's test collects paths into one list and reads them later, which
|
|
381
|
+
turns that race into an ENOENT on a path that existed at walk time.
|
|
382
|
+
|
|
383
|
+
`__ref` joins `SKIP_DIRS` in `scripts/check-ascii.mjs`, beside `dist` and
|
|
384
|
+
`coverage`, which is where a generated tree belongs. One directory name fixes both
|
|
385
|
+
readers - the guard's own walk and the test's unfiltered one, since the test
|
|
386
|
+
imports `SKIP_DIRS` rather than keeping its own list.
|
|
387
|
+
|
|
388
|
+
NOT fixed by catching ENOENT in the reader, which is the other obvious way: that
|
|
389
|
+
would make the guard able to skip a real file silently, and a guard that can do
|
|
390
|
+
that is worse than the flake. Verified both directions - the guard still scans its
|
|
391
|
+
full set and still REJECTS an em dash injected into `src/dev.ts`, because a skip
|
|
392
|
+
rule that quietly matched too much would be indistinguishable from a working
|
|
393
|
+
guard.
|
|
394
|
+
|
|
395
|
+
### `gate.mjs` advertised a flag it no longer implements
|
|
396
|
+
|
|
397
|
+
Its usage block offered `node scripts/gate.mjs [--expect-stamp-drift]`. The flag
|
|
398
|
+
is gone - the docblock says so nine lines later ("it was a countdown, not a
|
|
399
|
+
setting, and a second use would have made it one") and the implementation went
|
|
400
|
+
with it. Only the usage line survived, documenting an option that does nothing,
|
|
401
|
+
and passing it is silently accepted as an unknown argument: the worst of the three
|
|
402
|
+
possible behaviours, because a reader who follows the usage line believes stamp
|
|
403
|
+
drift is being tolerated when the check is unconditional.
|
|
404
|
+
|
|
405
|
+
### Documentation: fourteen false claims, and one drift direction worth naming
|
|
406
|
+
|
|
407
|
+
Every tracked document that had never been read end to end was read end to end,
|
|
408
|
+
each claim checked against source before reading for sense. What that found, by
|
|
409
|
+
file:
|
|
410
|
+
|
|
411
|
+
- **ROADMAP.md** - "we align on rc.8" while the pin reads rc.9, and a header line
|
|
412
|
+
that read "Last reviewed against Vue 3.6.0-rc.8". The file carried FIVE
|
|
413
|
+
`vc:vueAligned` markers: four were stamped and correct the whole time, and only
|
|
414
|
+
the one hand-typed mention was stale. It is a marker now. Also: the prose plugin
|
|
415
|
+
list omitted `optimisticUndo` and `supersede`, and the feature matrix - which
|
|
416
|
+
that file calls "the single source of truth for feature status" - omitted
|
|
417
|
+
`supersede` entirely, a public export since v1.9.0. And "Four pieces have landed
|
|
418
|
+
under it" was two short.
|
|
419
|
+
- **CONTRIBUTING.md** - `plugins-io.ts` still listed `sync` as a plugin;
|
|
420
|
+
`directives.ts` named one Vapor directive where there are three; and
|
|
421
|
+
`npm run gate` appeared ZERO times, in the file whose Workflow section lists
|
|
422
|
+
seven commands and says "the order is part of the gate". It is now the first
|
|
423
|
+
thing that section offers, with its two traps: it wants a committed tree, and
|
|
424
|
+
`| tail` masks its exit code.
|
|
425
|
+
- **docs/integrations/laravel.md** - it told backend authors `code` does not
|
|
426
|
+
survive a 200 body and to read it back only from a throw. Also added, because
|
|
427
|
+
nothing said it: a redirect is a FAILED dispatch whether or not `onRedirect` is
|
|
428
|
+
wired, it carries `VC_TRANSPORT_REDIRECT` and `error.context.url`, and `retry()`
|
|
429
|
+
will not re-send it.
|
|
430
|
+
- **docs/integrations/vitest.md** - the Diagnostics section enumerates what a
|
|
431
|
+
`VcTestError` carries, and it gained a field this cycle.
|
|
432
|
+
- **examples/vapor-island-cart/README.md** - called `createChannel` a plugin and
|
|
433
|
+
counted "four plugins", while its own `src/store.ts` says "the bridge is NOT a
|
|
434
|
+
bus plugin", uses `createFastLane()`, and has three `bus.use` calls. The rename
|
|
435
|
+
swept the name and left the semantics, which is what v1.22.0 actually changed.
|
|
436
|
+
- **examples/exo-astro/README.md** - "vapor-chamber itself still runs on Node
|
|
437
|
+
>= 20.19; only this example carries the higher bar". The root `engines.node` is
|
|
438
|
+
`>=22.12.0`, so there is no higher bar. True once; the root floor moved up.
|
|
439
|
+
- **examples/vapor-sfc/README.md** - a file tree that reads as complete, missing
|
|
440
|
+
one file.
|
|
441
|
+
|
|
442
|
+
Source comments in the same pass: four modules stopped calling the alignment log a
|
|
443
|
+
table, `src/glyphs.ts` and `src/store.ts` lost three citations to files that never
|
|
444
|
+
existed, `src/iife-elements.ts` and `src/mcp.ts` stopped carrying `@example`s that
|
|
445
|
+
instruct the reader into a no-op, and `src/plugins-io.ts` records what happens when
|
|
446
|
+
a backend uses the `VC_` prefix.
|
|
447
|
+
|
|
448
|
+
**The drift direction worth naming, because nothing watches for it.** A doc that
|
|
449
|
+
correctly describes a LIMITATION acquires an expiry date the moment someone sets
|
|
450
|
+
out to remove the limitation. It does not drift away from the code; the code moves
|
|
451
|
+
to meet it, and the sentence stays internally coherent, cites real behaviour, and
|
|
452
|
+
is exactly what a reader trusts. Two instances landed in one day, both about
|
|
453
|
+
`code` not surviving an envelope failure - the page and the PHP docblock each said
|
|
454
|
+
so accurately, and then the client was fixed. When a commit removes a limitation,
|
|
455
|
+
grep for the words that documented it.
|
|
456
|
+
|
|
457
|
+
Two checks earned their place and one did not. A document that carries the same
|
|
458
|
+
fact as BOTH a `vc:` marker and a hand-typed literal will drift at the hand-typed
|
|
459
|
+
one - that is how ROADMAP broke, and it is worth grepping a file for a marker
|
|
460
|
+
family and then for that value outside a marker. Where a runnable example ships
|
|
461
|
+
for a documented pattern, diff them; three of this pass's sharpest finds came from
|
|
462
|
+
an example contradicting its own README. And a scan for staleness BY VALUE cannot
|
|
463
|
+
see staleness by mismatch: it finds a current value typed by hand, never an old
|
|
464
|
+
one, which is why "we align on rc.8" survived the first sweep of the same file.
|
|
465
|
+
|
|
466
|
+
### Upgrade note: your OWN Vapor directives, if they take an argument
|
|
467
|
+
|
|
468
|
+
v1.22.0's rc.9 notes cover this library's directives. The same Vue change
|
|
469
|
+
reaches any Vapor directive a consumer wrote: from rc.9 (#15490) the third
|
|
470
|
+
parameter is a getter, `() => Arg`, not the string. Code written against rc.8
|
|
471
|
+
that reads it as a string fails only in a COMPILED template, so tests that
|
|
472
|
+
hand-write the `withVaporDirectives` tuple stay green. Reported by
|
|
473
|
+
`vapor-chamber-router-wire`: `v-wire:title` threw `path.split is not a
|
|
474
|
+
function` on every compiled template while six hand-tuple tests passed.
|
|
475
|
+
|
|
476
|
+
- function vMine(el, value, argument, modifiers) { use(argument) }
|
|
477
|
+
+ function vMine(el, value, argument, modifiers) { use(argument?.()) }
|
|
478
|
+
|
|
479
|
+
`tests/compile-vapor.ts` is the fixture that catches it; compile the template
|
|
480
|
+
on the installed Vue rather than writing its output by hand. If a vitest config
|
|
481
|
+
aliases `vue` to the with-vapor build, make it `{ find: /^vue$/ }` - a string
|
|
482
|
+
key also rewrites `vue/compiler-sfc`, and `vitest.vapor.config.ts` here now
|
|
483
|
+
does the same.
|
|
484
|
+
|
|
485
|
+
### Fixed: one refused command no longer holds the outbox forever
|
|
486
|
+
|
|
487
|
+
`runFlush` stopped at the first failed replay of ANY kind and kept that record
|
|
488
|
+
at the head. That is right for a transient failure - order is preserved and the
|
|
489
|
+
next flush retries from the same spot - and wrong for a refusal: the server has
|
|
490
|
+
already answered, so every flush re-sent the same record, got the same answer,
|
|
491
|
+
and the records queued behind it never left. Measured with the reference
|
|
492
|
+
controller's batch shape, one refused record ahead of two good ones:
|
|
493
|
+
|
|
494
|
+
flush 1: { replayed: 0, failed: 1 } pending 3
|
|
495
|
+
flush 2: { replayed: 0, failed: 1 } pending 3
|
|
496
|
+
flush 3: { replayed: 0, failed: 1 } pending 3
|
|
497
|
+
|
|
498
|
+
The only way out was `clear()`, which drops the good records too, and
|
|
499
|
+
`'outboxFlushed'` did not say which record was stuck.
|
|
500
|
+
|
|
501
|
+
A failure the SERVER gave as its verdict on the command is now final: the record
|
|
502
|
+
is dropped, `'outboxRejected'` fires with `{ record, error }` so the app can tell
|
|
503
|
+
the user or re-queue it, and the flush moves on. "Verdict" by default means a
|
|
504
|
+
refusal inside a 2xx, or a 4xx other than 401, 408, 419 and 429. Everything else
|
|
505
|
+
still blocks exactly as before - a 5xx, a network failure, and every error the
|
|
506
|
+
library raised itself (a plugin that threw, a handler not registered yet, a
|
|
507
|
+
redirect) - because dropping a queued command over a client-side failure loses
|
|
508
|
+
the user's data. The two existing tests that pin "nothing was lost" for those
|
|
509
|
+
cases are unchanged and pass.
|
|
510
|
+
|
|
511
|
+
Which failures are final is the app's call, so it is an option:
|
|
512
|
+
`createOutbox({ isRetryable: (error, record) => boolean })`. It sees the record,
|
|
513
|
+
so the rule can be per action and in the backend's own codes; `() => true`
|
|
514
|
+
restores the old block-on-everything behaviour.
|
|
515
|
+
|
|
516
|
+
**Changed shape:** the flush summary gains `rejected` -
|
|
517
|
+
`{ replayed, failed, rejected }`, from `flush()` and on `'outboxFlushed'`.
|
|
518
|
+
|
|
519
|
+
**Fixture:** `tests/outbox-refusal.test.ts`, real bridges with only `fetch`
|
|
520
|
+
stubbed - a batched refusal and a single-endpoint 422 at the head are rejected
|
|
521
|
+
and the rest replay; 401, 429 and 500 are kept; the app's predicate overrides
|
|
522
|
+
both ways. All 8 cases fail against the pre-fix module.
|
|
523
|
+
|
|
524
|
+
### Fixed: `onRedirect` never fired with the reference controller, and the batching bridge ignored it
|
|
525
|
+
|
|
526
|
+
Two halves of one feature, each broken on its own, measured by running the real
|
|
527
|
+
controller under Laravel and feeding its output to the real bridge.
|
|
528
|
+
|
|
529
|
+
**The controller.** `docs/integrations/laravel.md` tells an action to hand a
|
|
530
|
+
navigation back with `return ['redirect' => route('login')]`. The controller
|
|
531
|
+
wrapped every return value as `state`, so it answered
|
|
532
|
+
`{"ok":true,"state":{"redirect":"/login"}}` - and both bridges read `redirect`
|
|
533
|
+
at the TOP of the envelope. Fed that exact body, `createHttpBridge` with
|
|
534
|
+
`onRedirect` set: `onRedirect calls: 0 | result.ok: true | value: {"redirect":"/login"}`.
|
|
535
|
+
The documented recipe had never navigated. `dispatchOne()` now lifts exactly
|
|
536
|
+
that shape - an array whose only key is `redirect` - to `{ redirect }`, and per
|
|
537
|
+
result on `batch()`. A state that merely contains a `redirect` key is still
|
|
538
|
+
data.
|
|
539
|
+
|
|
540
|
+
**The batching bridge.** `BatchingHttpBridgeOptions` extends
|
|
541
|
+
`HttpBridgeOptions`, so it has always ACCEPTED `onRedirect`, and never read it:
|
|
542
|
+
a redirect result resolved as a success. It now fails that command with
|
|
543
|
+
`VC_TRANSPORT_REDIRECT` and `context.url`, as the single bridge does, and calls
|
|
544
|
+
`onRedirect` once per batch with the first URL - two navigations in one tick
|
|
545
|
+
race. `BackendResponse` gains the `redirect?: string` field the single bridge
|
|
546
|
+
was already reading untyped.
|
|
547
|
+
|
|
548
|
+
**Fixture:** `tests/batch-redirect.test.ts` - a redirect beside a normal
|
|
549
|
+
result, two redirects in one batch, no `onRedirect`, and the single bridge as
|
|
550
|
+
the control. The three batch cases fail against the pre-fix module; the
|
|
551
|
+
control passes on both.
|
|
552
|
+
|
|
553
|
+
### Fixed: the ascii guard's test failed outside a git checkout
|
|
554
|
+
|
|
555
|
+
`tests/ascii-guard.test.ts` lists the tracked root files with `git ls-files`
|
|
556
|
+
at collection time. In a copy that is not a git checkout - a release folder, an
|
|
557
|
+
extracted tarball - that threw, and the whole file failed with 0 tests: a false
|
|
558
|
+
failure, and its other 14 checks never ran. It now falls back to every regular
|
|
559
|
+
file in the root, which is a SUPERSET of what git tracks, so the check is never
|
|
560
|
+
weaker there. Armed both ways: with the guard's `rootFiles()` stubbed to return
|
|
561
|
+
nothing, the fallback fails the assertion it exists for; restored, 15/15 pass,
|
|
562
|
+
in a checkout and out of one.
|
|
563
|
+
|
|
564
|
+
### Documentation: three whitepaper claims and one integration-guide claim
|
|
565
|
+
|
|
566
|
+
- `whitepaper.md`, the plugin table: the batching bridge had "Same options and
|
|
567
|
+
backend contract". Its `retry` covers a failure of the whole POST only; a
|
|
568
|
+
command refused inside the batch arrives with the backend's `code`, and
|
|
569
|
+
re-sending it is the app's `retry({ isRetryable })`. Section 11.7 now shows
|
|
570
|
+
that, scoped to the bridged actions.
|
|
571
|
+
- `whitepaper.md`, the `retry` example: `isRetryable: (err) => err.message !==
|
|
572
|
+
'Unauthorized'` matched nothing the library emits (`authGuard` says
|
|
573
|
+
"Unauthorized: X requires authentication"). It now shows the app's rule in
|
|
574
|
+
the backend's own codes, and says the predicate replaces the default.
|
|
575
|
+
- `whitepaper.md`: the module map still listed `sync` in `plugins-io.ts`, and
|
|
576
|
+
the store row still called it `sync`.
|
|
577
|
+
- `integrations/laravel.md`: "a failure inside a 2xx ... you refused in the
|
|
578
|
+
body" is false on the batch endpoint, where a crash (`internal_error`, 500 in
|
|
579
|
+
`dispatchOne()`) is delivered inside the same 200. It says so now, and shows
|
|
580
|
+
the app-owned rule.
|
|
581
|
+
|
|
5
582
|
## v1.22.0 - 2026-09-21 Vue 3.6.0-rc.9 alignment
|
|
6
583
|
|
|
7
584
|
89 commits in `v3.6.0-rc.8..v3.6.0-rc.9` (the full log, not first-parent; the
|