@tokenoftrust/storefront-runner 4.12.0 → 4.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/storefront/src/lib/access/auth/internalContentAuthority.ts +1 -0
- package/apps/storefront/src/lib/access/auth/totAccessClient.ts +2 -0
- package/apps/storefront/src/lib/access/dashboard/staffVendorAccess.ts +8 -1
- package/apps/storefront/src/lib/access/shared/accessExplain.ts +2 -0
- package/apps/storefront/src/lib/access/shared/developerInvites.ts +12 -3
- package/apps/storefront/src/lib/access/shared/invitePreflight.ts +2 -1
- package/apps/storefront/src/lib/access/shared/principalGrants.ts +10 -3
- package/apps/storefront/test/pipeline/matrix-faults.ts +34 -12
- package/apps/storefront/test/pipeline/pipeline-host.ts +7 -0
- package/apps/storefront/test/pipeline/publish-kinds.ts +1 -1
- package/apps/storefront/test/pipeline/synthetic-job.ts +32 -0
- package/apps/storefront/test/pipeline-faults/matrix-lib.ts +7 -1
- package/package.json +1 -1
|
@@ -98,6 +98,7 @@ export async function authorizeInternalContentRequest(
|
|
|
98
98
|
try {
|
|
99
99
|
response = await (input.fetchImpl ?? fetch)(`${coreUrl}/api/broker/appDomains`, {
|
|
100
100
|
method: "GET",
|
|
101
|
+
redirect: "manual",
|
|
101
102
|
headers: {
|
|
102
103
|
accept: "application/json",
|
|
103
104
|
"X-ToT-Broker-Assertion": assertion,
|
|
@@ -298,6 +298,7 @@ export class HttpTotAccessClient implements TotAccessClient {
|
|
|
298
298
|
} catch {
|
|
299
299
|
return { ok: false, reason: "error" };
|
|
300
300
|
}
|
|
301
|
+
if (res.status >= 300 && res.status < 400) return { ok: false, reason: "error" };
|
|
301
302
|
let body: unknown;
|
|
302
303
|
try {
|
|
303
304
|
body = await res.json();
|
|
@@ -329,6 +330,7 @@ export class HttpTotAccessClient implements TotAccessClient {
|
|
|
329
330
|
}
|
|
330
331
|
return { ok: false, reason: "error" };
|
|
331
332
|
}
|
|
333
|
+
if (res.status >= 300 && res.status < 400) return { ok: false, reason: "error" };
|
|
332
334
|
let body: unknown = "(unread)";
|
|
333
335
|
try {
|
|
334
336
|
body = await res.json();
|
|
@@ -98,13 +98,14 @@ export interface HttpTransport {
|
|
|
98
98
|
/** Default transport: JSON `fetch` GET/POST. */
|
|
99
99
|
export class FetchHttpTransport implements HttpTransport {
|
|
100
100
|
async get(url: string, headers: Record<string, string>) {
|
|
101
|
-
return fetch(url, { method: "GET", headers: { accept: "application/json", ...headers } });
|
|
101
|
+
return fetch(url, { method: "GET", headers: { accept: "application/json", ...headers }, redirect: "manual" });
|
|
102
102
|
}
|
|
103
103
|
async post(url: string, headers: Record<string, string>, body: unknown) {
|
|
104
104
|
return fetch(url, {
|
|
105
105
|
method: "POST",
|
|
106
106
|
headers: { "content-type": "application/json", accept: "application/json", ...headers },
|
|
107
107
|
body: JSON.stringify(body),
|
|
108
|
+
redirect: "manual",
|
|
108
109
|
});
|
|
109
110
|
}
|
|
110
111
|
}
|
|
@@ -191,6 +192,9 @@ export class HttpStaffVendorAccessClient implements StaffVendorAccessClient {
|
|
|
191
192
|
message: "staffVendorAccess: transport failure searching vendors",
|
|
192
193
|
};
|
|
193
194
|
}
|
|
195
|
+
if (res.status >= 300 && res.status < 400) {
|
|
196
|
+
return { ok: false, status: res.status, reason: "error", message: "staffVendorAccess: redirects are not accepted" };
|
|
197
|
+
}
|
|
194
198
|
let parsed: unknown;
|
|
195
199
|
try {
|
|
196
200
|
parsed = await res.json();
|
|
@@ -229,6 +233,9 @@ export class HttpStaffVendorAccessClient implements StaffVendorAccessClient {
|
|
|
229
233
|
message: "staffVendorAccess: transport failure granting access",
|
|
230
234
|
};
|
|
231
235
|
}
|
|
236
|
+
if (res.status >= 300 && res.status < 400) {
|
|
237
|
+
return { ok: false, status: res.status, reason: "error", message: "staffVendorAccess: redirects are not accepted" };
|
|
238
|
+
}
|
|
232
239
|
let parsed: unknown;
|
|
233
240
|
try {
|
|
234
241
|
parsed = await res.json();
|
|
@@ -161,10 +161,12 @@ export class FetchExplainTransport implements ExplainTransport {
|
|
|
161
161
|
try {
|
|
162
162
|
const res = await fetch(url, {
|
|
163
163
|
method: "POST",
|
|
164
|
+
redirect: "manual",
|
|
164
165
|
headers: { "content-type": "application/json", ...headers },
|
|
165
166
|
body: JSON.stringify(body),
|
|
166
167
|
signal: ctrl.signal,
|
|
167
168
|
});
|
|
169
|
+
if (res.status >= 300 && res.status < 400) throw new Error("authenticated transport refused redirect");
|
|
168
170
|
return { status: res.status, json: () => res.json() };
|
|
169
171
|
} finally {
|
|
170
172
|
clearTimeout(timer);
|
|
@@ -318,8 +318,9 @@ export interface HttpTransport {
|
|
|
318
318
|
/** Default transport: JSON `fetch` POST/GET/DELETE. */
|
|
319
319
|
export class FetchHttpTransport implements HttpTransport {
|
|
320
320
|
async post(url: string, headers: Record<string, string>, body: unknown) {
|
|
321
|
-
|
|
321
|
+
const res = await fetch(url, {
|
|
322
322
|
method: "POST",
|
|
323
|
+
redirect: "manual",
|
|
323
324
|
headers: {
|
|
324
325
|
"content-type": "application/json",
|
|
325
326
|
accept: "application/json",
|
|
@@ -327,18 +328,26 @@ export class FetchHttpTransport implements HttpTransport {
|
|
|
327
328
|
},
|
|
328
329
|
body: JSON.stringify(body),
|
|
329
330
|
});
|
|
331
|
+
if (res.status >= 300 && res.status < 400) throw new Error("authenticated transport refused redirect");
|
|
332
|
+
return res;
|
|
330
333
|
}
|
|
331
334
|
async get(url: string, headers: Record<string, string>) {
|
|
332
|
-
|
|
335
|
+
const res = await fetch(url, {
|
|
333
336
|
method: "GET",
|
|
337
|
+
redirect: "manual",
|
|
334
338
|
headers: { accept: "application/json", ...headers },
|
|
335
339
|
});
|
|
340
|
+
if (res.status >= 300 && res.status < 400) throw new Error("authenticated transport refused redirect");
|
|
341
|
+
return res;
|
|
336
342
|
}
|
|
337
343
|
async delete(url: string, headers: Record<string, string>) {
|
|
338
|
-
|
|
344
|
+
const res = await fetch(url, {
|
|
339
345
|
method: "DELETE",
|
|
346
|
+
redirect: "manual",
|
|
340
347
|
headers: { accept: "application/json", ...headers },
|
|
341
348
|
});
|
|
349
|
+
if (res.status >= 300 && res.status < 400) throw new Error("authenticated transport refused redirect");
|
|
350
|
+
return res;
|
|
342
351
|
}
|
|
343
352
|
}
|
|
344
353
|
|
|
@@ -41,7 +41,8 @@ export class FetchPreflightTransport implements PreflightTransport {
|
|
|
41
41
|
const ctrl = new AbortController();
|
|
42
42
|
const timer = setTimeout(() => ctrl.abort(), PREFLIGHT_TIMEOUT_MS);
|
|
43
43
|
try {
|
|
44
|
-
const res = await fetch(url, { method: "GET", headers, signal: ctrl.signal });
|
|
44
|
+
const res = await fetch(url, { method: "GET", headers, signal: ctrl.signal, redirect: "manual" });
|
|
45
|
+
if (res.status >= 300 && res.status < 400) throw new Error("authenticated transport refused redirect");
|
|
45
46
|
return { status: res.status, json: () => res.json() };
|
|
46
47
|
} finally {
|
|
47
48
|
clearTimeout(timer);
|
|
@@ -287,17 +287,24 @@ export interface HttpTransport {
|
|
|
287
287
|
|
|
288
288
|
export class FetchHttpTransport implements HttpTransport {
|
|
289
289
|
async post(url: string, headers: Record<string, string>, body: unknown) {
|
|
290
|
-
|
|
290
|
+
const res = await fetch(url, {
|
|
291
291
|
method: "POST",
|
|
292
|
+
redirect: "manual",
|
|
292
293
|
headers: { "content-type": "application/json", accept: "application/json", ...headers },
|
|
293
294
|
body: JSON.stringify(body),
|
|
294
295
|
});
|
|
296
|
+
if (res.status >= 300 && res.status < 400) throw new Error("authenticated transport refused redirect");
|
|
297
|
+
return res;
|
|
295
298
|
}
|
|
296
299
|
async get(url: string, headers: Record<string, string>) {
|
|
297
|
-
|
|
300
|
+
const res = await fetch(url, { method: "GET", headers: { accept: "application/json", ...headers }, redirect: "manual" });
|
|
301
|
+
if (res.status >= 300 && res.status < 400) throw new Error("authenticated transport refused redirect");
|
|
302
|
+
return res;
|
|
298
303
|
}
|
|
299
304
|
async delete(url: string, headers: Record<string, string>) {
|
|
300
|
-
|
|
305
|
+
const res = await fetch(url, { method: "DELETE", headers: { accept: "application/json", ...headers }, redirect: "manual" });
|
|
306
|
+
if (res.status >= 300 && res.status < 400) throw new Error("authenticated transport refused redirect");
|
|
307
|
+
return res;
|
|
301
308
|
}
|
|
302
309
|
}
|
|
303
310
|
|
|
@@ -39,10 +39,19 @@ export interface MatrixFault {
|
|
|
39
39
|
mode: MatrixMode;
|
|
40
40
|
/** `effect` mode: the effect after which the object is evicted. */
|
|
41
41
|
effect?: string | null;
|
|
42
|
+
/** Test-only constructor delay after this eviction, to reproduce a late alarm. */
|
|
43
|
+
resumeDelayMs?: number;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
/** What the object looked like at the instant a matrix fault evicted it. */
|
|
45
47
|
export interface MatrixKill {
|
|
48
|
+
instanceId: string;
|
|
49
|
+
execution: number;
|
|
50
|
+
at: number;
|
|
51
|
+
stepStartSeq: number;
|
|
52
|
+
transitionSeq: number;
|
|
53
|
+
effectSeq: number;
|
|
54
|
+
resumeNotBefore: number;
|
|
46
55
|
jobId: string;
|
|
47
56
|
kind: string;
|
|
48
57
|
step: string;
|
|
@@ -63,6 +72,10 @@ export interface MatrixKill {
|
|
|
63
72
|
}
|
|
64
73
|
|
|
65
74
|
export interface MatrixEffect {
|
|
75
|
+
seq: number;
|
|
76
|
+
at: number;
|
|
77
|
+
instanceId: string;
|
|
78
|
+
execution: number;
|
|
66
79
|
jobId: string;
|
|
67
80
|
kind: string;
|
|
68
81
|
step: string;
|
|
@@ -102,6 +115,7 @@ export function previewServicesWithEffects(services: PreviewJobServices, landed:
|
|
|
102
115
|
}
|
|
103
116
|
|
|
104
117
|
interface Running {
|
|
118
|
+
execution: number;
|
|
105
119
|
jobId: string;
|
|
106
120
|
kind: string;
|
|
107
121
|
step: string;
|
|
@@ -110,9 +124,9 @@ interface Running {
|
|
|
110
124
|
function tables(state: DurableObjectState): void {
|
|
111
125
|
const { sql } = state.storage;
|
|
112
126
|
sql.exec(
|
|
113
|
-
`CREATE TABLE IF NOT EXISTS matrix_faults (id INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT, step TEXT, mode TEXT, effect TEXT, remaining INTEGER)`,
|
|
127
|
+
`CREATE TABLE IF NOT EXISTS matrix_faults (id INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT, step TEXT, mode TEXT, effect TEXT, remaining INTEGER, resume_delay INTEGER)`,
|
|
114
128
|
);
|
|
115
|
-
sql.exec(`CREATE TABLE IF NOT EXISTS matrix_effects (seq INTEGER PRIMARY KEY AUTOINCREMENT, job_id TEXT, kind TEXT, step TEXT, effect TEXT)`);
|
|
129
|
+
sql.exec(`CREATE TABLE IF NOT EXISTS matrix_effects (seq INTEGER PRIMARY KEY AUTOINCREMENT, job_id TEXT, kind TEXT, step TEXT, effect TEXT, at INTEGER, instance_id TEXT, execution INTEGER)`);
|
|
116
130
|
sql.exec(`CREATE TABLE IF NOT EXISTS matrix_execs (job_id TEXT, step TEXT, n INTEGER, PRIMARY KEY (job_id, step))`);
|
|
117
131
|
sql.exec(`CREATE TABLE IF NOT EXISTS matrix_kills (seq INTEGER PRIMARY KEY AUTOINCREMENT, kill TEXT)`);
|
|
118
132
|
sql.exec(`CREATE TABLE IF NOT EXISTS matrix_holds (job_id TEXT, step TEXT, released INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (job_id, step))`);
|
|
@@ -122,6 +136,7 @@ function tables(state: DurableObjectState): void {
|
|
|
122
136
|
const HOLD_LIMIT_MS = 30_000;
|
|
123
137
|
|
|
124
138
|
export class MatrixFaults {
|
|
139
|
+
private readonly instanceId = crypto.randomUUID();
|
|
125
140
|
private running: Running | null = null;
|
|
126
141
|
|
|
127
142
|
constructor(
|
|
@@ -144,7 +159,7 @@ export class MatrixFaults {
|
|
|
144
159
|
? {
|
|
145
160
|
onTerminal: async (info: Parameters<NonNullable<JobKindDefinition<unknown>["onTerminal"]>>[0]) => {
|
|
146
161
|
const previous = this.running;
|
|
147
|
-
this.running = { jobId: info.jobId, kind: name, step: "on-terminal" };
|
|
162
|
+
this.running = { jobId: info.jobId, kind: name, step: "on-terminal", execution: 0 };
|
|
148
163
|
try {
|
|
149
164
|
await onTerminal(info);
|
|
150
165
|
} finally {
|
|
@@ -171,7 +186,7 @@ export class MatrixFaults {
|
|
|
171
186
|
step.name,
|
|
172
187
|
)
|
|
173
188
|
.one().n;
|
|
174
|
-
this.running = { jobId: ctx.jobId, kind, step: step.name };
|
|
189
|
+
this.running = { jobId: ctx.jobId, kind, step: step.name, execution: n };
|
|
175
190
|
try {
|
|
176
191
|
if (n === 1) await this.maybeHold(kind, step.name, ctx);
|
|
177
192
|
if (n === 1) await this.maybeKill("before", null);
|
|
@@ -190,22 +205,24 @@ export class MatrixFaults {
|
|
|
190
205
|
readonly landed: Landed = async (effect) => {
|
|
191
206
|
const running = this.running;
|
|
192
207
|
this.state.storage.sql.exec(
|
|
193
|
-
`INSERT INTO matrix_effects (job_id, kind, step, effect) VALUES (?, ?, ?, ?)`,
|
|
208
|
+
`INSERT INTO matrix_effects (job_id, kind, step, effect, at, instance_id, execution) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
194
209
|
running?.jobId ?? "",
|
|
195
210
|
running?.kind ?? "",
|
|
196
211
|
running?.step ?? "",
|
|
197
212
|
effect,
|
|
213
|
+
Date.now(), this.instanceId, running?.execution ?? 0,
|
|
198
214
|
);
|
|
199
215
|
if (running) await this.maybeKill("effect", effect);
|
|
200
216
|
};
|
|
201
217
|
|
|
202
218
|
arm(fault: MatrixFault): void {
|
|
203
219
|
this.state.storage.sql.exec(
|
|
204
|
-
`INSERT INTO matrix_faults (kind, step, mode, effect, remaining) VALUES (?, ?, ?, ?, 1)`,
|
|
220
|
+
`INSERT INTO matrix_faults (kind, step, mode, effect, remaining, resume_delay) VALUES (?, ?, ?, ?, 1, ?)`,
|
|
205
221
|
fault.kind,
|
|
206
222
|
fault.step,
|
|
207
223
|
fault.mode,
|
|
208
224
|
fault.effect ?? null,
|
|
225
|
+
Math.max(0, Math.min(5_000, fault.resumeDelayMs ?? 0)),
|
|
209
226
|
);
|
|
210
227
|
}
|
|
211
228
|
|
|
@@ -249,12 +266,12 @@ export class MatrixFaults {
|
|
|
249
266
|
effects(jobId?: string): MatrixEffect[] {
|
|
250
267
|
const { sql } = this.state.storage;
|
|
251
268
|
const rows = jobId
|
|
252
|
-
? sql.exec<{ job_id: string; kind: string; step: string; effect: string }>(
|
|
253
|
-
`SELECT
|
|
269
|
+
? sql.exec<{ seq: number; at: number; instance_id: string; execution: number; job_id: string; kind: string; step: string; effect: string }>(
|
|
270
|
+
`SELECT * FROM matrix_effects WHERE job_id = ? ORDER BY seq`,
|
|
254
271
|
jobId,
|
|
255
272
|
)
|
|
256
|
-
: sql.exec<{ job_id: string; kind: string; step: string; effect: string }>(`SELECT
|
|
257
|
-
return rows.toArray().map((r) => ({ jobId: r.job_id, kind: r.kind, step: r.step, effect: r.effect }));
|
|
273
|
+
: sql.exec<{ seq: number; at: number; instance_id: string; execution: number; job_id: string; kind: string; step: string; effect: string }>(`SELECT * FROM matrix_effects ORDER BY seq`);
|
|
274
|
+
return rows.toArray().map((r) => ({ jobId: r.job_id, kind: r.kind, step: r.step, effect: r.effect, seq: r.seq, at: r.at, instanceId: r.instance_id, execution: r.execution }));
|
|
258
275
|
}
|
|
259
276
|
|
|
260
277
|
execs(jobId: string): { step: string; n: number }[] {
|
|
@@ -286,8 +303,8 @@ export class MatrixFaults {
|
|
|
286
303
|
if (!running) return;
|
|
287
304
|
const { sql } = this.state.storage;
|
|
288
305
|
const fault = sql
|
|
289
|
-
.exec<{ id: number }>(
|
|
290
|
-
`SELECT id FROM matrix_faults WHERE kind = ? AND step = ? AND mode = ? AND IFNULL(effect, '') = ? AND remaining > 0 ORDER BY id LIMIT 1`,
|
|
306
|
+
.exec<{ id: number; resume_delay: number }>(
|
|
307
|
+
`SELECT id, resume_delay FROM matrix_faults WHERE kind = ? AND step = ? AND mode = ? AND IFNULL(effect, '') = ? AND remaining > 0 ORDER BY id LIMIT 1`,
|
|
291
308
|
running.kind,
|
|
292
309
|
running.step,
|
|
293
310
|
mode,
|
|
@@ -297,6 +314,7 @@ export class MatrixFaults {
|
|
|
297
314
|
if (!fault) return;
|
|
298
315
|
sql.exec(`UPDATE matrix_faults SET remaining = remaining - 1 WHERE id = ?`, fault.id);
|
|
299
316
|
const kill = await this.snapshot(running, mode, effect);
|
|
317
|
+
kill.resumeNotBefore = fault.resume_delay ? Date.now() + fault.resume_delay : 0;
|
|
300
318
|
sql.exec(`INSERT INTO matrix_kills (kill) VALUES (?)`, JSON.stringify(kill));
|
|
301
319
|
// Everything written so far — the effect itself included — is durable, exactly as a real crash
|
|
302
320
|
// after it would leave it.
|
|
@@ -327,6 +345,10 @@ export class MatrixFaults {
|
|
|
327
345
|
}
|
|
328
346
|
}
|
|
329
347
|
return {
|
|
348
|
+
instanceId: this.instanceId, execution: running.execution, at: Date.now(), resumeNotBefore: 0,
|
|
349
|
+
stepStartSeq: sql.exec<{ seq: number }>(`SELECT MAX(seq) seq FROM transitions WHERE job_id = ? AND step = ? AND note LIKE 'step-start%'`, running.jobId, running.step).one().seq,
|
|
350
|
+
transitionSeq: sql.exec<{ seq: number }>(`SELECT MAX(seq) seq FROM transitions`).one().seq,
|
|
351
|
+
effectSeq: sql.exec<{ seq: number }>(`SELECT MAX(seq) seq FROM matrix_effects`).one().seq ?? 0,
|
|
330
352
|
jobId: running.jobId,
|
|
331
353
|
kind: running.kind,
|
|
332
354
|
step: running.step,
|
|
@@ -80,6 +80,13 @@ function harnessSeedServices(
|
|
|
80
80
|
const matrices = new WeakMap<DurableObjectState, { faults: MatrixFaults; registry: JobKindRegistry }>();
|
|
81
81
|
|
|
82
82
|
export class TenantPipeline extends DeployedTenantPipeline {
|
|
83
|
+
constructor(ctx: DurableObjectState, env: unknown) {
|
|
84
|
+
super(ctx, env);
|
|
85
|
+
const resumeAt = matrices.get(ctx)?.faults.kills().at(-1)?.resumeNotBefore ?? 0;
|
|
86
|
+
if (resumeAt > Date.now()) void ctx.blockConcurrencyWhile(async () => {
|
|
87
|
+
await new Promise((resolve) => setTimeout(resolve, resumeAt - Date.now()));
|
|
88
|
+
});
|
|
89
|
+
}
|
|
83
90
|
private commercePaused = false;
|
|
84
91
|
private commerceResume: (() => void) | null = null;
|
|
85
92
|
private commerceOutcome: string = "complete";
|
|
@@ -57,7 +57,7 @@ export const PUBLISH_HARNESS_TIMING: PublishTiming = {
|
|
|
57
57
|
export const PUBLISH_HARNESS_TUNING: PublishKindTuning = {
|
|
58
58
|
leaseMs: 3_000,
|
|
59
59
|
retryBaseMs: 20,
|
|
60
|
-
deadlineMs: { synchronous:
|
|
60
|
+
deadlineMs: { synchronous: 60_000, asynchronous: 60_000 },
|
|
61
61
|
hotfixExtraMs: 0,
|
|
62
62
|
compensationRetryBaseMs: 300,
|
|
63
63
|
};
|
|
@@ -172,6 +172,10 @@ export const SYNTHETIC_COOPERATIVE_HANG_KIND = "synthetic-cooperative-hang";
|
|
|
172
172
|
export const LONG_STEP_CHUNKS = 12;
|
|
173
173
|
export const LONG_STEP_CHUNK_MS = 100;
|
|
174
174
|
export const LONG_STEP_MAX_EXECUTION_MS = 450;
|
|
175
|
+
export const SYNTHETIC_STEP_CLOCK_KIND = "synthetic-step-clock";
|
|
176
|
+
export const STEP_CLOCK_BASE_MS = 1_000;
|
|
177
|
+
export const STEP_CLOCK_CAPTURE_MS = 1_500;
|
|
178
|
+
export const STEP_CLOCK_LIMIT_MS = 4_000;
|
|
175
179
|
|
|
176
180
|
function engineKinds(state: DurableObjectState): JobKindDefinition<{ mode?: string }>[] {
|
|
177
181
|
const { sql } = state.storage;
|
|
@@ -180,6 +184,34 @@ function engineKinds(state: DurableObjectState): JobKindDefinition<{ mode?: stri
|
|
|
180
184
|
const parse = (raw: unknown) => (raw && typeof raw === "object" ? (raw as { mode?: string }) : {});
|
|
181
185
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
182
186
|
return [
|
|
187
|
+
{
|
|
188
|
+
...base,
|
|
189
|
+
kind: SYNTHETIC_STEP_CLOCK_KIND,
|
|
190
|
+
parseInput: parse,
|
|
191
|
+
deadlineMs: STEP_CLOCK_BASE_MS,
|
|
192
|
+
leaseMs: 10_000,
|
|
193
|
+
maxAttemptsPerStep: 3,
|
|
194
|
+
steps: [
|
|
195
|
+
{ name: "capture", deadlineMs: STEP_CLOCK_LIMIT_MS, run: async (ctx) => {
|
|
196
|
+
const deadline = sql.exec<{ deadline: number }>(`SELECT deadline FROM jobs WHERE job_id = ?`, ctx.jobId).one().deadline;
|
|
197
|
+
ctx.setProduces({ captureStartedAt: ctx.now(), captureDeadline: deadline });
|
|
198
|
+
if (ctx.input.mode === "evict" && !ctx.checkpoint) {
|
|
199
|
+
ctx.saveCheckpoint({ evicted: true });
|
|
200
|
+
await ctx.flush();
|
|
201
|
+
state.abort("synthetic eviction during bounded step clock");
|
|
202
|
+
}
|
|
203
|
+
if (ctx.input.mode === "hang") await new Promise<void>((resolve) => ctx.signal.addEventListener("abort", () => resolve(), { once: true }));
|
|
204
|
+
else await sleep(STEP_CLOCK_CAPTURE_MS);
|
|
205
|
+
return { kind: "done" as const };
|
|
206
|
+
} },
|
|
207
|
+
{ name: "after", run: async (ctx) => {
|
|
208
|
+
const deadline = sql.exec<{ deadline: number }>(`SELECT deadline FROM jobs WHERE job_id = ?`, ctx.jobId).one().deadline;
|
|
209
|
+
ctx.setProduces({ afterStartedAt: ctx.now(), afterDeadline: deadline });
|
|
210
|
+
await new Promise<void>((resolve) => ctx.signal.addEventListener("abort", () => resolve(), { once: true }));
|
|
211
|
+
return { kind: "done" as const };
|
|
212
|
+
} },
|
|
213
|
+
],
|
|
214
|
+
},
|
|
183
215
|
{
|
|
184
216
|
kind: SYNTHETIC_OPTIONAL_TAIL_KIND,
|
|
185
217
|
parseInput: parse,
|
|
@@ -561,9 +561,15 @@ export function chainViolations(log: TransitionView[]): string[] {
|
|
|
561
561
|
}
|
|
562
562
|
|
|
563
563
|
/** Across every job of the tenant: between a step's start and its outcome, no other step starts. */
|
|
564
|
-
export function overlapViolations(all: TransitionView[]): string[] {
|
|
564
|
+
export function overlapViolations(all: TransitionView[], kills: readonly MatrixKill[] = [], effects: readonly MatrixEffect[] = []): string[] {
|
|
565
565
|
let open: TransitionView | null = null;
|
|
566
566
|
for (const t of all) {
|
|
567
|
+
// A recorded abort ends only its exact execution; any later effect keeps the overlap visible.
|
|
568
|
+
if (open && kills.some((kill) => ["before", "after", "resume", "effect"].includes(kill.mode) && !!kill.instanceId && kill.execution > 0 &&
|
|
569
|
+
kill.jobId === open!.jobId && kill.step === open!.step && kill.stepStartSeq === open!.seq &&
|
|
570
|
+
kill.transitionSeq >= open!.seq && kill.transitionSeq < t.seq &&
|
|
571
|
+
!effects.some((effect) => effect.jobId === kill.jobId && effect.step === kill.step &&
|
|
572
|
+
effect.instanceId === kill.instanceId && effect.execution === kill.execution && effect.seq > kill.effectSeq))) open = null;
|
|
567
573
|
if (t.note?.startsWith("step-start")) {
|
|
568
574
|
if (open && open.jobId !== t.jobId) return [`step ${t.step} of ${t.jobId} started while ${open.step} of ${open.jobId} was running`];
|
|
569
575
|
open = t;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/storefront-runner",
|
|
3
|
-
"version": "4.12.
|
|
3
|
+
"version": "4.12.2",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
|
|
6
6
|
"packageManager": "pnpm@11.9.0",
|