Skip to content

Commit 8c12a91

Browse files
committed
feat(driver-turso)!: rename the published config timeout to timeoutMs, tombstone on the old spelling (#15682)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
1 parent 8c39a94 commit 8c12a91

4 files changed

Lines changed: 127 additions & 13 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
"@objectstack/driver-turso": minor
3+
---
4+
5+
feat(driver-turso)!: the published connection config names its timeout's unit (#15682, ruling B on #14478)
6+
7+
<!-- adr-0087: not-required (already-registered turso-config-timeout-to-timeout-ms) that protocol-18 conversion rewrites `datasources[].config.timeout` to `timeoutMs` for turso datasources, which is the same authored key this package's schema mirrors — a second entry would restate the same rewrite without converting anything more -->
8+
9+
**BREAKING**`TursoConfigSchema`'s `timeout` is renamed to **`timeoutMs`**. The
10+
value is unchanged: the same milliseconds, the same `min(0)` bound, the same
11+
optionality.
12+
13+
`@objectstack/spec`'s own turso contract renamed the same authored key in
14+
#15680. This package publishes a parallel schema for the same connection config
15+
— the Spec / Studio metadata a host reads to expose Turso configuration UI — so
16+
until now the two declarations of one setting disagreed on its spelling. They
17+
agree again.
18+
19+
The unit was never in the key name, only in the describe prose, while
20+
`sync.intervalSeconds` — the same shape, three keys above — already spelled its
21+
own. One published config carrying both conventions is what made the bare name
22+
dangerous rather than untidy: an author who has just written
23+
`intervalSeconds: 30` has no reason to read `timeout: 30` as milliseconds, and
24+
nothing in the schema, the type or the parse would have told them otherwise.
25+
26+
The old spelling is not dropped in silence. `TursoConfigSchema` is a plain
27+
`z.object`, so a bare deletion would have STRIPPED `timeout` and parsed
28+
successfully. The key stays declared as a tombstone instead: `tsc` refuses it on
29+
anything typed `TursoConfig`, and a value that reaches the parse raises a
30+
message naming `timeoutMs` rather than a generic unrecognised-key error.
31+
32+
```diff
33+
- TursoConfigSchema.parse({ url: 'libsql://app.turso.io', timeout: 30000 })
34+
+ TursoConfigSchema.parse({ url: 'libsql://app.turso.io', timeoutMs: 30000 })
35+
```
36+
37+
`TursoDriverConfig` — this package's TypeScript constructor option, a separate
38+
declaration — keeps its `timeout` spelling and is untouched here.

packages/drivers/driver-turso/src/spec/turso.test.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,13 @@ describe('TursoConfigSchema', () => {
6262
intervalSeconds: 120,
6363
onConnect: false,
6464
},
65-
timeout: 30000,
65+
timeoutMs: 30000,
6666
wasm: true,
6767
});
6868

6969
expect(config.encryptionKey).toBe('my-secret-key-256');
7070
expect(config.concurrency).toBe(50);
71-
expect(config.timeout).toBe(30000);
71+
expect(config.timeoutMs).toBe(30000);
7272
expect(config.wasm).toBe(true);
7373
});
7474

@@ -83,7 +83,7 @@ describe('TursoConfigSchema', () => {
8383
expect(config.syncUrl).toBeUndefined();
8484
expect(config.localPath).toBeUndefined();
8585
expect(config.sync).toBeUndefined();
86-
expect(config.timeout).toBeUndefined();
86+
expect(config.timeoutMs).toBeUndefined();
8787
expect(config.wasm).toBeUndefined();
8888
});
8989

@@ -124,13 +124,42 @@ describe('TursoConfigSchema', () => {
124124
})).toThrow();
125125
});
126126

127-
it('should reject config with negative timeout', () => {
127+
it('should reject config with negative timeoutMs', () => {
128128
expect(() => TursoConfigSchema.parse({
129129
url: ':memory:',
130-
timeout: -1,
130+
timeoutMs: -1,
131131
})).toThrow();
132132
});
133133

134+
// [#15682, ruling B on #14478] The rename's tombstone. Asserted on the
135+
// REFUSAL ENVELOPE — which key was refused and what the message prescribes —
136+
// rather than on `toThrow()` alone: a bare `toThrow()` here stays green for a
137+
// schema that lost the tombstone entirely and simply required `url`, and it
138+
// would stay green for a strip that never refused at all. What must hold is
139+
// that the old spelling is REFUSED and that the refusal names the new key.
140+
it('refuses the retired `timeout` spelling, and the refusal names `timeoutMs`', () => {
141+
const result = TursoConfigSchema.safeParse({ url: ':memory:', timeout: 30000 });
142+
143+
expect(result.success).toBe(false);
144+
const issue = result.error!.issues.find((i) => i.path.join('.') === 'timeout');
145+
expect(issue).toBeDefined();
146+
expect(issue!.message).toContain('timeoutMs');
147+
expect(issue!.message).toContain('milliseconds');
148+
// The standardized closing sentence — the one channel an upgrading author
149+
// is guaranteed to hit carries the command, not just the diagnosis.
150+
expect(issue!.message).toContain('os migrate meta --from 17');
151+
});
152+
153+
// The other half of the tombstone: it refuses a VALUE, it does not make the
154+
// whole config unparseable. A tombstone that took the object down with it
155+
// would read identically in the case above.
156+
it('the tombstone leaves a config that never wrote `timeout` untouched', () => {
157+
const config = TursoConfigSchema.parse({ url: ':memory:', timeoutMs: 5000 });
158+
159+
expect(config.timeoutMs).toBe(5000);
160+
expect('timeout' in config).toBe(false);
161+
});
162+
134163
it('should accept config with environment variable patterns', () => {
135164
const config = TursoConfigSchema.parse({
136165
url: '${TURSO_DATABASE_URL}',
@@ -150,13 +179,13 @@ describe('TursoConfigSchema', () => {
150179
expect(config.concurrency).toBe(100);
151180
});
152181

153-
it('should accept zero timeout (no timeout)', () => {
182+
it('should accept zero timeoutMs (no timeout)', () => {
154183
const config = TursoConfigSchema.parse({
155184
url: ':memory:',
156-
timeout: 0,
185+
timeoutMs: 0,
157186
});
158187

159-
expect(config.timeout).toBe(0);
188+
expect(config.timeoutMs).toBe(0);
160189
});
161190
});
162191

packages/drivers/driver-turso/src/spec/turso.zod.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,22 @@ export const TursoSyncConfigSchema = lazySchema(() => z.object({
4646
// 2. Connection Configuration
4747
// ==========================================================================
4848

49+
/**
50+
* The prescription the retired `timeout` key raises, and the text `tsc` and the
51+
* parse both carry. Standardized closing sentence — the `os migrate meta`
52+
* wording states a property of the TOOL and is not a choice (see
53+
* `retired-key.ts` in `@objectstack/spec`, whose header owns that ruling).
54+
*
55+
* ⛔ No internal issue id in this string: it is customer-facing text and
56+
* `check:doc-authoring` refuses one. The ids live in the comments beside the
57+
* keys below.
58+
*/
59+
const TIMEOUT_RETIRED =
60+
'`turso config.timeout` was renamed to `timeoutMs` in @objectstack/driver-turso 17 — the unit of a '
61+
+ 'duration-shaped number lives in the key name, not only in the describe prose. Rename the key to '
62+
+ '`timeoutMs`; the value (milliseconds) is unchanged. '
63+
+ 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.';
64+
4965
export const TursoConfigSchema = lazySchema(() => z.object({
5066
/**
5167
* Database URL.
@@ -100,8 +116,34 @@ export const TursoConfigSchema = lazySchema(() => z.object({
100116

101117
/**
102118
* Timeout for database operations in milliseconds.
119+
*
120+
* Renamed from `timeout` (#15682, ruling B on #14478): the unit lived only in
121+
* the describe prose, while `sync.intervalSeconds` — the same shape, three
122+
* keys above — already spelled ITS unit. One published connection config
123+
* carrying both conventions is what made the bare name dangerous rather than
124+
* untidy. `@objectstack/spec`'s own turso contract renamed the same authored
125+
* key in #15680; this mirror now agrees with it, and with the ADR-0087
126+
* conversion (`turso-config-timeout-to-timeout-ms`) that rewrites the stored
127+
* spelling on load.
128+
*/
129+
timeoutMs: z.number().int().min(0).optional().describe('Operation timeout in milliseconds'),
130+
131+
/**
132+
* Tombstone for the rename above (#15682, ruling B on #14478).
133+
*
134+
* This is a plain `z.object`, so zod's default STRIP posture would make a
135+
* bare deletion SILENT: an author's `timeout: 30000` would vanish and the
136+
* parse would still succeed. `z.never()` keeps the key DECLARED and
137+
* unwritable, so the old spelling raises the prescription instead of
138+
* disappearing — the two channels an upgrading author actually meets (`tsc`
139+
* sees `never` at the authoring site; the parse raises the text itself).
140+
*
141+
* Spelled inline rather than through `@objectstack/spec`'s `retiredKey()`:
142+
* that helper is internal to the spec package and is not on its published
143+
* `./shared` entry point, so this package cannot import it. The shape is the
144+
* same one-liner.
103145
*/
104-
timeout: z.number().int().min(0).optional().describe('Operation timeout in milliseconds'),
146+
timeout: z.never({ error: () => TIMEOUT_RETIRED }).optional().describe(`[REMOVED] ${TIMEOUT_RETIRED}`),
105147

106148
/**
107149
* Enable WASM mode.

packages/spec/src/conversions/registry.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8945,10 +8945,15 @@ const memoryPersistenceAutoSaveIntervalToMs: MetadataConversion = {
89458945
* The neighbour is why this key was worth the rename: `sync.intervalSeconds`,
89468946
* two keys above, already spelled ITS unit. One shape, both conventions.
89478947
*
8948-
* ⚠️ This converts the SPEC's turso contract. The driver package ships its own
8949-
* parallel `turso.zod.ts` whose `timeout` is outside this card's declared
8950-
* population; it is renamed by the card that widens that population, and until
8951-
* then the two declarations disagree by design.
8948+
* ⚠️ This converts the SPEC's turso contract. `@objectstack/driver-turso` ships
8949+
* its own parallel `turso.zod.ts` declaring the same authored key, which was
8950+
* outside this gate's declared population when this entry was written. #15682
8951+
* widened that population to every workspace package's zod schemas and renamed
8952+
* the mirror in the same PR, so both declarations now spell `timeoutMs` and
8953+
* this ONE conversion covers the authored surface for both. The mirror
8954+
* registers no second entry: it would restate the same rewrite in
8955+
* `spec-changes.json` and the upgrade guide without converting anything the
8956+
* rename above has not already converted.
89528957
*/
89538958
const tursoConfigTimeoutToTimeoutMs: MetadataConversion = {
89548959
id: 'turso-config-timeout-to-timeout-ms',

0 commit comments

Comments
 (0)