refactor(runtime,cli): give the optional Turso/libSQL loader one owner (#6268) - #7313
Conversation
#6268) The libSQL/Turso host driver factory existed twice — runtime's turso-driver-factory.ts (#5820) and cli's utils/storage-driver.ts (#5602) — kept equal by hand. The drift had already started: #6345 moved the CLI half onto @objectstack/spec's shared driver vocabulary and left the runtime half on a private Set(['turso', 'libsql']). The runtime now owns the loader; the CLI delegates and re-exports. Critically MissingDriverPackageError is ONE class across both packages, because serve.ts decides fatality with `e instanceof MissingDriverPackageError` and two same-named classes would make that predicate silently stop matching. Two things stay host-owned because moving them would change behaviour: the dynamic import() specifier (the optional package is a peer of @objectstack/cli and is not linked under @objectstack/runtime at all) and the error TYPE for a url-less turso config (the CLI's UnsupportedDriverError, which the ruling keeps in the CLI). The message for the latter is shared. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0158ZQo7LiHSxGWpYKuPq1wu
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 2 package(s): 33 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also reference the affected code. These are read-only:
|
Fixes #6268
@objectstack/driver-tursois an optional install, so neither host can let the open-core datasource factory build thedefaultdatasource for alibsql://selection — both inject a host driver factory instead. That loader was written out twice, and the two copies were kept equal by hand:packages/runtime/src/turso-driver-factory.tsos migrate,createStandaloneStack, embedded hostspackages/cli/src/utils/storage-driver.tsos serve,os startPremise re-verified — and the drift had already started
Both copies were still present on
origin/main@5777b1a, and they were no longer equal. Two divergences, both measured before any edit:isTursoDriverId. cli/runtime: explicit driver(--database-driver/OS_DATABASE_DRIVER)在 CLI 与 standalone stack 之间仍有三处分叉(#6265 后续) #6345 moved the CLI half onto@objectstack/spec's shared driver vocabulary (resolveDatabaseDriverId(id) === 'turso') and left the runtime half on a privateSet(['turso', 'libsql']). They agree only because the spec table'stursorow happens to list exactly those two aliases today — a third spelling added to the table would have reached one host and not the other, silently.create()refusal. The CLI raisesUnsupportedDriverError; the runtime raises a plainErrorwith a[StandaloneStack]prefix.That is the #3741 → #3758 shape the card names: one decision, two implementations, one of them fixed.
What changed
@objectstack/runtimeowns the loader;@objectstack/cliconsumes it. The runtime now exportsloadTursoDriverFactory,isTursoDriverId,MissingDriverPackageError,TURSO_DRIVER_PACKAGEandTURSO_DRIVER_INSTALL_COMMAND;packages/cli/src/utils/storage-driver.tsre-exports them, so every existing CLI import site is unchanged —serve.tswas not touched.isTursoDriverIdmoved onto the shared spec vocabulary on the runtime side too, which is what retires divergence 1 structurally rather than by re-alignment.UnsupportedDriverErrorstays in the CLI per the ruling.One class identity, and it is proven rather than argued
packages/cli/src/commands/serve.ts:1136decides whether a boot failure is fatal withA convergence leaving two same-named classes would make that predicate stop matching and degrade a fatal branch to a non-fatal one with zero noise. So the CLI re-exports the runtime's class rather than declaring its own, and a new test raises the error from the runtime loader and asserts the CLI-side binding still matches it.
Reverse verification. A same-named twin class was injected into the CLI file on purpose, the suite re-run, and the twin removed:
The third line is the trap's exact signature. The suite also carries an in-test demonstration that a message assertion is blind to this defect: a locally-declared twin matches the message, the
nameand theinstallCommandand fails only theinstanceof.Note honestly which direction is load-bearing: the reverse-direction case ("the CLI loader's error matches the runtime binding") stayed green under the break, correctly — with the twin injected the CLI still delegates, so the error was still runtime-raised. The forward direction is the one
serve.tsperforms and the one that moves.Two things deliberately NOT moved
These are host-supplied inputs, not surviving duplication:
The dynamic
import()specifier — its resolution ROOT.import()resolves from the node_modules tree of the module that evaluates it.@objectstack/driver-tursois an optional peer of@objectstack/cliand is not declared by@objectstack/runtimeat all. Measured in this worktree:Had the CLI taken the runtime's default thunk, an operator who ran the exact install command the error prints would still have been told the package was missing. The CLI therefore keeps passing its own (typed) thunk. Both halves are now pinned from opposite sides: a new CLI case asserts the default thunk does find the package, and the pre-existing
standalone-stack.libsql.test.tscase asserts alibsql://boot with no injected thunk takes the missing-package arm precisely because the runtime does not declare it.The error TYPE for a url-less turso config. CLI-only semantics by the ruling. Only the type is host-chosen; the message is passed down from the runtime, so the wording is not duplicated.
The one behaviour difference, reported rather than silently chosen
The card asked for this to be surfaced instead of decided quietly. The two copies' missing-package messages differed, and each difference was pinned by a test on its own side:
/optional peer/i/would start the server against an empty local database/i/OPTIONAL package//os migrate//refuses rather than falling back to SQLite/iOne owner cannot carry two wordings without re-introducing the hand-alignment the card is about, so there is now one message that names both consequences — a server booted against an empty local database, and an
os migrateDDL run against that same one — rather than each host mentioning only its own. Every assertion in the table above still passes unedited, on both sides: the two hosts' existing pins now jointly constrain the single message, which is the strongest available evidence that nothing was quietly dropped.content/docs/data-modeling/drivers.mdxparaphrases this failure and remains accurate; no doc edit was needed.Everything else is identical for every existing path: same error class and fields, same
npm install @objectstack/driver-turso, same underlying import error preserved, same factory handle shape, and no silent SQLite fallback anywhere.Verification
pnpm --filter '@objectstack/cli^...' build— dependency closure first (fresh worktree).pnpm --filter @objectstack/runtime test→ 118 files, 1839 tests passed.pnpm --filter @objectstack/cli test→ 105 files, 1144 tests passed.pnpm --filter @objectstack/cli --filter @objectstack/runtime typecheck→ bothDone, 0 errors.turbo run build --concurrency=2(72/72) thenpnpm check:type-check-debt→OK — 33 ledger entr(ies) re-measured, none above its recorded number;@objectstack/runtimeTEST_DEBT records 227, re-measured 225 — it went down, not up. Ledger left untouched (lowering it is another card's).node scripts/check-nul-bytes.mjsOK;eslinton all four changed files clean.A changeset is included: the missing-package message is operator-visible and
@objectstack/runtimegains public exports, so this is not purely internal.Generated by Claude Code