fix(js): report dropped publishes and wire mismatches - #875
Conversation
0-jake-0
left a comment
There was a problem hiding this comment.
Reviewed. This is in scope, respects both compatibility boundaries #836 set (boolean not buffering; surface the mismatch without rejecting), and I found no correctness bugs. Comments below are documentation and follow-up sized, not blockers.
Verification — the gap you flagged is now closed
You couldn't run against a real wasm build, and neither did CI (see finding 1), so I ran it here:
pnpm install --frozen-lockfile, then a locally stubbedjs/src/wasm/(gitignored) standing in for the wasm-pack output — the same shapetests/codec.test.tsalready mocks. The client tests mock every codec function they touch, so the stub only has to satisfy module resolution.pnpm run lint— clean.pnpm test— 40 passed, matching your report.- Mutation check: with the new tests in place I reverted
js/src/index.tsto the base commit (932107e). 4 of the 5 new tests fail, so they are real regressions rather than tautologies. The fifth ("does not report matching versions") passes vacuously against base, which is the expected shape for a negative test.
1. CI did not run the JS suite on this PR
The checks on this PR are clippy, Lint (fmt & clippy), Test (wingfoil), cargo audit, dependency review and pnpm audit. No web-integration run — and that is the only workflow that runs pnpm test.
web-integration.yml triggers on push, workflow_call and workflow_dispatch, with no pull_request; its only caller (integration-tests.yml) is itself workflow_call-only. Because this PR comes from a fork, no push event reaches this repo, so a JS-only change merges on a Rust-only signal.
Not caused by this PR — pre-existing, and worth its own issue — but it is why the local run above matters, and it means the js/tests/ gate that landed with the "the vitest suite ran nowhere until now" comment still doesn't cover fork PRs.
2. The retry advice conflates two different falses
README.md ("Publishing is best effort") says callers "should check the return value and apply their own domain-specific retry policy". But false covers two cases that want opposite handling:
- transient — wasm booting, socket not open. Retry is right.
- permanent — JSON cannot carry the value (the case that also
console.warns). Retry spins forever.
One sentence separating them makes the contract actionable, e.g. noting that only the permanent failure warns, so a retry policy should bound itself rather than loop. The same distinction is worth a line in the publish doc comment (js/src/index.ts:317-320).
3. The quick-start snippet models the pattern the new section warns against
README.md:39 is const sent = client.publish("ui", { kind: "click", note: "hi" }); — assigned and never used, which is exactly the discard the section below it argues against. Something like:
if (!client.publish("ui", { kind: "click", note: "hi" })) {
// dropped — see "Publishing is best effort"
}teaches the contract in the place most readers stop.
4. LatencyTracker is the one first-party consumer still blind
js/src/tracing.ts:164 and :205 call publish and discard the result. send() increments and returns this.seq unconditionally, so a dropped probe consumes a sequence number and produces an onResponse that simply never fires — the same silent-drop class #836 is about. Solid, Svelte and Vue got the boolean; tracing didn't.
Changing send()'s number return is more than this PR should take on, but a doc line on send() saying the publish may be dropped (and that the returned seq is therefore not a promise of a round trip) would close the loop cheaply. tests/tracing.test.ts:34's FakeClient.publish also still returns void — harmless today because it goes through as unknown as WingfoilClient, and because tsconfig.json only includes src/** so tests aren't type-checked at all.
5. Test fixture duplication
tests/client.test.ts's wasm vi.mock factory and FakeSocket are near-copies of tests/codec.test.ts:6-18 and its FakeSocket. The vi.mock call has to stay per-file because of hoisting, but the factory object and the socket double could live in a shared tests/fixtures.ts — the new FakeSocket is a strict superset of codec's, so consolidation is one-directional. Cheaper to do now, at two copies, than at four.
6. Two small precision points
- "once per connection" (PR body and commit message) is really once per
Helloframe — nothing dedupes, and the test sends a single Hello. True in practice since the server sends one, just don't lean on the stronger claim. console.errorfor a version-1 server is what #836 asked for, so no objection. Worth noting the tension it creates: theREADME.mdparagraph directly below says a version-1 server "stays compatible", and that supported configuration now prints a red error on every connect. If it grates in practice,warnfor the known-compatible direction (server older than client) anderrorotherwise would keep the signal without crying wolf. Apps that want to react programmatically already can —ConnectionState.opencarriesversionandwireVersion()is exported — which might be worth a README pointer.
Generated by Claude Code
|
Thanks for the thorough review. I addressed the publish-contract precision in I kept fixture extraction out of this behavioral PR. I also filed #877 for the fork-PR JavaScript CI gap so that the workflow trigger can be reviewed independently. |
0-jake-0
left a comment
There was a problem hiding this comment.
Re-reviewed at 323498f. All four actionable points from the last pass are addressed, and this time I ran it against a real wasm build rather than a stub, which closes the verification gap both of us had.
Verification
wasm-packv0.13.1 +rustup target add wasm32-unknown-unknown, thenpnpm run build:wasm— genuinejs/src/wasm/, not a module-shape stubpnpm run lint— cleanpnpm test— 40 passed across 5 files (client5,codec8,burst5,reconnect7,tracing15)
So the 40-test claim in your PR body holds on the real codec, not just the stub. Combined with the mutation check from the previous pass (4 of 5 new tests fail against 932107e), the new coverage is doing real work.
The delta
README.md:39now branches on the boolean instead of assigning and discarding — the quick start teaches the contract at the place most readers stop (finding 3 ✅)- "Publishing is best effort" now separates the transient
false(booting, reconnecting) from the permanent one (JSON cannot carry the value), and says retries should be bounded (finding 2 ✅) - The
publishdoc comment carries the same distinction rather than deferring to the README (finding 2 ✅) LatencyTracker.send()states that the returned seq is not a promise of a round trip (finding 4 ✅)- The mismatch test no longer claims deduplication the code doesn't do (finding 6a ✅)
Remaining, none blocking
- The README still says the client "logs one explicit error for that connection". It is really once per
Helloframe — true in practice, since the server sends one per connection. Fine as written; just don't build a stronger claim on it later. - Finding 5 (extracting the
vi.mockfactory andFakeSocketintotests/fixtures.ts) deliberately deferred — agreed, it doesn't belong in a behavioural PR. Worth an issue so the third copy doesn't land before the second gets consolidated. - Finding 6b (warn when the server is older than the client, error otherwise) not taken. It was offered as optional and the current behaviour is what #836 asked for, so no objection.
Before merge
Lint (fmt & clippy) on this PR is cancelled (started 02:30, cancelled 03:01) and clippy is neutral, which is why GitHub reports unstable. Nothing in this diff is Rust, so that is infrastructure noise — it needs a re-run rather than a change.
More usefully: #878 registers the pull_request trigger on web-integration.yml, which is the gap finding 1 opened. If that lands first and this branch is rebased or repushed, this PR gets a real pnpm test + typecheck run in CI instead of my local one — and becomes the first JS change here to merge on a JS signal. That's the merge order I'd suggest: #878, then this.
Good, disciplined PR — the boolean-not-buffering line and the "informational, not fatal" mismatch call are both the right side of the compatibility boundaries #836 drew.
Generated by Claude Code
|
Merged as You got both compatibility boundaries #836 drew on the right side without being told: a boolean rather than a buffer, and a mismatch that is surfaced rather than fatal. Those are the two calls the issue left open, and either one taken the other way would have been a much harder change to walk back once the package shipped. The verification write-up was honest about its own limits — you said plainly that the Windows host had no You also turned review feedback around cleanly, and rather than quietly working around the CI gap you found, you filed #877 for it so the workflow trigger could be argued on its own merits. That's the right instinct. Thanks again — please do send more. Generated by Claude Code |
What this changes
WingfoilClient.publish()return whether a frame reached an open WebSocketWhy
The client previously dropped publishes silently during wasm startup and reconnect windows, while subscription replay made the API look more durable than it is. It also stored the server's Hello version without comparing it to the local wire version, so incompatibilities surfaced later as opaque decode noise.
Closes #836.
How it was verified
cd js && pnpm run lintcd js && pnpm test- 40 passed, including 5 new client testsgit diff --checkThe clean checkout does not contain generated
js/src/wasm, and this Windows host does not havewasm-pack. The TypeScript and Vitest runs therefore used a gitignored module-shape stub; the client tests mock the codec functions they exercise. Upstream CI remains the proof for the real WASM build.Notes for the reviewer
Publishes are deliberately not queued: replaying stale UI or order events after reconnect is domain-unsafe. A version mismatch is likewise informational rather than fatal because adjacent wire versions can remain compatible.