Skip to content

Commit eaf82d3

Browse files
d-csclaude
andcommitted
feat(run-ops): core residency classifier + ksuid mint primitives
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 3af1d22 commit eaf82d3

5 files changed

Lines changed: 412 additions & 5 deletions

File tree

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { afterEach, describe, expect, it, vi } from "vitest";
2+
import {
3+
RunId,
4+
WaitpointId,
5+
SnapshotId,
6+
QueueId,
7+
setKsuidMintEnabled,
8+
isKsuidMintEnabled,
9+
generateKsuidId,
10+
decodeKsuid,
11+
KSUID_PAYLOAD_BYTES,
12+
} from "./friendlyId.js";
13+
14+
const CUID_LEN = 25;
15+
const KSUID_LEN = 27;
16+
17+
afterEach(() => setKsuidMintEnabled(false)); // never leak flag state across tests
18+
19+
describe("KSUID mint (flag-gated) for RunId + WaitpointId", () => {
20+
it("flag OFF: run + waitpoint mint cuid (25) and round-trip", () => {
21+
setKsuidMintEnabled(false);
22+
expect(isKsuidMintEnabled()).toBe(false);
23+
for (const util of [RunId, WaitpointId]) {
24+
const { id, friendlyId } = util.generate();
25+
expect(id.length).toBe(CUID_LEN);
26+
expect(util.fromFriendlyId(friendlyId)).toBe(id);
27+
expect(util.toId(friendlyId)).toBe(id);
28+
expect(util.toId(id)).toBe(id);
29+
expect(util.toFriendlyId(id)).toBe(friendlyId);
30+
}
31+
});
32+
33+
it("flag ON: run + waitpoint mint 27-char and round-trip", () => {
34+
setKsuidMintEnabled(true);
35+
expect(isKsuidMintEnabled()).toBe(true);
36+
for (const util of [RunId, WaitpointId]) {
37+
const { id, friendlyId } = util.generate();
38+
expect(id.length).toBe(KSUID_LEN);
39+
expect(util.fromFriendlyId(friendlyId)).toBe(id);
40+
expect(util.toId(friendlyId)).toBe(id);
41+
expect(util.toId(id)).toBe(id);
42+
expect(util.toFriendlyId(id)).toBe(friendlyId);
43+
}
44+
});
45+
46+
it("flag ON: SnapshotId stays cuid (25) — never 27-char", () => {
47+
setKsuidMintEnabled(true);
48+
const { id } = SnapshotId.generate();
49+
expect(id.length).toBe(CUID_LEN);
50+
});
51+
52+
it("flag ON: a non-run/waitpoint entity stays cuid (25)", () => {
53+
setKsuidMintEnabled(true);
54+
expect(QueueId.generate().id.length).toBe(CUID_LEN);
55+
});
56+
57+
it("disjoint lengths: 27 (new) vs 25 (cuid) — the classifier margin", () => {
58+
setKsuidMintEnabled(true);
59+
expect(RunId.generate().id.length).not.toBe(SnapshotId.generate().id.length);
60+
});
61+
62+
it("generateKsuidId() is directly callable, independent of the flag", () => {
63+
setKsuidMintEnabled(false); // must NOT be gated behind the global flag
64+
expect(generateKsuidId().length).toBe(KSUID_LEN);
65+
});
66+
});
67+
68+
describe("generateKsuidId is a genuine KSUID (decodable timestamp, time-ordered)", () => {
69+
const BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
70+
const KSUID_EPOCH = 1_400_000_000;
71+
72+
// Decode the 27-char base62 body back to the 4-byte timestamp prefix (unix seconds).
73+
function decodeTimestamp(id: string): number {
74+
let n = 0n;
75+
for (const ch of id) n = n * 62n + BigInt(BASE62.indexOf(ch));
76+
return Number(n >> 128n) + KSUID_EPOCH; // top 4 of the 20 bytes
77+
}
78+
79+
afterEach(() => vi.useRealTimers());
80+
81+
it("is exactly 27 base62 chars", () => {
82+
expect(generateKsuidId()).toMatch(/^[0-9A-Za-z]{27}$/);
83+
});
84+
85+
it("carries a decodable timestamp within a few seconds of now", () => {
86+
const before = Math.floor(Date.now() / 1000);
87+
const ts = decodeTimestamp(generateKsuidId());
88+
expect(ts).toBeGreaterThanOrEqual(before - 2);
89+
expect(ts).toBeLessThanOrEqual(Math.floor(Date.now() / 1000) + 2);
90+
});
91+
92+
it("is k-sortable: ids from later seconds sort lexicographically after earlier ones", () => {
93+
vi.useFakeTimers();
94+
const ids: string[] = [];
95+
for (const t of ["2026-01-01T00:00:00Z", "2026-01-01T00:05:00Z", "2026-09-01T12:00:00Z"]) {
96+
vi.setSystemTime(new Date(t));
97+
ids.push(generateKsuidId());
98+
}
99+
expect([...ids].sort()).toEqual(ids);
100+
});
101+
102+
it("is unique across many mints in the same second", () => {
103+
const n = 1000;
104+
expect(new Set(Array.from({ length: n }, generateKsuidId)).size).toBe(n);
105+
});
106+
});
107+
108+
describe("KSUID payload encode/decode (foundation primitive)", () => {
109+
it("round-trips a full 16-byte payload exactly", () => {
110+
const payload = new Uint8Array(KSUID_PAYLOAD_BYTES).map((_, i) => (i * 17 + 1) & 0xff);
111+
const { payload: decoded } = decodeKsuid(generateKsuidId(payload));
112+
expect(Array.from(decoded)).toEqual(Array.from(payload));
113+
});
114+
115+
it("preserves a partial payload prefix and keeps the remainder for entropy", () => {
116+
const meta = new Uint8Array([9, 8, 7, 6]);
117+
const { payload } = decodeKsuid(generateKsuidId(meta));
118+
expect(Array.from(payload.slice(0, 4))).toEqual([9, 8, 7, 6]);
119+
expect(payload.length).toBe(KSUID_PAYLOAD_BYTES);
120+
});
121+
122+
it("still carries a decodable timestamp when a payload is embedded", () => {
123+
const before = Math.floor(Date.now() / 1000);
124+
const { timestampSeconds } = decodeKsuid(generateKsuidId(new Uint8Array([1, 2, 3])));
125+
expect(timestampSeconds).toBeGreaterThanOrEqual(before - 2);
126+
expect(timestampSeconds).toBeLessThanOrEqual(Math.floor(Date.now() / 1000) + 2);
127+
});
128+
129+
it("stays 27 chars with a full payload and decodes through a friendlyId prefix", () => {
130+
const id = generateKsuidId(new Uint8Array(KSUID_PAYLOAD_BYTES).fill(0xab));
131+
expect(id).toMatch(/^[0-9A-Za-z]{27}$/);
132+
expect(Array.from(decodeKsuid(`run_${id}`).payload)).toEqual(
133+
new Array(KSUID_PAYLOAD_BYTES).fill(0xab)
134+
);
135+
});
136+
137+
it("throws if the payload exceeds the 16-byte budget", () => {
138+
expect(() => generateKsuidId(new Uint8Array(KSUID_PAYLOAD_BYTES + 1))).toThrow();
139+
});
140+
141+
it("decodeKsuid rejects a body that is not 27 base62 chars", () => {
142+
expect(() => decodeKsuid("run_tooShort")).toThrow();
143+
});
144+
});

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

Lines changed: 140 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,139 @@ export function generateFriendlyId(prefix: string, size?: number) {
77
return `${prefix}_${idGenerator(size)}`;
88
}
99

10-
export function generateInternalId() {
10+
// Injected by the server split-mode bootstrap. Default false =
11+
// today's behavior (cuid). Isomorphic file: never read process.env here.
12+
let ksuidMintEnabled = false;
13+
14+
/** Server bootstrap calls this once with isSplitEnabled(). Off by default. */
15+
export function setKsuidMintEnabled(enabled: boolean): void {
16+
ksuidMintEnabled = enabled;
17+
}
18+
19+
/** Test/diagnostic read-back of the current mint mode. */
20+
export function isKsuidMintEnabled(): boolean {
21+
return ksuidMintEnabled;
22+
}
23+
24+
// KSUID epoch (2014-05-13T16:53:20Z) — seconds offset applied to the unix timestamp.
25+
const KSUID_EPOCH = 1_400_000_000;
26+
const KSUID_TIMESTAMP_BYTES = 4;
27+
export const KSUID_PAYLOAD_BYTES = 16;
28+
const KSUID_TOTAL_BYTES = KSUID_TIMESTAMP_BYTES + KSUID_PAYLOAD_BYTES;
29+
const KSUID_STRING_LENGTH = 27;
30+
const BASE62_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
31+
32+
/** Encode raw bytes as base62 (big-endian), left-padded to the given length. */
33+
function base62Encode(bytes: Uint8Array, length: number): string {
34+
const digits = Array.from(bytes);
35+
let result = "";
36+
37+
while (digits.length > 0) {
38+
let remainder = 0;
39+
const quotient: number[] = [];
40+
41+
for (let i = 0; i < digits.length; i++) {
42+
const acc = (digits[i] ?? 0) + remainder * 256;
43+
const q = Math.floor(acc / 62);
44+
remainder = acc % 62;
45+
46+
if (quotient.length > 0 || q > 0) {
47+
quotient.push(q);
48+
}
49+
}
50+
51+
result = BASE62_ALPHABET.charAt(remainder) + result;
52+
digits.length = 0;
53+
digits.push(...quotient);
54+
}
55+
56+
return result.padStart(length, BASE62_ALPHABET.charAt(0));
57+
}
58+
59+
/**
60+
* 27-char, base62, time-ordered KSUID body (length-disjoint from the 25-char cuid): a 4-byte
61+
* timestamp (seconds since the KSUID epoch) + a 16-byte payload; ids from different seconds
62+
* sort in mint order. Payload defaults to CSPRNG entropy; callers may supply up to
63+
* KSUID_PAYLOAD_BYTES metadata bytes (written first, remainder stays random for uniqueness).
64+
*/
65+
export function generateKsuidId(payload?: Uint8Array): string {
66+
const bytes = new Uint8Array(KSUID_TOTAL_BYTES);
67+
68+
const timestamp = Math.floor(Date.now() / 1000) - KSUID_EPOCH;
69+
bytes[0] = (timestamp >>> 24) & 0xff;
70+
bytes[1] = (timestamp >>> 16) & 0xff;
71+
bytes[2] = (timestamp >>> 8) & 0xff;
72+
bytes[3] = timestamp & 0xff;
73+
74+
if (payload && payload.length > KSUID_PAYLOAD_BYTES) {
75+
throw new Error(
76+
`KSUID payload must be at most ${KSUID_PAYLOAD_BYTES} bytes (got ${payload.length})`
77+
);
78+
}
79+
const reserved = payload?.length ?? 0;
80+
if (payload && reserved > 0) {
81+
bytes.set(payload, KSUID_TIMESTAMP_BYTES);
82+
}
83+
if (reserved < KSUID_PAYLOAD_BYTES) {
84+
globalThis.crypto.getRandomValues(bytes.subarray(KSUID_TIMESTAMP_BYTES + reserved));
85+
}
86+
87+
return base62Encode(bytes, KSUID_STRING_LENGTH);
88+
}
89+
90+
/** Decoded parts of a KSUID body: its mint timestamp and 16-byte payload. */
91+
export interface DecodedKsuid {
92+
timestampSeconds: number;
93+
timestamp: Date;
94+
payload: Uint8Array;
95+
}
96+
97+
/**
98+
* Decode a KSUID body (or a `prefix_<body>` friendly id) into its timestamp + 16-byte payload.
99+
* The inverse of generateKsuidId's layout. Throws if the body is not 27 base62 chars.
100+
*/
101+
export function decodeKsuid(idOrFriendlyId: string): DecodedKsuid {
102+
const underscore = idOrFriendlyId.indexOf("_");
103+
const body = underscore === -1 ? idOrFriendlyId : idOrFriendlyId.slice(underscore + 1);
104+
if (body.length !== KSUID_STRING_LENGTH) {
105+
throw new Error(
106+
`Not a KSUID body: expected ${KSUID_STRING_LENGTH} base62 chars, got ${body.length}`
107+
);
108+
}
109+
110+
let n = 0n;
111+
for (const ch of body) {
112+
const digit = BASE62_ALPHABET.indexOf(ch);
113+
if (digit < 0) {
114+
throw new Error(`Invalid base62 character in KSUID body: ${ch}`);
115+
}
116+
n = n * 62n + BigInt(digit);
117+
}
118+
119+
const bytes = new Uint8Array(KSUID_TOTAL_BYTES);
120+
for (let i = KSUID_TOTAL_BYTES - 1; i >= 0; i--) {
121+
bytes[i] = Number(n & 0xffn);
122+
n >>= 8n;
123+
}
124+
125+
const timestampSeconds =
126+
(bytes[0] ?? 0) * 0x1000000 +
127+
(bytes[1] ?? 0) * 0x10000 +
128+
(bytes[2] ?? 0) * 0x100 +
129+
(bytes[3] ?? 0) +
130+
KSUID_EPOCH;
131+
132+
return {
133+
timestampSeconds,
134+
timestamp: new Date(timestampSeconds * 1000),
135+
payload: bytes.slice(KSUID_TIMESTAMP_BYTES),
136+
};
137+
}
138+
139+
export function generateInternalId(useKsuidWhenEnabled = false): string {
140+
if (useKsuidWhenEnabled && ksuidMintEnabled) {
141+
return generateKsuidId();
142+
}
11143
return cuid();
12144
}
13145

@@ -58,10 +190,13 @@ export function fromFriendlyId(friendlyId: string, expectedEntityName?: string):
58190
}
59191

60192
export class IdUtil {
61-
constructor(private entityName: string) {}
193+
constructor(
194+
private entityName: string,
195+
private ksuidWhenEnabled = false
196+
) {}
62197

63198
generate() {
64-
const internalId = generateInternalId();
199+
const internalId = generateInternalId(this.ksuidWhenEnabled);
65200

66201
return {
67202
id: internalId,
@@ -90,9 +225,9 @@ export class IdUtil {
90225
export const BackgroundWorkerId = new IdUtil("worker");
91226
export const CheckpointId = new IdUtil("checkpoint");
92227
export const QueueId = new IdUtil("queue");
93-
export const RunId = new IdUtil("run");
228+
export const RunId = new IdUtil("run", true);
94229
export const SnapshotId = new IdUtil("snapshot");
95-
export const WaitpointId = new IdUtil("waitpoint");
230+
export const WaitpointId = new IdUtil("waitpoint", true);
96231
export const BatchId = new IdUtil("batch");
97232
export const BulkActionId = new IdUtil("bulk");
98233
export const AttemptId = new IdUtil("attempt");

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from "./friendlyId.js";
2+
export * from "./runOpsResidency.js";
23
export * from "./duration.js";
34
export * from "./maxDuration.js";
45
export * from "./queueName.js";
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { afterEach, describe, expect, it } from "vitest";
2+
import { RunId, WaitpointId, SnapshotId, setKsuidMintEnabled } from "./friendlyId.js";
3+
import {
4+
ownerEngine,
5+
classifyResidency,
6+
classifyKind,
7+
isClassifiable,
8+
UnclassifiableRunId,
9+
} from "./runOpsResidency.js";
10+
11+
afterEach(() => setKsuidMintEnabled(false)); // never leak mint-flag state across tests
12+
13+
const SAMPLES = 50_000; // property-scale; CI-fast. (Bump locally toward "millions" for deeper coverage.)
14+
15+
describe("ownerEngine — residency classifier", () => {
16+
it("cuid-length ids (mint flag OFF) classify LEGACY, friendly + internal", () => {
17+
setKsuidMintEnabled(false);
18+
for (const util of [RunId, WaitpointId]) {
19+
const { id, friendlyId } = util.generate();
20+
expect(ownerEngine(id)).toBe("LEGACY");
21+
expect(ownerEngine(friendlyId)).toBe("LEGACY"); // strips run_/waitpoint_ prefix
22+
expect(classifyResidency(id)).toBe("LEGACY"); // alias agrees
23+
expect(classifyKind(id)).toBe("cuid");
24+
expect(isClassifiable(id)).toBe(true);
25+
}
26+
});
27+
28+
it("ksuid-length ids (mint flag ON) classify NEW, friendly + internal", () => {
29+
setKsuidMintEnabled(true);
30+
for (const util of [RunId, WaitpointId]) {
31+
const { id, friendlyId } = util.generate();
32+
expect(ownerEngine(id)).toBe("NEW");
33+
expect(ownerEngine(friendlyId)).toBe("NEW");
34+
expect(classifyResidency(id)).toBe("NEW");
35+
expect(classifyKind(id)).toBe("ksuid");
36+
}
37+
});
38+
39+
it("disjointness: no cuid sample is ever NEW, no ksuid sample is ever LEGACY", () => {
40+
for (let i = 0; i < SAMPLES; i++) {
41+
setKsuidMintEnabled(false);
42+
expect(ownerEngine(RunId.generate().id)).toBe("LEGACY");
43+
setKsuidMintEnabled(true);
44+
expect(ownerEngine(RunId.generate().id)).toBe("NEW");
45+
}
46+
});
47+
48+
it("throws UnclassifiableRunId on malformed lengths (24, 26, 28, empty)", () => {
49+
for (const bad of ["", "x".repeat(24), "x".repeat(26), "x".repeat(28), "x".repeat(40)]) {
50+
expect(() => ownerEngine(bad)).toThrow(UnclassifiableRunId);
51+
}
52+
});
53+
54+
it("error carries the offending value + length for diagnostics", () => {
55+
try {
56+
ownerEngine("x".repeat(26));
57+
throw new Error("should have thrown");
58+
} catch (e) {
59+
expect(e).toBeInstanceOf(UnclassifiableRunId);
60+
expect((e as UnclassifiableRunId).message).toContain("26");
61+
}
62+
});
63+
64+
it("SnapshotId (always cuid, even with flag ON) classifies LEGACY — proves snapshot needs no residency key", () => {
65+
setKsuidMintEnabled(true);
66+
expect(ownerEngine(SnapshotId.generate().id)).toBe("LEGACY");
67+
});
68+
});

0 commit comments

Comments
 (0)