Skip to content

Commit deba8a1

Browse files
os-salesclaude
andcommitted
wip(spec): rename the seven duration keys, conversions, readers, pins (#14478)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
1 parent 7e0ba72 commit deba8a1

46 files changed

Lines changed: 871 additions & 84 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec)!: a duration-shaped `z.number()` key carries its unit in the key name — `hook.timeout` / `job.timeout` / `DriverOptions.timeout``timeoutMs`, `MetadataManagerConfig.cache.ttl``ttlSeconds`, `cache.databaseLoader.ttl``ttlMs`, tenant `idleTimeout` / `sessionTimeout``*Seconds`; new gate `check:duration-unit-keys` (#14478, #14519)
6+
7+
<!-- adr-0087: registered hook-timeout-to-timeout-ms, job-timeout-to-timeout-ms, metadata-manager-config-cache-ttl-unit-in-key, driver-options-timeout-to-timeout-ms, tenant-timeouts-unit-in-key -->
8+
9+
**BREAKING** rename of seven published authorable keys, shipped as `minor` under
10+
the repo's launch-window convention for breaking changes; every rename is
11+
registered under protocol major 18. Maintainer ruling 2026-09-02 on #14478
12+
(director decision batch #14, verbatim 「14461 你不处理,其他同意」): **ruled B**
13+
a spec-source gate for duration-shaped number keys **with no grandfathered
14+
baseline**, plus an ADR-0087 conversion of every offender the ruling named, in
15+
one PR, on the standing rules 「不考虑存量」 and 「项目在创业阶段,用户也很少,短期不考虑渐进。」.
16+
⛔ No alias, no transition window: each old spelling is a `retiredKey()`
17+
tombstone whose rejection names the new key.
18+
19+
## The defect
20+
21+
`kernel/metadata-loader.zod.ts` carried two keys spelled `ttl` fourteen lines
22+
apart: `cache.ttl` in **seconds** (default 3600) and `cache.databaseLoader.ttl`
23+
in **milliseconds** (default 60000). Both descriptions named their unit; the
24+
key names did not. An author who copied the outer number into the inner block
25+
got a 3.6-second cache and no error anywhere — the number was valid, the type
26+
was right, the cache was simply cold. `hook.timeout`, `job.timeout` and
27+
`DriverOptions.timeout` had the same shape (milliseconds, said only in prose)
28+
beside siblings that spell theirs (`backoffMs`, `intervalMs`, the body-level
29+
`timeoutMs`). The two tenant keys were worse for the reader who matters most:
30+
`.describe()` is what `content/docs/references/**` publishes and the JSDoc above
31+
a key is not, so `idleTimeout` / `sessionTimeout` said "in seconds" in a source
32+
comment and published a bare `300` / `3600` to the reference page (#14519).
33+
34+
## FROM → TO
35+
36+
| schema | before | after | value |
37+
|:--|:--|:--|:--|
38+
| `HookSchema` (`hooks[]`) | `timeout` | `timeoutMs` | unchanged (ms) |
39+
| `JobSchema` (`jobs[]`) | `timeout` | `timeoutMs` | unchanged (ms) |
40+
| `DriverOptionsSchema` | `timeout` | `timeoutMs` | unchanged (ms) |
41+
| `MetadataManagerConfigSchema` | `cache.ttl` | `cache.ttlSeconds` | unchanged (s, default 3600) |
42+
| `MetadataManagerConfigSchema` | `cache.databaseLoader.ttl` | `cache.databaseLoader.ttlMs` | unchanged (ms, default 60000) |
43+
| `DatabaseLevelIsolationStrategySchema` | `connectionPool.idleTimeout` | `connectionPool.idleTimeoutSeconds` | unchanged (s, default 300) |
44+
| `TenantSecurityPolicySchema` | `accessControl.sessionTimeout` | `accessControl.sessionTimeoutSeconds` | unchanged (s, default 3600) |
45+
46+
```ts
47+
// before
48+
defineHook({ name: 'audit_order', object: 'order', events: ['afterInsert'], handler: 'auditOrder', timeout: 5000 });
49+
defineJob({ name: 'nightly_sweep', schedule: { type: 'cron', expression: '0 1 * * *' }, handler: 'sweep', timeout: 300000 });
50+
new MetadataManager({ cache: { ttl: 3600, databaseLoader: { ttl: 60_000 } } });
51+
52+
// after — rename the key; the number is unchanged
53+
defineHook({ name: 'audit_order', object: 'order', events: ['afterInsert'], handler: 'auditOrder', timeoutMs: 5000 });
54+
defineJob({ name: 'nightly_sweep', schedule: { type: 'cron', expression: '0 1 * * *' }, handler: 'sweep', timeoutMs: 300000 });
55+
new MetadataManager({ cache: { ttlSeconds: 3600, databaseLoader: { ttlMs: 60_000 } } });
56+
```
57+
58+
**Migration.** Rename each key; no value changes. Authoring an old spelling
59+
fails to compile (`tsc`: the input type is `never`) and fails to parse with a
60+
prescription naming the new key. For `hooks[]` / `jobs[]` the rename is a
61+
mechanical D2 conversion (`hook-timeout-to-timeout-ms`,
62+
`job-timeout-to-timeout-ms`, retired from the load path): run
63+
`os migrate meta --from 17` to list the edits for existing sources and apply
64+
them by hand; stored `sys_metadata` rows are rehydrated through the same chain.
65+
The other five keys have no stack seam (runtime config, a per-call options
66+
argument, cloud tenancy config) and carry a semantic entry each. The
67+
`JobScheduleOptions` contract key that carries `job.timeoutMs` to the scheduler
68+
is renamed in lockstep (`timeout``timeoutMs`), as is `DatabaseLoaderOptions.cache.ttl``ttlMs` in `@objectstack/metadata`.
69+
70+
## The gate
71+
72+
`pnpm --filter @objectstack/spec check:duration-unit-keys`
73+
(`packages/spec/scripts/check-duration-unit-keys.ts`, wired into `lint.yml`):
74+
a property whose value is a `z.number()` / `z.int()` / `z.coerce.number()`
75+
chain and whose `.describe()` names a time unit must carry that unit as a token
76+
of its key name (`Ms` / `Seconds` / `Minutes` / `Hours` / `Days`, and the
77+
knex-inherited `Millis`), and the token must agree with the prose — `ttlMs`
78+
described "in seconds" is refused too. A `{ value, unit }` pair is recognised
79+
by its sibling `unit` key; duration literals are strings and outside the
80+
population. Calendar positions ("day of the month") and rates ("requests per
81+
second") are skipped. There is no baseline and no `gen:`; a red is a rename
82+
under an ADR-0087 conversion or a describe to fix.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
"@objectstack/metadata": minor
3+
---
4+
5+
feat(metadata)!: `DatabaseLoaderOptions.cache.ttl``cache.ttlMs` — the read-through cache TTL carries its unit in the key name (#14478)
6+
7+
<!-- adr-0087: not-required (already-registered metadata-manager-config-cache-ttl-unit-in-key) The authorable key this option mirrors — `MetadataManagerConfig.cache.databaseLoader.ttl` → `ttlMs` — is registered by the `@objectstack/spec` changeset of the same change; this package's exported `DatabaseLoaderCacheOptions` interface follows that key one-to-one and has no separate metadata surface to register. -->
8+
9+
**BREAKING** rename on the exported `DatabaseLoaderOptions.cache` shape
10+
(`DatabaseLoaderCacheOptions.ttl``ttlMs`), shipped as `minor` under the
11+
launch-window convention. `MetadataManager` hands `config.cache.databaseLoader`
12+
straight to `new DatabaseLoader({ cache })`, so this option is the spec key
13+
`cache.databaseLoader.ttlMs` one layer down and renames with it: a loader
14+
configured with `ttlMs: 60_000` expires entries after 60 seconds exactly as
15+
`ttl: 60_000` did. The README example and the kernel metadata-service docs page
16+
spell the new key.
17+
18+
```ts
19+
// before
20+
new DatabaseLoader({ driver, cache: { enabled: true, maxSize: 500, ttl: 60_000 } });
21+
// after
22+
new DatabaseLoader({ driver, cache: { enabled: true, maxSize: 500, ttlMs: 60_000 } });
23+
```
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@objectstack/objectql": patch
3+
---
4+
5+
fix(objectql): the declarative hook wrapper reads the renamed `hook.timeoutMs` (#14478)
6+
7+
`wrapDeclarativeHook` reads its wall-clock abort budget from `meta.timeoutMs`
8+
instead of `meta.timeout`, following the `@objectstack/spec` rename of the
9+
authored key (the unit now lives in the key name). Same value, same magnitude,
10+
same abort; no public surface of this package changes.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@objectstack/runtime": patch
3+
---
4+
5+
fix(runtime): `AppPlugin` threads the authored `job.timeoutMs` to the scheduler as `timeoutMs` (#14478)
6+
7+
The declarative job door passes `{ retryPolicy, timeoutMs }` to
8+
`IJobService.schedule`, following the `@objectstack/spec` rename of the
9+
authored key and of the `JobScheduleOptions` contract key that carries it. Same
10+
value, same per-attempt limit.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/service-job": patch
3+
---
4+
5+
fix(service-job): `runWithPolicy` and the DB job adapter read `JobScheduleOptions.timeoutMs` (#14478)
6+
7+
The per-attempt time limit is read from `options.timeoutMs`, following the
8+
`@objectstack/spec` rename of both the authored `job.timeoutMs` and the
9+
`JobScheduleOptions` contract key that carries it. Same value, same per-attempt
10+
race, same `JobTimeoutError`; `withoutPolicy` strips the renamed key so the
11+
timer adapter downstream never runs a second budget.

.github/workflows/lint.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3884,6 +3884,24 @@ jobs:
38843884
node packages/lint/scripts/check-reference-carrier-shape.mjs --self-test
38853885
node packages/lint/scripts/check-reference-carrier-shape.mjs
38863886
3887+
# #14478 (maintainer ruling 2026-09-02, "ruled B"): a duration-shaped
3888+
# `z.number()` key in packages/spec/src/** whose `.describe()` names a
3889+
# time unit must carry that unit in its NAME (`Ms` / `Seconds` /
3890+
# `Minutes` / `Hours` / `Days`), or the schema uses a unit-carrying
3891+
# VALUE (`'14d'` literals, `{ value, unit }` pairs). The founding
3892+
# specimen was two keys spelled `ttl` fourteen lines apart, one in
3893+
# seconds and one in milliseconds, each unit named only in prose — an
3894+
# author copying the outer number into the inner block got a 3.6-second
3895+
# cache and no error anywhere. No baseline, by ruling: the gate is green
3896+
# only at zero offenders, and a red is a rename under an ADR-0087
3897+
# conversion (or a describe to fix), never a ledger row. Placed LAST in
3898+
# this job on purpose: it is a new tree-wide ratchet, and the job aborts
3899+
# at the first non-zero exit — nothing behind it would be masked by its
3900+
# red. Reads src/ through tsx (no build); self-tests first, wired into
3901+
# the package script as `check:self-test-wired` requires.
3902+
- name: Duration-shaped spec keys carry their unit in the key name
3903+
run: pnpm --filter @objectstack/spec check:duration-unit-keys
3904+
38873905
# Lane 1 of 4 behind the required `TypeScript Type Check` context. The
38883906
# aggregator job at the bottom of this file explains the split, holds the
38893907
# contract, and is the thing the merge queue actually requires — read it

content/docs/automation/jobs.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export const HealthSweepJob = defineJob({
1818
schedule: { type: 'cron', expression: '0 1 * * *', timezone: 'UTC' },
1919
handler: 'sweepProjectHealth',
2020
retryPolicy: { maxRetries: 2, backoffMs: 5000, backoffMultiplier: 2 },
21-
timeout: 300000,
21+
timeoutMs: 300000,
2222
});
2323
```
2424

content/docs/protocol/kernel/metadata-service.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,14 +248,14 @@ parse-time error carrying the prescription.)
248248

249249
### 3. DatabaseLoader Read-Through Cache
250250

251-
`DatabaseLoader` wraps `load` / `loadMany` / `list` / `stat` results in a generic LRU cache (lazy TTL, promote-on-get, write invalidation). Reads always observe writes performed through the same loader instance; out-of-band SQL writes are honored within `ttl` milliseconds.
251+
`DatabaseLoader` wraps `load` / `loadMany` / `list` / `stat` results in a generic LRU cache (lazy TTL, promote-on-get, write invalidation). Reads always observe writes performed through the same loader instance; out-of-band SQL writes are honored within `ttlMs` milliseconds.
252252

253253
```typescript
254254
new MetadataManager({
255255
datasource: 'default',
256256
cache: {
257257
enabled: true,
258-
databaseLoader: { enabled: true, maxSize: 500, ttl: 60_000 },
258+
databaseLoader: { enabled: true, maxSize: 500, ttlMs: 60_000 },
259259
},
260260
});
261261
```

examples/app-showcase/src/automation/jobs/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export const HealthSweepJob = defineJob({
2020
schedule: { type: 'cron', expression: '0 1 * * *', timezone: 'UTC' },
2121
handler: 'sweepProjectHealth',
2222
retryPolicy: { maxRetries: 2, backoffMs: 5000, backoffMultiplier: 2 },
23-
timeout: 300000,
23+
timeoutMs: 300000,
2424
enabled: true,
2525
});
2626

packages/metadata/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ new MetadataManager({
165165
`DatabaseLoader` wraps `load` / `loadMany` / `list` / `stat` results in a
166166
generic LRU cache (see `src/utils/lru-cache.ts`). Writes invalidate the
167167
affected entries, so reads always observe writes made through the same loader
168-
instance; out-of-band SQL writes are honored within `ttl` milliseconds.
168+
instance; out-of-band SQL writes are honored within `ttlMs` milliseconds.
169169

170170
Configuration lives under `cache.databaseLoader`:
171171

@@ -177,7 +177,7 @@ new MetadataManager({
177177
databaseLoader: {
178178
enabled: true,
179179
maxSize: 500, // Max cached (type, name) entries
180-
ttl: 60_000, // Cache TTL in milliseconds
180+
ttlMs: 60_000, // Cache TTL in milliseconds
181181
},
182182
},
183183
});

0 commit comments

Comments
 (0)