Skip to content

Commit 5dcfa45

Browse files
d-csclaude
andcommitted
feat(core): residency classifier ownerEngine(id) for run-ops split
Add a pure, isomorphic length-disjoint residency classifier in packages/core/src/v3/isomorphic/runOpsResidency.ts. It classifies a run-ops id (RunId or WaitpointId, friendly or internal) by internal id length: cuid-length (25) -> "LEGACY", ksuid-length (27) -> "NEW", and throws UnclassifiableRunId on any other length. Exports ownerEngine / classifyResidency (same fn), classifyKind, isClassifiable, UnclassifiableRunId, and the CUID_LENGTH/KSUID_LENGTH constants. No dependency on friendlyId.ts (length-only); rides solely on the 25-vs-27 margin. Adds a property/disjointness vitest suite driving real cuid + ksuid mints. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 85b1af4 commit 5dcfa45

4 files changed

Lines changed: 133 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@trigger.dev/core": patch
3+
---
4+
5+
Add residency classifier `ownerEngine(id)` for the run-ops DB split (length-disjoint cuid→LEGACY / ksuid→NEW; throws on ambiguity).

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 W0-FND-01 flag state
12+
13+
const SAMPLES = 50_000; // property-scale; CI-fast. (Bump locally toward "millions" per DoD.)
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+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/** The two run-ops stores a run/waitpoint can reside in. */
2+
export type Residency = "LEGACY" | "NEW";
3+
4+
/** Underlying id format. cuid → LEGACY store, ksuid → NEW store. */
5+
export type ResidencyKind = "cuid" | "ksuid";
6+
7+
/** @bugsnag/cuid emits 25-char ids (W0-FND-01: cuid path, flag OFF). */
8+
export const CUID_LENGTH = 25;
9+
/** KSUID / nanoid-27 emits 27-char ids (W0-FND-01: ksuid path, flag ON). */
10+
export const KSUID_LENGTH = 27;
11+
12+
/** Thrown when an id length matches neither the cuid nor the ksuid margin. */
13+
export class UnclassifiableRunId extends Error {
14+
readonly value: string;
15+
readonly valueLength: number;
16+
constructor(value: string) {
17+
super(
18+
`Unclassifiable run-ops id: length ${value.length} matches neither cuid (${CUID_LENGTH}) nor ksuid (${KSUID_LENGTH}) — value=${JSON.stringify(
19+
value
20+
)}`
21+
);
22+
this.name = "UnclassifiableRunId";
23+
this.value = value;
24+
this.valueLength = value.length;
25+
}
26+
}
27+
28+
/**
29+
* Strip a single leading `<prefix>_` (e.g. `run_`, `waitpoint_`) if present,
30+
* so friendly and internal forms classify identically. Only the FIRST
31+
* underscore is treated as the prefix separator (mirrors fromFriendlyId's
32+
* two-part split contract in friendlyId.ts), without importing it.
33+
*/
34+
function internalForm(id: string): string {
35+
const underscore = id.indexOf("_");
36+
return underscore === -1 ? id : id.slice(underscore + 1);
37+
}
38+
39+
/** Returns the underlying id FORMAT (cuid|ksuid), or throws if unclassifiable. */
40+
export function classifyKind(id: string): ResidencyKind {
41+
const internal = internalForm(id);
42+
if (internal.length === CUID_LENGTH) return "cuid";
43+
if (internal.length === KSUID_LENGTH) return "ksuid";
44+
throw new UnclassifiableRunId(id);
45+
}
46+
47+
/** Non-throwing predicate: is this id length one we can classify? */
48+
export function isClassifiable(id: string): boolean {
49+
const len = internalForm(id).length;
50+
return len === CUID_LENGTH || len === KSUID_LENGTH;
51+
}
52+
53+
/** Map an id to its owning run-ops store by length. Throws on ambiguity. */
54+
export function classifyResidency(id: string): Residency {
55+
return classifyKind(id) === "ksuid" ? "NEW" : "LEGACY";
56+
}
57+
58+
/** Primary public name (doc 04 / RoutingRunStore / cross-seam guard). */
59+
export const ownerEngine = classifyResidency;

0 commit comments

Comments
 (0)