@tokenoftrust/storefront-runner 2.4.2 → 2.4.3
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.
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
* email, then the object is evicted
|
|
16
16
|
* - `reaccept-times-out` after the forge merged, the re-accept of a resumed merge answers a
|
|
17
17
|
* transient timeout instead of a refusal
|
|
18
|
+
* - `slow-build` every file of a build takes a few milliseconds (a build that is
|
|
19
|
+
* still running seconds after its intent was answered)
|
|
20
|
+
* The fake also records what each job reported to MCP (`fake_events` kind `report`) and which
|
|
21
|
+
* shas the preview could not render (`refused`).
|
|
18
22
|
* A fault fires `times` times (0 = every time); its firing is persisted before it acts.
|
|
19
23
|
*/
|
|
20
24
|
import type { DurableObjectState } from "cloudflare:workers";
|
|
@@ -29,6 +33,9 @@ import type {
|
|
|
29
33
|
/** Files a fake build materializes. */
|
|
30
34
|
export const FAKE_FILE_COUNT = 250;
|
|
31
35
|
|
|
36
|
+
/** How long one file of a `slow-build` takes: the whole build, about a second and a half. */
|
|
37
|
+
export const SLOW_FILE_MS = 6;
|
|
38
|
+
|
|
32
39
|
export interface FakeFault {
|
|
33
40
|
point: string;
|
|
34
41
|
times: number;
|
|
@@ -43,6 +50,9 @@ export interface FakeState {
|
|
|
43
50
|
staged: string[];
|
|
44
51
|
refreshes: string[];
|
|
45
52
|
emails: string[];
|
|
53
|
+
/** `<sha>:<evidence|refused>`, in report order. */
|
|
54
|
+
reports: string[];
|
|
55
|
+
refused: string[];
|
|
46
56
|
}
|
|
47
57
|
|
|
48
58
|
export function fakeTables(state: DurableObjectState): void {
|
|
@@ -66,6 +76,8 @@ export function configureFake(
|
|
|
66
76
|
faults?: FakeFault[];
|
|
67
77
|
/** Other open candidates a base advance rebuilds; `conflicts` ones email their author. */
|
|
68
78
|
rebaseCandidates?: { changeId: string; pr: number; conflicts?: boolean }[];
|
|
79
|
+
/** `[ancestor, descendant]` commit pairs the fake forge answers `isAncestor` true for. */
|
|
80
|
+
ancestry?: [string, string][];
|
|
69
81
|
},
|
|
70
82
|
): void {
|
|
71
83
|
fakeTables(state);
|
|
@@ -76,6 +88,9 @@ export function configureFake(
|
|
|
76
88
|
for (const f of config.faults ?? []) {
|
|
77
89
|
sql.exec(`INSERT OR REPLACE INTO fake_faults (point, times, fired) VALUES (?, ?, 0)`, f.point, f.times);
|
|
78
90
|
}
|
|
91
|
+
for (const [ancestor, descendant] of config.ancestry ?? []) {
|
|
92
|
+
sql.exec(`INSERT OR IGNORE INTO fake_ancestry (ancestor, descendant) VALUES (?, ?)`, ancestor, descendant);
|
|
93
|
+
}
|
|
79
94
|
for (const c of config.rebaseCandidates ?? []) {
|
|
80
95
|
sql.exec(`INSERT OR REPLACE INTO fake_candidates (change_id, pr, conflicts) VALUES (?, ?, ?)`, c.changeId, c.pr, c.conflicts ? 1 : 0);
|
|
81
96
|
}
|
|
@@ -103,6 +118,8 @@ export function readFakeState(state: DurableObjectState): FakeState {
|
|
|
103
118
|
staged: events("stage"),
|
|
104
119
|
refreshes: events("refresh"),
|
|
105
120
|
emails: events("email"),
|
|
121
|
+
reports: events("report"),
|
|
122
|
+
refused: events("refused"),
|
|
106
123
|
};
|
|
107
124
|
}
|
|
108
125
|
|
|
@@ -181,9 +198,11 @@ export function fakePreviewServices(state: DurableObjectState): PreviewJobServic
|
|
|
181
198
|
cause: new ForgeTransportError("timed out reading the body", { transient: true, errorKind: "timeout" }),
|
|
182
199
|
};
|
|
183
200
|
}
|
|
201
|
+
const slow = await fires("slow-build");
|
|
184
202
|
for (let file = 0; file < FAKE_FILE_COUNT; file += 1) {
|
|
185
203
|
const path = `content/file-${file}.json`;
|
|
186
204
|
if (resume.done(path)) continue;
|
|
205
|
+
if (slow) await new Promise((resolve) => setTimeout(resolve, SLOW_FILE_MS));
|
|
187
206
|
if (await fires(`evict-at-file:${file}`)) evict(`at file ${file}`);
|
|
188
207
|
sql.exec(`INSERT INTO fake_file_runs (sha, file) VALUES (?, ?)`, sha, file);
|
|
189
208
|
await resume.record(path, { storedVersionId: `v-${sha}-${file}`, issues: [], skippedHere: false });
|
|
@@ -191,8 +210,17 @@ export function fakePreviewServices(state: DurableObjectState): PreviewJobServic
|
|
|
191
210
|
return { ok: true, versionId: sha, digest: `digest-${sha}` };
|
|
192
211
|
},
|
|
193
212
|
|
|
194
|
-
async evidence() {
|
|
195
|
-
return { promotable: true };
|
|
213
|
+
async evidence({ sha }) {
|
|
214
|
+
return { promotable: true, report: { sha, promotable: true, checks: [] } };
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
async reportOutcome({ sha, outcome }) {
|
|
218
|
+
sql.exec(`INSERT INTO fake_events (kind, subject) VALUES ('report', ?)`, `${sha}:${outcome.kind}`);
|
|
219
|
+
return { reported: true };
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
async recordPreviewRefused({ sha }) {
|
|
223
|
+
sql.exec(`INSERT INTO fake_events (kind, subject) VALUES ('refused', ?)`, sha);
|
|
196
224
|
},
|
|
197
225
|
|
|
198
226
|
async previewPointer() {
|
|
@@ -204,8 +232,9 @@ export function fakePreviewServices(state: DurableObjectState): PreviewJobServic
|
|
|
204
232
|
return sql.exec(`SELECT 1 FROM fake_ancestry WHERE ancestor = ? AND descendant = ?`, ancestor, descendant).toArray().length > 0;
|
|
205
233
|
},
|
|
206
234
|
|
|
207
|
-
async advancePreview({ sha, membership, jobId, kind }) {
|
|
235
|
+
async advancePreview({ sha, membership, jobId, kind, releasable }) {
|
|
208
236
|
setPointer(sha);
|
|
237
|
+
if (!releasable) return;
|
|
209
238
|
sql.exec(
|
|
210
239
|
`INSERT INTO fake_advances (sha, membership, job_id, kind) VALUES (?, ?, ?, ?)`,
|
|
211
240
|
sha,
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
* The Worker that hosts the TenantPipeline Durable Object for the harness, in its own isolate
|
|
3
3
|
* (as in production, where callers never share the object's isolate — and where an eviction
|
|
4
4
|
* restarts the object without touching its callers). It runs the production engine with the
|
|
5
|
-
* synthetic job kinds AND the production preview kinds (integrate / rebuild / revert) over fake
|
|
5
|
+
* synthetic job kinds AND the production preview kinds (integrate / rebuild / revert / sync-preview) over fake
|
|
6
6
|
* services (`./fake-preview-services.ts`), and answers intents through `submitIntent`.
|
|
7
7
|
*/
|
|
8
8
|
import type { DurableObjectState } from "cloudflare:workers";
|
|
9
9
|
import { TenantPipeline as DeployedTenantPipeline } from "../../src/lib/pipeline/tenant-pipeline";
|
|
10
|
-
import { submitIntent, type TenantPipelineNamespace } from "../../src/lib/pipeline/intake";
|
|
10
|
+
import { submitIntent, tenantPipeline, type TenantPipelineNamespace } from "../../src/lib/pipeline/intake";
|
|
11
|
+
import { submitPreviewSync } from "../../src/lib/pipeline/preview-sync-intake";
|
|
11
12
|
import { StaleExecutionError, type JobKindRegistry, type PipelineIntent } from "../../src/lib/pipeline/job-runner";
|
|
12
13
|
import { abandonedContexts, syntheticJobKinds, syntheticSpans } from "./synthetic-job";
|
|
13
14
|
import { registerPreviewJobKinds, type PreviewJobTiming } from "../../src/lib/pipeline/preview-jobs";
|
|
@@ -28,7 +29,11 @@ export class TenantPipeline extends DeployedTenantPipeline {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
/** Harness-only: open candidates and arm faults for the fake preview services. */
|
|
31
|
-
async configureFake(config: {
|
|
32
|
+
async configureFake(config: {
|
|
33
|
+
open?: { changeId: string; headSha: string }[];
|
|
34
|
+
faults?: FakeFault[];
|
|
35
|
+
ancestry?: [string, string][];
|
|
36
|
+
}): Promise<void> {
|
|
32
37
|
configureFake(this.ctx, config);
|
|
33
38
|
}
|
|
34
39
|
|
|
@@ -91,8 +96,28 @@ interface Env {
|
|
|
91
96
|
TENANT_PIPELINE: TenantPipelineNamespace;
|
|
92
97
|
}
|
|
93
98
|
|
|
99
|
+
/**
|
|
100
|
+
* `/sync/<tenant>` answers a forwarded push the way `/api/preview/reconcile` does: submit the
|
|
101
|
+
* `sync-preview` intent and answer 202. `/sync-hold/<tenant>` submits, then holds the response open
|
|
102
|
+
* (a forward the caller kills mid-flight): the job must not depend on the request.
|
|
103
|
+
*/
|
|
104
|
+
async function syncRoute(request: Request, env: Env, tenant: string, hold: boolean): Promise<Response> {
|
|
105
|
+
const body = (await request.json()) as { repo: string; sha: string };
|
|
106
|
+
const result = await submitPreviewSync(
|
|
107
|
+
tenantPipeline(env.TENANT_PIPELINE, tenant),
|
|
108
|
+
tenant,
|
|
109
|
+
{ repo: body.repo, sha: body.sha, source: "push" },
|
|
110
|
+
{ actor: { type: "service", id: "harness-forward" }, admissionDecisionId: null },
|
|
111
|
+
);
|
|
112
|
+
if (!result.ok) return Response.json({ error: result.code }, { status: 400 });
|
|
113
|
+
if (hold) await new Promise((resolve) => setTimeout(resolve, 10_000));
|
|
114
|
+
return Response.json({ jobId: result.jobId, state: result.state, duplicate: result.duplicate }, { status: 202 });
|
|
115
|
+
}
|
|
116
|
+
|
|
94
117
|
export default {
|
|
95
118
|
async fetch(request: Request, env: Env): Promise<Response> {
|
|
119
|
+
const sync = /^\/(sync|sync-hold)\/([^/]+)$/.exec(new URL(request.url).pathname);
|
|
120
|
+
if (request.method === "POST" && sync) return syncRoute(request, env, decodeURIComponent(sync[2]!), sync[1] === "sync-hold");
|
|
96
121
|
const match = /^\/intents\/([^/]+)$/.exec(new URL(request.url).pathname);
|
|
97
122
|
if (request.method !== "POST" || !match) return new Response("not found", { status: 404 });
|
|
98
123
|
const response = await submitIntent(env.TENANT_PIPELINE, decodeURIComponent(match[1]!), (await request.json()) as PipelineIntent);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/storefront-runner",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.3",
|
|
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",
|