chore: import upstream snapshot with attribution
Deploy (to testing) and Test Playground Preview Worker / Deploy Playground Preview Worker (testing) (push) Has been skipped
Deploy Workers Shared Staging / Deploy Workers Shared Staging (push) Failing after 0s
Prerelease / build (push) Has been skipped
Handle Changesets / Handle Changesets (push) Has been cancelled
Semgrep OSS scan / semgrep-oss (push) Has been cancelled
Deploy (to testing) and Test Playground Preview Worker / Deploy Playground Preview Worker (testing) (push) Has been skipped
Deploy Workers Shared Staging / Deploy Workers Shared Staging (push) Failing after 0s
Prerelease / build (push) Has been skipped
Handle Changesets / Handle Changesets (push) Has been cancelled
Semgrep OSS scan / semgrep-oss (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@fixture/workflow-multiple",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"deploy": "wrangler deploy",
|
||||
"start": "wrangler dev",
|
||||
"test:ci": "vitest run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "catalog:default",
|
||||
"typescript": "catalog:default",
|
||||
"undici": "catalog:default",
|
||||
"vitest": "catalog:default",
|
||||
"wrangler": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import {
|
||||
WorkerEntrypoint,
|
||||
WorkflowEntrypoint,
|
||||
WorkflowEvent,
|
||||
WorkflowStep,
|
||||
} from "cloudflare:workers";
|
||||
|
||||
type Params = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export class Demo extends WorkflowEntrypoint<{}, Params> {
|
||||
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
|
||||
const { timestamp, payload } = event;
|
||||
|
||||
await step.sleep("Wait", "1 second");
|
||||
|
||||
const result = await step.do("First step", async function () {
|
||||
return {
|
||||
output: "First step result",
|
||||
};
|
||||
});
|
||||
|
||||
await step.sleep("Wait", "1 second");
|
||||
|
||||
const result2 = await step.do("Second step", async function () {
|
||||
return {
|
||||
output: "workflow1",
|
||||
};
|
||||
});
|
||||
|
||||
return "i'm workflow1";
|
||||
}
|
||||
}
|
||||
|
||||
export class Demo2 extends WorkflowEntrypoint<{}, Params> {
|
||||
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
|
||||
const { timestamp, payload } = event;
|
||||
|
||||
await step.sleep("Wait", "1 second");
|
||||
|
||||
const result = await step.do("First step", async function () {
|
||||
return {
|
||||
output: "First step result",
|
||||
};
|
||||
});
|
||||
|
||||
await step.sleep("Wait", "1 second");
|
||||
|
||||
const result2 = await step.do("Second step", async function () {
|
||||
return {
|
||||
output: "workflow2",
|
||||
};
|
||||
});
|
||||
|
||||
return "i'm workflow2";
|
||||
}
|
||||
}
|
||||
|
||||
export class Demo3 extends WorkflowEntrypoint<{}, Params> {
|
||||
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
|
||||
const result = await step.do("First step", async function () {
|
||||
return {
|
||||
output: "First step result",
|
||||
};
|
||||
});
|
||||
|
||||
await step.waitForEvent("wait for signal", {
|
||||
type: "continue",
|
||||
});
|
||||
|
||||
const result2 = await step.do("Second step", async function () {
|
||||
return {
|
||||
output: "workflow3",
|
||||
};
|
||||
});
|
||||
|
||||
return "i'm workflow3";
|
||||
}
|
||||
}
|
||||
|
||||
type Env = {
|
||||
WORKFLOW: Workflow;
|
||||
WORKFLOW2: Workflow;
|
||||
WORKFLOW3: Workflow;
|
||||
};
|
||||
|
||||
export default class extends WorkerEntrypoint<Env> {
|
||||
async fetch(req: Request) {
|
||||
const url = new URL(req.url);
|
||||
const id = url.searchParams.get("id");
|
||||
const workflowName = url.searchParams.get("workflowName");
|
||||
|
||||
if (url.pathname === "/favicon.ico") {
|
||||
return new Response(null, { status: 404 });
|
||||
}
|
||||
|
||||
let workflowToUse: Workflow;
|
||||
if (workflowName === "3") {
|
||||
workflowToUse = this.env.WORKFLOW3;
|
||||
} else if (workflowName === "2") {
|
||||
workflowToUse = this.env.WORKFLOW2;
|
||||
} else {
|
||||
workflowToUse = this.env.WORKFLOW;
|
||||
}
|
||||
|
||||
let handle: WorkflowInstance;
|
||||
if (url.pathname === "/create") {
|
||||
if (id === null) {
|
||||
handle = await workflowToUse.create();
|
||||
} else {
|
||||
handle = await workflowToUse.create({ id });
|
||||
}
|
||||
} else if (url.pathname === "/pause") {
|
||||
handle = await workflowToUse.get(id);
|
||||
await handle.pause();
|
||||
} else if (url.pathname === "/resume") {
|
||||
handle = await workflowToUse.get(id);
|
||||
await handle.resume();
|
||||
} else if (url.pathname === "/restart") {
|
||||
handle = await workflowToUse.get(id);
|
||||
await handle.restart();
|
||||
} else if (url.pathname === "/terminate") {
|
||||
handle = await workflowToUse.get(id);
|
||||
await handle.terminate();
|
||||
} else if (url.pathname === "/sendEvent") {
|
||||
handle = await workflowToUse.get(id);
|
||||
await handle.sendEvent({
|
||||
type: "continue",
|
||||
payload: await req.json(),
|
||||
});
|
||||
return Response.json({ ok: true });
|
||||
} else {
|
||||
handle = await workflowToUse.get(id);
|
||||
}
|
||||
|
||||
return Response.json({ status: await handle.status(), id: handle.id });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import { rm } from "fs/promises";
|
||||
import { resolve } from "path";
|
||||
import { fetch } from "undici";
|
||||
import { afterAll, beforeAll, describe, it, test, vi } from "vitest";
|
||||
import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived";
|
||||
|
||||
describe("Workflows", () => {
|
||||
let ip: string,
|
||||
port: number,
|
||||
stop: (() => Promise<unknown>) | undefined,
|
||||
getOutput: () => string;
|
||||
|
||||
beforeAll(async () => {
|
||||
// delete previous run contents because of persistence
|
||||
await rm(resolve(__dirname, "..") + "/.wrangler", {
|
||||
force: true,
|
||||
recursive: true,
|
||||
});
|
||||
({ ip, port, stop, getOutput } = await runWranglerDev(
|
||||
resolve(__dirname, ".."),
|
||||
["--port=0", "--inspector-port=0"]
|
||||
));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await stop?.();
|
||||
});
|
||||
|
||||
async function fetchJson(url: string, body?: unknown, method?: string) {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
"MF-Disable-Pretty-Error": "1",
|
||||
},
|
||||
method: method ?? "GET",
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
const text = await response.text();
|
||||
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (err) {
|
||||
throw new Error(`Couldn't parse JSON:\n\n${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
it("creates two instances with same id in two different workflows", async ({
|
||||
expect,
|
||||
}) => {
|
||||
// Create both workflow instances
|
||||
// Note: We don't assert the intermediate "running" status because the workflow
|
||||
// may complete before we can observe it, causing flaky tests on fast CI machines
|
||||
await Promise.all([
|
||||
fetchJson(`http://${ip}:${port}/create?workflowName=1&id=test`),
|
||||
fetchJson(`http://${ip}:${port}/create?workflowName=2&id=test`),
|
||||
]);
|
||||
|
||||
// Wait for both workflows to complete with their final outputs
|
||||
await Promise.all([
|
||||
vi.waitFor(
|
||||
async () => {
|
||||
await expect(
|
||||
fetchJson(`http://${ip}:${port}/status?workflowName=1&id=test`)
|
||||
).resolves.toStrictEqual({
|
||||
id: "test",
|
||||
status: {
|
||||
status: "complete",
|
||||
__LOCAL_DEV_STEP_OUTPUTS: [
|
||||
{ output: "First step result" },
|
||||
{ output: "workflow1" },
|
||||
],
|
||||
output: "i'm workflow1",
|
||||
},
|
||||
});
|
||||
},
|
||||
{ timeout: 10000 }
|
||||
),
|
||||
vi.waitFor(
|
||||
async () => {
|
||||
await expect(
|
||||
fetchJson(`http://${ip}:${port}/status?workflowName=2&id=test`)
|
||||
).resolves.toStrictEqual({
|
||||
id: "test",
|
||||
status: {
|
||||
status: "complete",
|
||||
__LOCAL_DEV_STEP_OUTPUTS: [
|
||||
{ output: "First step result" },
|
||||
{ output: "workflow2" },
|
||||
],
|
||||
output: "i'm workflow2",
|
||||
},
|
||||
});
|
||||
},
|
||||
{ timeout: 10000 }
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
describe("instance lifecycle methods (workflow3)", () => {
|
||||
test("pause and resume a workflow", async ({ expect }) => {
|
||||
const id = randomUUID();
|
||||
|
||||
await fetchJson(`http://${ip}:${port}/create?workflowName=3&id=${id}`);
|
||||
|
||||
await vi.waitFor(
|
||||
async () => {
|
||||
const result = (await fetchJson(
|
||||
`http://${ip}:${port}/status?workflowName=3&id=${id}`
|
||||
)) as {
|
||||
status: {
|
||||
__LOCAL_DEV_STEP_OUTPUTS: { output: string }[];
|
||||
};
|
||||
};
|
||||
expect(result.status.__LOCAL_DEV_STEP_OUTPUTS).toContainEqual({
|
||||
output: "First step result",
|
||||
});
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
|
||||
// Pause the instance
|
||||
await fetchJson(`http://${ip}:${port}/pause?workflowName=3&id=${id}`);
|
||||
|
||||
await vi.waitFor(
|
||||
async () => {
|
||||
const result = (await fetchJson(
|
||||
`http://${ip}:${port}/status?workflowName=3&id=${id}`
|
||||
)) as { status: { status: string } };
|
||||
expect(result.status.status).toBe("paused");
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
|
||||
// Resume the instance
|
||||
await fetchJson(`http://${ip}:${port}/resume?workflowName=3&id=${id}`);
|
||||
|
||||
await fetchJson(
|
||||
`http://${ip}:${port}/sendEvent?workflowName=3&id=${id}`,
|
||||
{ done: true },
|
||||
"POST"
|
||||
);
|
||||
|
||||
await vi.waitFor(
|
||||
async () => {
|
||||
const result = (await fetchJson(
|
||||
`http://${ip}:${port}/status?workflowName=3&id=${id}`
|
||||
)) as { status: { status: string; output: string } };
|
||||
expect(result.status.status).toBe("complete");
|
||||
expect(result.status.output).toBe("i'm workflow3");
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
});
|
||||
|
||||
test("terminate a running workflow", async ({ expect }) => {
|
||||
const id = randomUUID();
|
||||
|
||||
await fetchJson(`http://${ip}:${port}/create?workflowName=3&id=${id}`);
|
||||
|
||||
await vi.waitFor(
|
||||
async () => {
|
||||
const result = (await fetchJson(
|
||||
`http://${ip}:${port}/status?workflowName=3&id=${id}`
|
||||
)) as {
|
||||
status: {
|
||||
__LOCAL_DEV_STEP_OUTPUTS: { output: string }[];
|
||||
};
|
||||
};
|
||||
expect(result.status.__LOCAL_DEV_STEP_OUTPUTS).toContainEqual({
|
||||
output: "First step result",
|
||||
});
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
|
||||
// Terminate
|
||||
await fetchJson(`http://${ip}:${port}/terminate?workflowName=3&id=${id}`);
|
||||
|
||||
await vi.waitFor(
|
||||
async () => {
|
||||
const result = (await fetchJson(
|
||||
`http://${ip}:${port}/status?workflowName=3&id=${id}`
|
||||
)) as { status: { status: string } };
|
||||
expect(result.status.status).toBe("terminated");
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
});
|
||||
|
||||
test("restart a running workflow", async ({ expect }) => {
|
||||
const id = randomUUID();
|
||||
|
||||
await fetchJson(`http://${ip}:${port}/create?workflowName=3&id=${id}`);
|
||||
|
||||
await vi.waitFor(
|
||||
async () => {
|
||||
const result = (await fetchJson(
|
||||
`http://${ip}:${port}/status?workflowName=3&id=${id}`
|
||||
)) as {
|
||||
status: {
|
||||
__LOCAL_DEV_STEP_OUTPUTS: { output: string }[];
|
||||
};
|
||||
};
|
||||
expect(result.status.__LOCAL_DEV_STEP_OUTPUTS).toContainEqual({
|
||||
output: "First step result",
|
||||
});
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
|
||||
// Restart the instance
|
||||
await fetchJson(`http://${ip}:${port}/restart?workflowName=3&id=${id}`);
|
||||
|
||||
// After restart, wait for it to be running again
|
||||
await vi.waitFor(
|
||||
async () => {
|
||||
const result = (await fetchJson(
|
||||
`http://${ip}:${port}/status?workflowName=3&id=${id}`
|
||||
)) as { status: { status: string } };
|
||||
expect(result.status.status).toBe("running");
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
|
||||
// Send event to complete the restarted workflow
|
||||
await fetchJson(
|
||||
`http://${ip}:${port}/sendEvent?workflowName=3&id=${id}`,
|
||||
{ done: true },
|
||||
"POST"
|
||||
);
|
||||
|
||||
await vi.waitFor(
|
||||
async () => {
|
||||
const result = (await fetchJson(
|
||||
`http://${ip}:${port}/status?workflowName=3&id=${id}`
|
||||
)) as { status: { status: string; output: string } };
|
||||
expect(result.status.status).toBe("complete");
|
||||
expect(result.status.output).toBe("i'm workflow3");
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "@cloudflare/workers-tsconfig/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "preserve",
|
||||
"lib": ["ES2020"],
|
||||
"types": ["@cloudflare/workers-types"],
|
||||
"moduleResolution": "node",
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["tests"]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineProject, mergeConfig } from "vitest/config";
|
||||
import configShared from "../../vitest.shared";
|
||||
|
||||
export default mergeConfig(
|
||||
configShared,
|
||||
defineProject({
|
||||
test: {},
|
||||
})
|
||||
);
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "my-workflow-demo",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2024-10-22",
|
||||
"workflows": [
|
||||
{
|
||||
"binding": "WORKFLOW",
|
||||
"name": "my-workflow",
|
||||
"class_name": "Demo",
|
||||
},
|
||||
{
|
||||
"binding": "WORKFLOW2",
|
||||
"name": "my-workflow-2",
|
||||
"class_name": "Demo2",
|
||||
},
|
||||
{
|
||||
"binding": "WORKFLOW3",
|
||||
"name": "my-workflow-3",
|
||||
"class_name": "Demo3",
|
||||
},
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user