fix(runtime,service-datasource): converge the two libSQL loaders on one config read and one error class (#7314) - #7999
Conversation
…ne config read and one error class (#7314) Two loaders build the libSQL/Turso driver and which one runs is decided by whether the datasource happens to be the host's `default`. #6268 converged the two HOST-injected loaders; it could not reach the third — the open-core `turso` arm in `@objectstack/service-datasource` — and the two had drifted. Point 3: the host loader read `url` and `authToken`; the open-core arm read nine keys. So a `default` libSQL datasource silently lost `encryptionKey` / `syncUrl` / `sync` / `concurrency` / `timeout` / `mode` / `schemaMode`, all accepted by `TursoConfigSchema` and all honoured the moment the datasource was renamed. Both loaders now build through one `buildTursoDriverConfig`, whose key set is DERIVED from a reader table rather than hand-listed; a `packages/cli` compile-time pin fails when that builder and the driver's own `TursoDriverConfig` stop covering the same keys. The host loader also trims the url, as the open-core arm always has. Point 2: `MissingDriverPackageError` was declared in `@objectstack/runtime`, which `service-datasource` cannot import, so the open-core arm raised a plain `Error`. The class moves DOWN to the lowest package that raises it and is re-exported from its old home, so every existing importer keeps compiling — against the same class object, which is what `serve.ts`'s `e instanceof MissingDriverPackageError` fatal branch depends on. Pinned on the constructor argument (not a successful boot) and by object identity (not by name or message), because a dropped key and a twin class both produce a driver that constructs cleanly and a message that reads correctly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RjUepKTxiGcJX6WQtwFmcf
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 2 package(s): 20 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also reference the affected code. These are read-only:
|
|
PM review — The twin: the dispatch was wrong and the report corrects itMy claim comment read The twin is a deliberate negative control: something that matches on message and name and still fails Extending it to fail against the new open-core binding too, and recording the reasoning in place, is the right disposition. Noting this at length because the escape hatch is what saved it: the dispatch said "if it turns out to encode something the real class does NOT, say so — that is a finding, not a cleanup." A brief that had only said "delete it" would have been followed. Point 3 is fixed by derivation, and the pin is built so a copy cannot reappearOne exported The reverse verification proves the derivation is real rather than decorative: removing Two things found along the way that the card did not name: a ninth key ( The three pins each cover a different way this hides
The API-break trap was identified concretely
Ledger discipline held: the new runtime test initially added +1 to #7385 correctly untouched — with a useful note that the moved class is deliberately driver-agnostic ( Generated by Claude Code |
Part of #7314 — points 2 and 3. Point 1 landed via #7384 (
b0c16a561) from thedomain:servicesseat and is not touched here.The defect
Two loaders build the libSQL/Turso driver, and which one runs is decided by something an author cannot see — whether the datasource happens to be the host's
default:@objectstack/runtime'sloadTursoDriverFactory(single owner for the CLI and the standalone stack since observation: 可选 Turso driver 的 loader 现在有两份(cli/utils/storage-driver.ts 与 runtime/turso-driver-factory.ts)—— 建议收敛到单一 owner #6268) servesdefault;createDefaultDatasourceDriverFactory'stursoarm in@objectstack/service-datasourceserves every other door — a datasource created in Setup,testConnection, a declared non-default.#6268 converged the two host loaders. It could not reach the third:
runtimedepends onservice-datasource, never the reverse. The two had drifted in two ways.Point 3 — half the config was silently dropped for
defaultturso-driver-factory.ts:248builtnew TursoDriverCtor({ url, ...authToken })— two keys. The open-core arm read nine.TursoConfigSchemaaccepts all nine, so an encrypted or embedded-replicadefaultlostencryptionKey/syncUrl/sync/concurrency/timeout/mode/schemaModewith no diagnostic anywhere — and got them back the moment the datasource was renamed away fromdefault.Fixed by derivation, not a corrected copy. Both loaders now build through one exported
buildTursoDriverConfig; its key set comes from a reader table that TypeScript refuses to compile with a key missing, so the list is never written twice. A second hand-transcribed list is how the first two came to disagree — they agreed on the day they were written too.The host loader also now trims the url before testing it, as the open-core arm always has: a whitespace-only url is refused by name instead of reaching
@libsql/client.Point 2 — one
MissingDriverPackageErrorThe class was declared at
turso-driver-factory.ts:114in@objectstack/runtime, a package the open-core arm cannot import, so that arm raised a plainError— matched by noinstanceof, pinnable only by message text. The only legal convergence is moving the class down, and that is what this does: it now lives in@objectstack/service-datasource/src/missing-driver-package-error.ts, and both loaders throw the same class object.No importer changes.
MissingDriverPackageError,TURSO_DRIVER_PACKAGEandTURSO_DRIVER_INSTALL_COMMANDare still exported from@objectstack/runtime(and from@objectstack/cli'sutils/storage-driver.tsthrough it) — re-exports now rather than declarations.serve.ts'se instanceof MissingDriverPackageErrorfatal-boot branch depends on that identity, so dropping the old export would have been an API break performed for a refactor's convenience.Pins
Both required pins assert the property that hides from the obvious test:
turso-driver-factory.convergence.test.ts). A dropped key produces a driver that constructs perfectly and connects to the wrong thing; every boot-level assertion stayed green through the years this loader read two keys. Adefaultspec carrying all nine keys is asserted to reach the ctor with all nine, and to equal whatbuildTursoDriverConfigproduces — so a second hand-written list that happens to agree today still fails.MissingDriverPackageError ===theservice-datasourcebinding; the host loader's errorinstanceofthe open-core binding; and — the direction that could not be written before this change — the open-core arm's errorinstanceofthe runtime binding.packages/cli(the only one of the three packages carrying@objectstack/driver-turso, and whosetypecheckcompiles tests). A key the driver adds and the builder never reads is a defect with no runtime symptom at all.Reverse verification
Each half reverted independently, failure observed, restored:
{ url, authToken }AssertionError: expected { Object (url, authToken) } to deeply equal { Object (url, authToken, ...) }. The 5 identity cases stayed green — the halves are independent.throw new Error(...)AssertionError: expected Error: datasource 'warehouse': a libSQL/T… to be an instance of MissingDriverPackageError.encryptionKeyremoved from the shared builderpackages/clitypecheck red:storage-driver.test.ts(460,40): error TS2344: Type '"encryptionKey"' does not satisfy the constraint 'never'.A finding, not a cleanup —
MissingDriverPackageErrorTwinpackages/cli/src/utils/storage-driver.test.ts:447declares a hand-rolled twin of the class. The dispatch brief read it as a stand-in written because the real class could not be reached, to be deleted once it was importable. It is not that, and it was kept. The real class was already reachable in that file — two cases above the twin,MissingDriverPackageErroris asserted===the runtime one. The twin is a deliberate negative control: something that matches on message and name and still failsinstanceof, which is the only thing in the file demonstrating that the passing assertion tests identity rather than wording. Deleting it would have removed the evidence that the pin has teeth. It is extended here to fail against the new open-core binding too, and the reasoning is recorded in place.Checks
pnpm -w typecheck— clean (127 tasks).@objectstack/service-datasource339 ✓ ·@objectstack/runtime2165 ✓ ·@objectstack/cli1249 ✓.pnpm check:type-check-debt— clean; the ledger does not rise (the new runtime test initially added +1 toTEST_DEBT; the type was fixed rather than the entry raised)..changeset/turso-host-loader-convergence.md.Out of scope
#7385 (the
mongodb/sqlite-wasmarms of the same no-remedy defect class) stays withdomain:servicesand is untouched. Noted while here: those two arms still raise an untypedError. The class moved down is deliberately driver-agnostic (driverTypeis a field), so adopting it there is a constructor call away whenever that lane takes it up.Generated by Claude Code