Skip to content

Commit c5c2ea9

Browse files
authored
feat(core): add shard-routable run-ops id format and resolveShard (#4750)
## Summary Adds a second generation of run-ops id, plus the resolver that reads a store key straight out of an id. A gen-2 id keeps the existing 26-character layout, but the character at index 24 becomes a routing shard key instead of a region code, and the version character at index 25 becomes `"2"`. Nothing mints gen-2 ids yet, so this is inert on merge. ## Design The version character is a single character, so the gen-1 and gen-2 shape checks can never both match. That is what makes the two generations provably disjoint rather than disjoint by convention. ```ts resolveShard(id) // gen-2 body -> its shard key, [a-z0-9] // gen-1 v1 body -> "new" // anything else -> "legacy" ``` `resolveShard` is total: it returns a key for any input string, including an empty or malformed one, and never throws. `classifyResidency` keeps its signature and its two values, and now reports gen-2 ids as part of the dedicated family, so existing consumers of that boolean are unaffected. The body stays 26 characters rather than 27 deliberately. The older 27-character format is still in the wild and has to keep resolving to legacy, and a longer gen-2 shape would need probabilistic disambiguation against it. A rare misroute is not an acceptable property for a routing key. The one behavior change is that a 26-character body ending in `"2"` now routes by its shard key instead of falling back to legacy. Two test assertions pinned the old result and are updated here. A repository-wide search confirms they are the only two of their kind. Verified against the full run-store corpus (68 files, 370 tests) with no test-file changes there, plus the run-engine residency and waitpoint suites. No changeset: the new surface has no caller, so a version bump would tell a user nothing.
1 parent 60d71da commit c5c2ea9

4 files changed

Lines changed: 380 additions & 25 deletions

File tree

packages/core/src/v3/isomorphic/friendlyId.test.ts

Lines changed: 161 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,22 @@ import {
77
WebhookDeliveryId,
88
RUN_OPS_ID_LENGTH,
99
RUN_OPS_ID_REGION_INDEX,
10+
RUN_OPS_ID_SHARD_INDEX,
1011
RUN_OPS_ID_VERSION,
12+
RUN_OPS_ID_VERSION_2,
1113
RUN_OPS_ID_VERSION_INDEX,
1214
base32hexDecode,
1315
base32hexEncode,
1416
generateRunOpsId,
17+
generateRunOpsIdV2,
1518
parseRunId,
19+
parseRunOpsIdBody,
20+
parseRunOpsIdV2Body,
1621
} from "./friendlyId.js";
1722

23+
/** Every legal gen-2 shard char: the full DNS-safe lowercase range. */
24+
const SHARD_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789".split("");
25+
1826
const CUID_LEN = 25;
1927

2028
describe("RunId + WaitpointId mint cuid by default; run-ops v1 via generateRunOpsId", () => {
@@ -173,7 +181,8 @@ describe("parseRunId — version-char discrimination (not length)", () => {
173181

174182
it("falls back to legacy on a malformed v1 (bad alphabet / wrong version char)", () => {
175183
expect(parseRunId(`run_${"A".repeat(25)}1`).format).toBe("legacy"); // uppercase core
176-
expect(parseRunId(`run_${"a".repeat(25)}2`).format).toBe("legacy"); // wrong version
184+
expect(parseRunId(`run_${"a".repeat(25)}9`).format).toBe("legacy"); // unallocated version
185+
expect(parseRunId(`run_${"a".repeat(25)}2`).format).toBe("b32hexV2"); // "2" is now gen-2
177186
expect(parseRunId(`run_${"a".repeat(24)}-1`).format).toBe("legacy"); // region char not [a-z0-9]
178187
expect(parseRunId(`run_${"a".repeat(27)}`).format).toBe("legacy"); // old 27-char shape
179188
});
@@ -250,3 +259,154 @@ describe("WebhookDeliveryId (time-encoded)", () => {
250259
expect(WebhookDeliveryId.parseTimestamp(`whd_${"0".repeat(24)}9`)).toBeUndefined();
251260
});
252261
});
262+
263+
describe("generateRunOpsIdV2 — gen-2 id spec (shard char at 24, version '2' at 25)", () => {
264+
afterEach(() => vi.useRealTimers());
265+
266+
it("emits <24-char base32hex core><shard char><version '2'> — 26 chars total", () => {
267+
const id = generateRunOpsIdV2("a");
268+
expect(id.length).toBe(RUN_OPS_ID_LENGTH);
269+
expect(id).toMatch(/^[0-9a-v]{24}[a-z0-9]2$/);
270+
expect(id[RUN_OPS_ID_VERSION_INDEX]).toBe(RUN_OPS_ID_VERSION_2);
271+
expect(id[RUN_OPS_ID_SHARD_INDEX]).toBe("a");
272+
});
273+
274+
it("round-trips every legal shard char [a-z0-9] through parseRunOpsIdV2Body", () => {
275+
for (const c of SHARD_CHARS) {
276+
const id = generateRunOpsIdV2(c);
277+
const parsed = parseRunOpsIdV2Body(id);
278+
expect(parsed).toBeDefined();
279+
expect(parsed?.shard).toBe(c);
280+
expect(parsed?.version).toBe(RUN_OPS_ID_VERSION_2);
281+
// the core survives the round-trip: its bytes re-encode to the id's first 24 chars
282+
expect(base32hexEncode(base32hexDecode(id.slice(0, 24)))).toBe(id.slice(0, 24));
283+
}
284+
});
285+
286+
it("throws on a shard char outside [a-z0-9] (fail loud, never mint an unroutable id)", () => {
287+
for (const bad of ["", "-", "_", "A", "ab", " ", "/"]) {
288+
expect(() => generateRunOpsIdV2(bad)).toThrow(/shard/i);
289+
}
290+
});
291+
292+
it("only ever uses lowercase [a-z0-9] and NEVER '-' (DNS-1123 / pod-name invariant)", () => {
293+
for (let i = 0; i < 5_000; i++) {
294+
const id = generateRunOpsIdV2(SHARD_CHARS[i % SHARD_CHARS.length]!);
295+
expect(id).toMatch(/^[a-z0-9]+$/);
296+
expect(id).not.toContain("-");
297+
}
298+
});
299+
300+
it("sorts lexicographically in creation order at ms resolution, like gen-1", () => {
301+
vi.useFakeTimers();
302+
const t = new Date("2026-07-04T12:00:00.000Z").getTime();
303+
vi.setSystemTime(t);
304+
const a = generateRunOpsIdV2("a");
305+
vi.setSystemTime(t + 1000);
306+
const b = generateRunOpsIdV2("a");
307+
vi.setSystemTime(t + 3);
308+
const c = generateRunOpsIdV2("a");
309+
expect([b, c, a].sort()).toEqual([a, c, b]);
310+
});
311+
312+
it("decode recovers the exact ms timestamp", () => {
313+
vi.useFakeTimers();
314+
const t = new Date("2026-07-04T12:34:56.789Z");
315+
vi.setSystemTime(t);
316+
expect(parseRunOpsIdV2Body(generateRunOpsIdV2("e"))?.timestamp.getTime()).toBe(t.getTime());
317+
});
318+
319+
it("is unique across many mints in the same ms (72 bits of CSPRNG)", () => {
320+
vi.useFakeTimers();
321+
vi.setSystemTime(new Date("2026-07-04T00:00:00.000Z"));
322+
const n = 2_000;
323+
expect(new Set(Array.from({ length: n }, () => generateRunOpsIdV2("a"))).size).toBe(n);
324+
});
325+
});
326+
327+
describe("parseRunOpsIdV2Body — the mirror of the v1 shape check", () => {
328+
it("rejects a body that is not exactly 26 chars", () => {
329+
const core = "a".repeat(24);
330+
expect(parseRunOpsIdV2Body("")).toBeUndefined();
331+
expect(parseRunOpsIdV2Body(`${core}2`)).toBeUndefined(); // 25
332+
expect(parseRunOpsIdV2Body(`${core}ee2`)).toBeUndefined(); // 27
333+
expect(parseRunOpsIdV2Body("a".repeat(40))).toBeUndefined();
334+
});
335+
336+
it("rejects a body without '2' at index 25", () => {
337+
const core = "a".repeat(24);
338+
for (const version of ["1", "0", "3", "z", "-"]) {
339+
expect(parseRunOpsIdV2Body(`${core}e${version}`)).toBeUndefined();
340+
}
341+
});
342+
343+
it("rejects a body whose 24-char core is not base32hex", () => {
344+
for (const badCore of ["w".repeat(24), "z".repeat(24), "A".repeat(24), `${"a".repeat(23)}-`]) {
345+
expect(parseRunOpsIdV2Body(`${badCore}e2`)).toBeUndefined();
346+
}
347+
});
348+
349+
it("rejects a body whose char at index 24 is outside [a-z0-9]", () => {
350+
const core = "a".repeat(24);
351+
for (const badShard of ["-", "_", "A", ".", " "]) {
352+
expect(parseRunOpsIdV2Body(`${core}${badShard}2`)).toBeUndefined();
353+
}
354+
});
355+
356+
it("never throws, for any input string", () => {
357+
for (const input of ["", "x", "-".repeat(26), " ".repeat(26), "\u{1F642}".repeat(26)]) {
358+
expect(() => parseRunOpsIdV2Body(input)).not.toThrow();
359+
}
360+
});
361+
});
362+
363+
describe("gen-1 and gen-2 parsers reject each other (the disjointness foundation)", () => {
364+
it("parseRunOpsIdBody rejects every gen-2 id", () => {
365+
for (const c of SHARD_CHARS) {
366+
expect(parseRunOpsIdBody(generateRunOpsIdV2(c))).toBeUndefined();
367+
}
368+
});
369+
370+
it("parseRunOpsIdV2Body rejects every gen-1 v1 id", () => {
371+
for (const region of [undefined, "us-east-1", "us-west-2", "eu-central-1"]) {
372+
expect(parseRunOpsIdV2Body(generateRunOpsId(region))).toBeUndefined();
373+
}
374+
});
375+
376+
it("generateRunOpsId still mints v1 ids — the gen-1 generator is unchanged", () => {
377+
const id = generateRunOpsId("us-east-1");
378+
expect(id).toMatch(/^[0-9a-v]{24}[a-z0-9]1$/);
379+
expect(id[RUN_OPS_ID_VERSION_INDEX]).toBe(RUN_OPS_ID_VERSION);
380+
expect(parseRunOpsIdBody(id)?.region).toBe("e");
381+
});
382+
383+
it("the shard index and the region index are the same position", () => {
384+
expect(RUN_OPS_ID_SHARD_INDEX).toBe(RUN_OPS_ID_REGION_INDEX);
385+
});
386+
});
387+
388+
describe("parseRunId — v2 arm", () => {
389+
it("parses a gen-2 friendly id as partitioned with its shard + version", () => {
390+
const parsed = parseRunId(`run_${generateRunOpsIdV2("e")}`);
391+
expect(parsed).toMatchObject({
392+
format: "b32hexV2",
393+
table: "partitioned",
394+
shard: "e",
395+
version: "2",
396+
});
397+
});
398+
399+
it("still parses a gen-1 v1 friendly id as b32hex — the v1 arm is unchanged", () => {
400+
expect(parseRunId(`run_${generateRunOpsId("us-west-2")}`)).toMatchObject({
401+
format: "b32hex",
402+
table: "partitioned",
403+
region: "w",
404+
version: "1",
405+
});
406+
});
407+
408+
it("classifies a gen-2 body without the run_ prefix, and under a wrong prefix, legacy", () => {
409+
expect(parseRunId(generateRunOpsIdV2("a")).format).toBe("legacy");
410+
expect(parseRunId(`waitpoint_${generateRunOpsIdV2("a")}`).format).toBe("legacy");
411+
});
412+
});

packages/core/src/v3/isomorphic/friendlyId.ts

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ export const RUN_OPS_ID_LENGTH = 26;
2323
export const RUN_OPS_ID_REGION_INDEX = 24;
2424
export const RUN_OPS_ID_VERSION_INDEX = 25;
2525
export const RUN_OPS_ID_VERSION = "1";
26+
// Gen-2 id: same 26-char layout, but index 24 carries a routing SHARD KEY rather
27+
// than a region char. MUST stay 26 chars: a 27-char shape could collide with the
28+
// pre-cutover base62 format, which must keep classifying legacy.
29+
export const RUN_OPS_ID_VERSION_2 = "2";
30+
export const RUN_OPS_ID_SHARD_INDEX = RUN_OPS_ID_REGION_INDEX;
2631
const RUN_OPS_ID_CORE_BYTES = 15; // 6 timestamp + 9 random → exactly 24 base32hex chars
2732
const RUN_OPS_ID_CORE_LENGTH = 24;
2833
const RUN_OPS_ID_TIMESTAMP_BYTES = 6;
@@ -33,6 +38,8 @@ export const DEFAULT_REGION_CHAR = "0";
3338
// decoding), NOT part of the base32hex core — so it may use the full DNS-safe
3439
// lowercase [a-z0-9] range (e.g. "w" for us-west-2, which is outside [0-9a-v]).
3540
const REGION_CHAR_PATTERN = /^[a-z0-9]$/;
41+
// Same slot, same range: the gen-2 shard key is a region char's positional twin.
42+
const SHARD_CHAR_PATTERN = REGION_CHAR_PATTERN;
3643
/** One lowercase [a-z0-9] char per supported region, at RUN_OPS_ID_REGION_INDEX. */
3744
export const REGION_CODES: Readonly<Record<string, string>> = {
3845
"us-east-1": "e",
@@ -110,27 +117,47 @@ export function base32hexDecode(s: string): Uint8Array {
110117
return Uint8Array.from(out);
111118
}
112119

120+
// Shared by both generations. The buffer MUST be per-call — a hoisted one would
121+
// let concurrent mints overwrite each other's bytes.
122+
function mintRunOpsIdCore(): string {
123+
const core = new Uint8Array(RUN_OPS_ID_CORE_BYTES);
124+
125+
let ms = Date.now();
126+
for (let i = RUN_OPS_ID_TIMESTAMP_BYTES - 1; i >= 0; i--) {
127+
core[i] = ms % 256;
128+
ms = Math.floor(ms / 256);
129+
}
130+
getRandomValues(core.subarray(RUN_OPS_ID_TIMESTAMP_BYTES));
131+
132+
return base32hexEncode(core);
133+
}
134+
113135
/**
114136
* Mint a run-ops v1 id body (26 chars, no prefix): 24-char base32hex core
115137
* (6-byte ms timestamp + 9 CSPRNG bytes) + region char + version char "1".
116138
* The trailing version char at RUN_OPS_ID_VERSION_INDEX is the residency
117139
* discriminator — see runOpsResidency.ts.
118140
*/
119141
export function generateRunOpsId(region?: string): string {
120-
const core = new Uint8Array(RUN_OPS_ID_CORE_BYTES);
142+
return `${mintRunOpsIdCore()}${regionCharForRegion(region)}${RUN_OPS_ID_VERSION}`;
143+
}
121144

122-
let ms = Date.now();
123-
for (let i = RUN_OPS_ID_TIMESTAMP_BYTES - 1; i >= 0; i--) {
124-
core[i] = ms % 256;
125-
ms = Math.floor(ms / 256);
145+
/**
146+
* Mint a gen-2 id body (26 chars, no prefix): the same core, then the shard key,
147+
* then version char "2". Throws on a shard char outside [a-z0-9] — an id that
148+
* cannot be routed must never be minted.
149+
*/
150+
export function generateRunOpsIdV2(shardChar: string): string {
151+
if (!SHARD_CHAR_PATTERN.test(shardChar)) {
152+
throw new Error(`invalid run-ops shard char: ${JSON.stringify(shardChar)}`);
126153
}
127-
getRandomValues(core.subarray(RUN_OPS_ID_TIMESTAMP_BYTES));
128154

129-
return `${base32hexEncode(core)}${regionCharForRegion(region)}${RUN_OPS_ID_VERSION}`;
155+
return `${mintRunOpsIdCore()}${shardChar}${RUN_OPS_ID_VERSION_2}`;
130156
}
131157

132158
export type ParsedRunId =
133159
| { format: "b32hex"; table: "partitioned"; timestamp: Date; region: string; version: string }
160+
| { format: "b32hexV2"; table: "partitioned"; timestamp: Date; shard: string; version: string }
134161
| { format: "legacy"; table: "legacy" };
135162

136163
const LEGACY_RUN_ID: ParsedRunId = { format: "legacy", table: "legacy" };
@@ -149,6 +176,15 @@ export function parseRunOpsIdBody(
149176
const region = body[RUN_OPS_ID_REGION_INDEX] ?? "";
150177
if (!REGION_CHAR_PATTERN.test(region)) return undefined;
151178

179+
const timestamp = parseRunOpsIdCoreTimestamp(body);
180+
if (timestamp === undefined) return undefined;
181+
182+
return { timestamp, region, version: RUN_OPS_ID_VERSION };
183+
}
184+
185+
// Decode the leading 24-char core and recover its embedded ms timestamp.
186+
// Returns undefined (never throws) when the core is outside the base32hex alphabet.
187+
function parseRunOpsIdCoreTimestamp(body: string): Date | undefined {
152188
let core: Uint8Array;
153189
try {
154190
core = base32hexDecode(body.slice(0, RUN_OPS_ID_CORE_LENGTH));
@@ -161,19 +197,45 @@ export function parseRunOpsIdBody(
161197
ms = ms * 256 + (core[i] ?? 0);
162198
}
163199

164-
return { timestamp: new Date(ms), region, version: RUN_OPS_ID_VERSION };
200+
return new Date(ms);
201+
}
202+
203+
/**
204+
* Parse a gen-2 id body (no prefix): the mirror of {@link parseRunOpsIdBody},
205+
* requiring version "2" at index 25 and a shard key in [a-z0-9] at index 24.
206+
* Total: returns undefined for any other string, and never throws.
207+
*/
208+
export function parseRunOpsIdV2Body(
209+
body: string
210+
): { timestamp: Date; shard: string; version: string } | undefined {
211+
if (body.length !== RUN_OPS_ID_LENGTH) return undefined;
212+
if (body[RUN_OPS_ID_VERSION_INDEX] !== RUN_OPS_ID_VERSION_2) return undefined;
213+
const shard = body[RUN_OPS_ID_SHARD_INDEX] ?? "";
214+
if (!SHARD_CHAR_PATTERN.test(shard)) return undefined;
215+
216+
const timestamp = parseRunOpsIdCoreTimestamp(body);
217+
if (timestamp === undefined) return undefined;
218+
219+
return { timestamp, shard, version: RUN_OPS_ID_VERSION_2 };
165220
}
166221

167222
/** True if the (prefixless) id body is a well-formed run-ops v1 id. */
168223
export function isRunOpsIdBody(body: string): boolean {
169224
return parseRunOpsIdBody(body) !== undefined;
170225
}
171226

172-
/** Parse a `run_`-prefixed friendly id; anything not a well-formed v1 id is legacy. */
227+
/** Parse a `run_`-prefixed friendly id; anything not a well-formed v1/gen-2 id is legacy. */
173228
export function parseRunId(id: string): ParsedRunId {
174229
if (!id.startsWith("run_")) return LEGACY_RUN_ID;
175-
const parsed = parseRunOpsIdBody(id.slice(4));
176-
return parsed ? { format: "b32hex", table: "partitioned", ...parsed } : LEGACY_RUN_ID;
230+
const body = id.slice(4);
231+
232+
const v1 = parseRunOpsIdBody(body);
233+
if (v1) return { format: "b32hex", table: "partitioned", ...v1 };
234+
235+
const v2 = parseRunOpsIdV2Body(body);
236+
if (v2) return { format: "b32hexV2", table: "partitioned", ...v2 };
237+
238+
return LEGACY_RUN_ID;
177239
}
178240

179241
export function generateInternalId(): string {

0 commit comments

Comments
 (0)