fix(driver-turso)!: refuse timeout beside a wss:// / ws:// url at construction (ADR-0049 enforce-or-remove) - #16616
Conversation
…struction (ADR-0049 enforce-or-remove) A non-zero TursoDriverConfig.timeout beside a WebSocket url in remote mode reached nothing (@libsql/client 0.17.4's ws client takes no fetch and no timeout option); the constructor now refuses the pair as VALIDATION_ERROR / 400 before super(), naming the key, the scheme and both ways out. Controls pin the width: no timeout, timeout: 0, every HTTP-side scheme with a window, and the replica arm stay accepted. Docblock, README and design-doc lines that promised "not bounded" now say "refused". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
…rso-ws-timeout-refusal
…er entry — nothing is retired) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
📓 Docs Drift Check3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 532de85bf6d32aeff8e47741e0e0eb90a5cc2e82 && git checkout 532de85bf6d32aeff8e47741e0e0eb90a5cc2e82
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a7c14cdf371bdb0f22be28e47a40fab9f7be88d0 6acaf013e40f0926f669e8b27756b4223e0190ed && git checkout -B drift-repro a7c14cdf371bdb0f22be28e47a40fab9f7be88d0 && git merge --no-ff 6acaf013e40f0926f669e8b27756b4223e0190ed
node scripts/docs-audit/affected-docs.mjs --json a7c14cdf371bdb0f22be28e47a40fab9f7be88d0 |
Clause-② cleared → ready → auto-merge armed 14:34:55Z
Provenance. In-seat contract review returned PASS and is recorded verbatim on the card: objectstack-ai/objectstack#16378 (comment 5572144928). Isolated review subagent,
Tier verification (
Landing pre-checks — stated as measured, including the one that is not clean
② in full, because "partial" without the shape is worthless.
⇒ What the offline run did establish: it served 3 reads and its sole complaint was the comment thread — ⛔ not a label disagreement, which is what exit 4 would have been. So the label limbs were read and are consistent. The unread channel is the comment/event corroboration, not the carriers themselves. Substituted instrument, stated so it can be checked rather than trusted: both carriers' label sets read back directly after the strip — card #16378 Governed-surface probe — 0, with a firing control⇒ the one Carried forward, ⛔ not ridden on this PRThe review measured one corner the refusal does not reach:
PM dispatch seat · Generated by Claude Code |
Fixes #16378
What
TursoDriverConfig.timeoutbeside awss:///ws://url in remote mode is refused at construction —VALIDATION_ERROR/ 400, the ADR-0112 envelope, with a message naming the key, the scheme it met and both ways out — instead of being accepted and never delivered. This is option (c) from the card, as routed by triage; (a) routing WebSocket urls over HTTP, (b) a driver-side race and (d) leaving it documented are not taken.Premise check — re-derived on this head
Installed from
packages/drivers/driver-turso:@libsql/client@0.17.4/@libsql/core@0.17.4/@libsql/hrana-client@0.10.0— the pinned versions, unmoved.@libsql/client/lib-esm/node.jssendswss/wsto the ws client andhttps/httpto the HTTP client; the entry this driver imports expandslibsql://to HTTPS (@libsql/coreexpandConfig(config, true)).lib-esm/ws.js: 0fetchhits; it openshrana.openWs(url, config.authToken).lib-esm/http.jsforwardsconfig.fetchintohrana.openHttp(lines 29, 50, 201).@libsql/hrana-client/lib-esm/ws/*.js+lib-esm/index.js: 0timeouthits. The control that fires: afetchgrep overlib-esm/http/hitsclient.js33/46/51/87/114/118 andstream.js21/37/245/246. Note: the card's control as literally worded — atimeoutgrep overlib-esm/http/— also returns 0 on this head; the control that makes the zero a reading is thefetchgrep. The conclusion is unchanged.TursoDriver.detectModeacceptswss:///ws://as'remote'(pinned inturso-driver.test.ts), and after PR feat(driver-turso)!:timeoutbounds remote operations;localPathandwasmleave the published config schema (ADR-0049 enforce-or-remove) #16376 the window is applied only insidecreateRemoteClient(fetch: fetchBoundedBy(timeoutMs)), which the WebSocket client never reads. Awss://url withtimeout: 30000constructed, connected and ran unbounded on this head. Premise valid.Where the refusal sits
In the constructor beside
detectMode, beforesuper()— ahead of the Knex base and of any client, so it cannot be reached with a half-built driver. Both loaders (@objectstack/runtime's host factory and the open-core datasource factory) reach it throughbuildTursoDriverConfigthennew TursoDriver, with no catch in between, so a datasource authoringtimeoutMsbeside a WebSocket url now fails its connect by name. Scoped to remote mode: on the replica armsync()is bounded whatever the url's scheme;timeout: 0is the documented "no bound" and is not refused; a caller-suppliedclientis not consulted (its transport is not the driver's to know — see acceptance notes).Surface repaired
The docblock, README and
docs/design/driver-turso.mdlines PR #16376 wrote ("not bounded") now say "refused" — "not bounded" and "refused" are different promises.TursoConfigSchema(this package's mirror) and@objectstack/spec's strict turso contract are untouched: neither makes a WebSocket promise, so nothing there went stale.Clause-②: yes
Re-derived from the diff: the constructor newly refuses a configuration it accepted on
origin/main— an accept-set narrowing on a published driver option. Changeset@objectstack/driver-turso: minor, BREAKING in the body under the launch-window convention, ADR-0087 dispositionnot-required (no-migration-prescription)— nothing is retired or renamed; the refusal text names both ways out.needs:contract-reviewis hung on the card and on this PR together.Evidence
Package
@objectstack/driver-turso; every build and test run went throughscripts/pm/os-verify-lock.sh(shared box — durations are not idle-box figures).turbo run build --filter='@objectstack/driver-turso^...': 7/7 tasks, exit 0. Package build: exit 0 (check-dts-emitted1/1).tsc --noEmit --listFiles: exit 0; the new pin file is in the program (1 hit).vitest run src/turso-driver-ws-timeout-refusal.test.ts src/turso-driver-timeout.test.ts src/turso-driver.test.ts src/spec/turso.test.ts— 4 files, 146 tests, exit 0.94d13f601d: 46 files, 1189 tests, exit 0 (35.7 s).pnpm --filter @objectstack/spec buildexit 0;check:generated15/15 artifacts up to date, exit 0.Reverse verification — direction predicted before it ran: refusal cases RED, controls GREEN
turso-driver.tsrestored tree-only from BASEac9376a74: anchorrefuseWebSocketTimeoutcount 4 to 0, mutated blob84c634d1…equal to the BASE blob. Pin run under the mutation: 4 failed | 7 passed, exit 1 — the four refusal cases red (the constructor returned a driver,transportMode: 'remote'), all seven controls green. Restored withgit checkout HEAD -- path: count back to 4, blob5c2ff12f…equal to the HEAD blob,git diff HEADempty,git status --porcelainempty. The pin resolves./turso-driver.jsto in-package source, so nodist/leg applies to this ablation.Gates — derived on the merged tree
94d13f601d, 57 commands;--ran: 57 derived, 57 run, 0 unruncheck-adr-0087-registration --base origin/mainwas red on the first changeset draft — it read a two-column way-out table as a migration prescription contradictingno-migration-prescription— rewritten as prose, committed (6acaf013e4), re-run: exit 0.check-changeset-no-major,check-empty-changesetandcheck:nul-bytesre-run on the rewrite: exit 0.PREREQUISITE NOT MET, exit 3), left to CI which builds the closure first:check:doc-formula-expressions(@objectstack/lintnot built),check:dual-build-cjs-loads(a package with nodist/),check:type-check-debt(unbuilt type closure). Per the PM's correction: no heap raise, no whole-workspace build.Acceptance notes
timeoutgrep over hranalib-esm/http/) returns 0 on this head; the firing control is thefetchgrep. Same conclusion.clientin remote mode leavestimeoutreaching nothing — the window is applied only bycreateRemoteClient, which both remote client sites skip whenclientis set. Same ADR-0049 shape, one composition over; outside option (c).OS_DATABASE_URLboot path forwards onlyurlandauthToken, so an env-configured deployment cannot carrytimeoutat all. That is a reading about this repository, not evidence about external deployments.config.timeout, so a datasource authored with the canonicaltimeoutMsis dropped at the seam that builds the driver config #16023 remains open and is not touched here (the datasource seam's reading of the retiredconfig.timeout).Generated by Claude Code