70bf21e064
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
1791 lines
46 KiB
TypeScript
1791 lines
46 KiB
TypeScript
import { getWorkerRegistry, Miniflare } from "miniflare";
|
|
import { describe, onTestFinished, test, vi } from "vitest";
|
|
import { useDispose, useTmp } from "./test-shared";
|
|
import type { MiniflareOptions, WorkerRegistry } from "miniflare";
|
|
|
|
describe.sequential("DevRegistry", () => {
|
|
test("fetch to service worker", async ({ expect }) => {
|
|
const unsafeDevRegistryPath = await useTmp();
|
|
const remote = new Miniflare({
|
|
name: "remote-worker",
|
|
unsafeDevRegistryPath,
|
|
compatibilityFlags: ["experimental"],
|
|
script: `addEventListener("fetch", (event) => {
|
|
event.respondWith(new Response("Hello from service worker!"));
|
|
})`,
|
|
});
|
|
|
|
await remote.ready;
|
|
|
|
const local = new Miniflare({
|
|
name: "local-worker",
|
|
unsafeDevRegistryPath,
|
|
serviceBindings: {
|
|
SERVICE: {
|
|
name: "remote-worker",
|
|
},
|
|
},
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
export default {
|
|
async fetch(request, env, ctx) {
|
|
return await env.SERVICE.fetch(request);
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(local);
|
|
|
|
const res = await local.dispatchFetch("http://placeholder");
|
|
|
|
expect(await res.text()).toBe("Hello from service worker!");
|
|
expect(res.status).toBe(200);
|
|
|
|
// Kill the remote worker to see if it fails gracefully
|
|
await remote.dispose();
|
|
await vi.waitFor(
|
|
async () => {
|
|
const res = await local.dispatchFetch("http://placeholder");
|
|
|
|
expect(await res.text()).toBe(
|
|
`Worker "remote-worker" not found. Make sure it is running locally.`
|
|
);
|
|
expect(res.status).toBe(503);
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
});
|
|
|
|
test("fetch to module worker", async ({ expect }) => {
|
|
const unsafeDevRegistryPath = await useTmp();
|
|
const local = new Miniflare({
|
|
name: "local-worker",
|
|
unsafeDevRegistryPath,
|
|
serviceBindings: {
|
|
SERVICE: {
|
|
name: "remote-worker",
|
|
},
|
|
},
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
export default {
|
|
async fetch(request, env, ctx) {
|
|
const response = await env.SERVICE.fetch(request.url);
|
|
const text = await response.text();
|
|
|
|
return new Response("Response from remote worker: " + text, {
|
|
status: response.status,
|
|
});
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(local);
|
|
|
|
const res = await local.dispatchFetch("http://example.com?name=World");
|
|
expect(await res.text()).toBe(
|
|
`Response from remote worker: Worker "remote-worker" not found. Make sure it is running locally.`
|
|
);
|
|
expect(res.status).toBe(503);
|
|
|
|
const remote = new Miniflare({
|
|
name: "remote-worker",
|
|
unsafeDevRegistryPath,
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
export default {
|
|
async fetch(request, env, ctx) {
|
|
const url = new URL(request.url);
|
|
const name = url.searchParams.get("name") ?? 'anonymous';
|
|
|
|
return new Response("Hello " + name);
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(remote);
|
|
|
|
await remote.ready;
|
|
await vi.waitFor(
|
|
async () => {
|
|
const res = await local.dispatchFetch("http://example.com?name=World");
|
|
const result = await res.text();
|
|
expect(result).toBe("Response from remote worker: Hello World");
|
|
expect(res.status).toBe(200);
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
});
|
|
|
|
test("WebSocket upgrade to module worker", async ({ expect }) => {
|
|
const unsafeDevRegistryPath = await useTmp();
|
|
const local = new Miniflare({
|
|
name: "local-worker",
|
|
unsafeDevRegistryPath,
|
|
serviceBindings: {
|
|
SERVICE: {
|
|
name: "remote-worker",
|
|
},
|
|
},
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
export default {
|
|
async fetch(request, env, ctx) {
|
|
const wsResponse = await env.SERVICE.fetch(request.url, {
|
|
headers: { Upgrade: "websocket" }
|
|
});
|
|
|
|
if (wsResponse.webSocket) {
|
|
wsResponse.webSocket.accept();
|
|
|
|
const messagePromise = new Promise((resolve) => {
|
|
wsResponse.webSocket.addEventListener("message", (event) => {
|
|
resolve(event.data);
|
|
});
|
|
});
|
|
|
|
// Test bidirectional communication
|
|
wsResponse.webSocket.send("ping");
|
|
|
|
const response = await messagePromise;
|
|
|
|
return new Response(\`WebSocket communication successful: \${response}\`, {
|
|
status: 200,
|
|
});
|
|
}
|
|
|
|
return new Response("WebSocket upgrade failed", {
|
|
status: 500,
|
|
});
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(local);
|
|
|
|
const remote = new Miniflare({
|
|
name: "remote-worker",
|
|
unsafeDevRegistryPath,
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
export default {
|
|
async fetch(request, env, ctx) {
|
|
// Handle WebSocket upgrade requests
|
|
if (request.headers.get("Upgrade") === "websocket") {
|
|
const [server, client] = Object.values(new WebSocketPair());
|
|
server.accept();
|
|
|
|
server.addEventListener("message", (event) => {
|
|
// Echo back with a response to test bidirectional communication
|
|
if (event.data === "ping") {
|
|
server.send("pong");
|
|
}
|
|
});
|
|
|
|
return new Response(null, { status: 101, webSocket: client });
|
|
}
|
|
|
|
// This test only focuses on WebSocket, no HTTP handling needed
|
|
return new Response("Not a WebSocket request", { status: 400 });
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(remote);
|
|
|
|
await remote.ready;
|
|
await vi.waitFor(
|
|
async () => {
|
|
const res = await local.dispatchFetch("http://example.com");
|
|
const result = await res.text();
|
|
expect(result).toBe("WebSocket communication successful: pong");
|
|
expect(res.status).toBe(200);
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
});
|
|
|
|
test("RPC to default entrypoint", async ({ expect }) => {
|
|
const unsafeDevRegistryPath = await useTmp();
|
|
const local = new Miniflare({
|
|
name: "local-worker",
|
|
unsafeDevRegistryPath,
|
|
serviceBindings: {
|
|
SERVICE: {
|
|
name: "remote-worker",
|
|
},
|
|
},
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
export default {
|
|
async fetch(request, env, ctx) {
|
|
try {
|
|
const result = await env.SERVICE.ping();
|
|
return new Response("Response from remote worker: " + result);
|
|
} catch (e) {
|
|
return new Response(e.message, { status: 500 });
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(local);
|
|
|
|
const res = await local.dispatchFetch("http://placeholder");
|
|
expect(await res.text()).toBe(
|
|
`Worker "remote-worker" not found. Make sure it is running locally.`
|
|
);
|
|
expect(res.status).toBe(500);
|
|
|
|
const remote = new Miniflare({
|
|
name: "remote-worker",
|
|
unsafeDevRegistryPath,
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
export default class TestEntrypoint extends WorkerEntrypoint {
|
|
ping() { return "pong"; }
|
|
}
|
|
`,
|
|
});
|
|
|
|
await remote.ready;
|
|
await vi.waitFor(
|
|
async () => {
|
|
const res = await local.dispatchFetch("http://placeholder");
|
|
const result = await res.text();
|
|
expect(result).toBe("Response from remote worker: pong");
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
|
|
// Kill the remote worker to see if it fails gracefully
|
|
await remote.dispose();
|
|
await vi.waitFor(
|
|
async () => {
|
|
const res = await local.dispatchFetch("http://placeholder");
|
|
expect(await res.text()).toBe(
|
|
`Worker "remote-worker" not found. Make sure it is running locally.`
|
|
);
|
|
expect(res.status).toBe(500);
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
});
|
|
|
|
test("RPC to custom entrypoint", async ({ expect }) => {
|
|
const unsafeDevRegistryPath = await useTmp();
|
|
const local = new Miniflare({
|
|
name: "local-worker",
|
|
unsafeDevRegistryPath,
|
|
serviceBindings: {
|
|
SERVICE: {
|
|
name: "remote-worker",
|
|
entrypoint: "TestEntrypoint",
|
|
},
|
|
},
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
export default {
|
|
async fetch(request, env, ctx) {
|
|
try {
|
|
const result = await env.SERVICE.ping();
|
|
return new Response("Response from remote worker: " + result);
|
|
} catch (e) {
|
|
return new Response(e.message, { status: 500 });
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(local);
|
|
|
|
const res = await local.dispatchFetch("http://placeholder");
|
|
expect(await res.text()).toBe(
|
|
`Worker "remote-worker" not found. Make sure it is running locally.`
|
|
);
|
|
expect(res.status).toBe(500);
|
|
|
|
const remote = new Miniflare({
|
|
name: "remote-worker",
|
|
unsafeDevRegistryPath,
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
export class TestEntrypoint extends WorkerEntrypoint {
|
|
ping() { return "pong"; }
|
|
}
|
|
`,
|
|
});
|
|
|
|
await remote.ready;
|
|
await vi.waitFor(
|
|
async () => {
|
|
const res = await local.dispatchFetch("http://placeholder");
|
|
const result = await res.text();
|
|
expect(result).toBe("Response from remote worker: pong");
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
|
|
await remote.dispose();
|
|
await vi.waitFor(
|
|
async () => {
|
|
const res = await local.dispatchFetch("http://placeholder");
|
|
expect(await res.text()).toBe(
|
|
`Worker "remote-worker" not found. Make sure it is running locally.`
|
|
);
|
|
expect(res.status).toBe(500);
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
});
|
|
|
|
test("service binding props are propagated across instances", async ({
|
|
expect,
|
|
}) => {
|
|
const unsafeDevRegistryPath = await useTmp();
|
|
const remote = new Miniflare({
|
|
name: "remote-worker",
|
|
unsafeDevRegistryPath,
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
export class PropsEntrypoint extends WorkerEntrypoint {
|
|
getProps() { return this.ctx.props; }
|
|
}
|
|
`,
|
|
});
|
|
useDispose(remote);
|
|
await remote.ready;
|
|
|
|
const local = new Miniflare({
|
|
name: "local-worker",
|
|
unsafeDevRegistryPath,
|
|
serviceBindings: {
|
|
SERVICE: {
|
|
name: "remote-worker",
|
|
entrypoint: "PropsEntrypoint",
|
|
props: { foo: 123, bar: { baz: "hello from props" } },
|
|
},
|
|
},
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
export default {
|
|
async fetch(request, env) {
|
|
return Response.json(await env.SERVICE.getProps());
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(local);
|
|
|
|
await vi.waitFor(
|
|
async () => {
|
|
const res = await local.dispatchFetch("http://placeholder");
|
|
const json = await res.json();
|
|
expect(res.status).toBe(200);
|
|
expect(json).toEqual({
|
|
foo: 123,
|
|
bar: { baz: "hello from props" },
|
|
});
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
});
|
|
|
|
test("fetch to module worker with node bindings", async ({ expect }) => {
|
|
const unsafeDevRegistryPath = await useTmp();
|
|
const local = new Miniflare({
|
|
name: "local-worker",
|
|
unsafeDevRegistryPath,
|
|
serviceBindings: {
|
|
SERVICE: {
|
|
name: "remote-worker",
|
|
},
|
|
},
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
export default {
|
|
async fetch(request, env, ctx) {
|
|
return new Response("Not implemented", { status: 501 });
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(local);
|
|
|
|
const bindings = await local.getBindings<Record<string, any>>();
|
|
|
|
await vi.waitFor(
|
|
async () => {
|
|
const res = await bindings.SERVICE.fetch(
|
|
"http://example.com?name=World"
|
|
);
|
|
expect(await res.text()).toBe(
|
|
`Worker "remote-worker" not found. Make sure it is running locally.`
|
|
);
|
|
expect(res.status).toBe(503);
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
|
|
const remote = new Miniflare({
|
|
name: "remote-worker",
|
|
unsafeDevRegistryPath,
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
export default {
|
|
async fetch(request, env, ctx) {
|
|
const url = new URL(request.url);
|
|
const name = url.searchParams.get("name") ?? 'anonymous';
|
|
|
|
return new Response("Hello " + name);
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
|
|
await remote.ready;
|
|
await vi.waitFor(
|
|
async () => {
|
|
const res = await bindings.SERVICE.fetch(
|
|
"http://example.com?name=World"
|
|
);
|
|
const result = await res.text();
|
|
expect(result).toBe("Hello World");
|
|
expect(res.status).toBe(200);
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
|
|
await remote.dispose();
|
|
await vi.waitFor(
|
|
async () => {
|
|
const res = await bindings.SERVICE.fetch(
|
|
"http://example.com?name=World"
|
|
);
|
|
expect(await res.text()).toBe(
|
|
`Worker "remote-worker" not found. Make sure it is running locally.`
|
|
);
|
|
expect(res.status).toBe(503);
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
});
|
|
|
|
test("RPC to default entrypoint with node bindings", async ({ expect }) => {
|
|
const unsafeDevRegistryPath = await useTmp();
|
|
const local = new Miniflare({
|
|
name: "local-worker",
|
|
unsafeDevRegistryPath,
|
|
serviceBindings: {
|
|
SERVICE: {
|
|
name: "remote-worker",
|
|
},
|
|
},
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
export default {
|
|
async fetch(request, env, ctx) {
|
|
return new Response("Not implemented", { status: 501 });
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(local);
|
|
|
|
const env = await local.getBindings<Record<string, any>>();
|
|
|
|
await vi.waitFor(
|
|
async () => {
|
|
try {
|
|
const result = await env.SERVICE.ping();
|
|
throw new Error(`Expected error, got result: ${result}`);
|
|
} catch (e) {
|
|
expect(e instanceof Error ? e.message : `${e}`).toBe(
|
|
`Worker "remote-worker" not found. Make sure it is running locally.`
|
|
);
|
|
}
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
|
|
const remote = new Miniflare({
|
|
name: "remote-worker",
|
|
unsafeDevRegistryPath,
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
export default class TestEntrypoint extends WorkerEntrypoint {
|
|
ping() { return "pong"; }
|
|
}
|
|
`,
|
|
});
|
|
|
|
await remote.ready;
|
|
await vi.waitFor(
|
|
async () => {
|
|
const result = await env.SERVICE.ping();
|
|
expect(result).toBe("pong");
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
|
|
// Kill the remote worker to see if it fails gracefully
|
|
await remote.dispose();
|
|
await vi.waitFor(
|
|
async () => {
|
|
try {
|
|
const result = await env.SERVICE.ping();
|
|
throw new Error(`Expected error, got result: ${result}`);
|
|
} catch (e) {
|
|
expect(e instanceof Error ? e.message : `${e}`).toBe(
|
|
`Worker "remote-worker" not found. Make sure it is running locally.`
|
|
);
|
|
}
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
});
|
|
|
|
test("fetch to durable object with remote running", async ({ expect }) => {
|
|
const unsafeDevRegistryPath = await useTmp();
|
|
const remote = new Miniflare({
|
|
name: "remote-worker",
|
|
unsafeDevRegistryPath,
|
|
|
|
compatibilityFlags: ["experimental"],
|
|
durableObjects: {
|
|
DO: {
|
|
className: "MyDurableObject",
|
|
},
|
|
},
|
|
modules: true,
|
|
script: `
|
|
import { DurableObject } from "cloudflare:workers";
|
|
export class MyDurableObject extends DurableObject {
|
|
fetch() {
|
|
return new Response('Hello from Durable Object!');
|
|
}
|
|
};
|
|
|
|
export default {
|
|
async fetch(request, env, ctx) {
|
|
return new Response("Hello from the default Worker Entrypoint!");
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(remote);
|
|
|
|
await remote.ready;
|
|
|
|
const local = new Miniflare({
|
|
name: "local-worker",
|
|
unsafeDevRegistryPath,
|
|
|
|
durableObjects: {
|
|
DO: {
|
|
className: "MyDurableObject",
|
|
scriptName: "remote-worker",
|
|
},
|
|
},
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
export default {
|
|
async fetch(request, env, ctx) {
|
|
const ns = env.DO;
|
|
const id = ns.newUniqueId();
|
|
const stub = ns.get(id);
|
|
return stub.fetch(request);
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(local);
|
|
|
|
await vi.waitFor(
|
|
async () => {
|
|
const res = await local.dispatchFetch("http://placeholder");
|
|
expect(await res.text()).toBe("Hello from Durable Object!");
|
|
expect(res.status).toBe(200);
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
});
|
|
|
|
test("RPC to durable object with remote running", async ({ expect }) => {
|
|
const unsafeDevRegistryPath = await useTmp();
|
|
const remote = new Miniflare({
|
|
name: "remote-worker",
|
|
unsafeDevRegistryPath,
|
|
|
|
compatibilityFlags: ["experimental"],
|
|
durableObjects: {
|
|
DO: {
|
|
className: "MyDurableObject",
|
|
},
|
|
},
|
|
modules: true,
|
|
script: `
|
|
import { DurableObject } from "cloudflare:workers";
|
|
export class MyDurableObject extends DurableObject {
|
|
ping() {
|
|
return "pong";
|
|
}
|
|
};
|
|
`,
|
|
});
|
|
useDispose(remote);
|
|
|
|
await remote.ready;
|
|
|
|
const local = new Miniflare({
|
|
name: "local-worker",
|
|
unsafeDevRegistryPath,
|
|
|
|
durableObjects: {
|
|
DO: {
|
|
className: "MyDurableObject",
|
|
scriptName: "remote-worker",
|
|
},
|
|
},
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
export default {
|
|
async fetch(request, env, ctx) {
|
|
try {
|
|
const ns = env.DO;
|
|
const id = ns.newUniqueId();
|
|
const stub = ns.get(id);
|
|
const result = await stub.ping();
|
|
|
|
return new Response(result);
|
|
} catch (ex) {
|
|
return new Response(ex.message, { status: 500 });
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(local);
|
|
|
|
await vi.waitFor(
|
|
async () => {
|
|
const res = await local.dispatchFetch("http://placeholder");
|
|
expect(await res.text()).toBe("pong");
|
|
expect(res.status).toBe(200);
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
});
|
|
|
|
test("fetch to durable object", async ({ expect }) => {
|
|
const unsafeDevRegistryPath = await useTmp();
|
|
const local = new Miniflare({
|
|
name: "local-worker",
|
|
unsafeDevRegistryPath,
|
|
|
|
durableObjects: {
|
|
DO: {
|
|
className: "MyDurableObject",
|
|
scriptName: "remote-worker",
|
|
},
|
|
},
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
export default {
|
|
async fetch(request, env, ctx) {
|
|
const ns = env.DO;
|
|
const id = ns.newUniqueId();
|
|
const stub = ns.get(id);
|
|
const response = await stub.fetch(request);
|
|
|
|
return response;
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(local);
|
|
|
|
const res = await local.dispatchFetch("http://placeholder");
|
|
expect(await res.text()).toBe(
|
|
`Worker "remote-worker" not found. Make sure it is running locally.`
|
|
);
|
|
expect(res.status).toBe(503);
|
|
|
|
const remote = new Miniflare({
|
|
name: "remote-worker",
|
|
unsafeDevRegistryPath,
|
|
|
|
compatibilityFlags: ["experimental"],
|
|
durableObjects: {
|
|
DO: {
|
|
className: "MyDurableObject",
|
|
},
|
|
},
|
|
modules: true,
|
|
script: `
|
|
import { DurableObject } from "cloudflare:workers";
|
|
export class MyDurableObject extends DurableObject {
|
|
fetch() {
|
|
return new Response('Hello from Durable Object!');
|
|
}
|
|
};
|
|
|
|
export default {
|
|
async fetch(request, env, ctx) {
|
|
return new Response("Hello from the default Worker Entrypoint!");
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(remote);
|
|
|
|
await remote.ready;
|
|
await vi.waitFor(
|
|
async () => {
|
|
const res = await local.dispatchFetch("http://placeholder");
|
|
expect(await res.text()).toBe("Hello from Durable Object!");
|
|
expect(res.status).toBe(200);
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
});
|
|
|
|
test("RPC to durable object", async ({ expect }) => {
|
|
const unsafeDevRegistryPath = await useTmp();
|
|
const local = new Miniflare({
|
|
name: "local-worker",
|
|
unsafeDevRegistryPath,
|
|
|
|
durableObjects: {
|
|
DO: {
|
|
className: "MyDurableObject",
|
|
scriptName: "remote-worker",
|
|
},
|
|
},
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
export default {
|
|
async fetch(request, env, ctx) {
|
|
try {
|
|
const ns = env.DO;
|
|
const id = ns.newUniqueId();
|
|
const stub = ns.get(id);
|
|
const result = await stub.ping();
|
|
|
|
return new Response("Response from remote worker: " + result);
|
|
} catch (ex) {
|
|
return new Response(ex.message, { status: 500 });
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(local);
|
|
|
|
const res = await local.dispatchFetch("http://placeholder");
|
|
expect(await res.text()).toContain(
|
|
`Worker "remote-worker" not found. Make sure it is running locally.`
|
|
);
|
|
expect(res.status).toBe(500);
|
|
|
|
const remote = new Miniflare({
|
|
name: "remote-worker",
|
|
unsafeDevRegistryPath,
|
|
|
|
compatibilityFlags: ["experimental"],
|
|
durableObjects: {
|
|
DO: {
|
|
className: "MyDurableObject",
|
|
},
|
|
},
|
|
modules: true,
|
|
script: `
|
|
import { DurableObject } from "cloudflare:workers";
|
|
export class MyDurableObject extends DurableObject {
|
|
ping() {
|
|
return "pong";
|
|
}
|
|
};
|
|
`,
|
|
});
|
|
useDispose(remote);
|
|
|
|
await remote.ready;
|
|
// DO RPC now works natively via the debug port
|
|
await vi.waitFor(
|
|
async () => {
|
|
const res = await local.dispatchFetch("http://placeholder");
|
|
expect(await res.text()).toBe("Response from remote worker: pong");
|
|
expect(res.status).toBe(200);
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
});
|
|
|
|
test("workflow with cross-worker scriptName", async ({ expect }) => {
|
|
const unsafeDevRegistryPath = await useTmp();
|
|
const local = new Miniflare({
|
|
name: "local-worker",
|
|
unsafeDevRegistryPath,
|
|
|
|
workflows: {
|
|
MY_WORKFLOW: {
|
|
name: "MY_WORKFLOW",
|
|
className: "MyWorkflow",
|
|
scriptName: "remote-worker",
|
|
},
|
|
},
|
|
compatibilityDate: "2024-11-20",
|
|
modules: true,
|
|
script: `
|
|
export default {
|
|
async fetch(request, env, ctx) {
|
|
const instance = await env.MY_WORKFLOW.create({ id: "cross-worker-instance" });
|
|
return Response.json({ id: instance.id });
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(local);
|
|
|
|
const remote = new Miniflare({
|
|
name: "remote-worker",
|
|
unsafeDevRegistryPath,
|
|
|
|
workflows: {
|
|
MY_WORKFLOW: {
|
|
name: "MY_WORKFLOW",
|
|
className: "MyWorkflow",
|
|
},
|
|
},
|
|
compatibilityDate: "2024-11-20",
|
|
modules: true,
|
|
script: `
|
|
import { WorkflowEntrypoint } from "cloudflare:workers";
|
|
export class MyWorkflow extends WorkflowEntrypoint {
|
|
async run(event, step) {
|
|
return await step.do("hello", async () => "from remote workflow");
|
|
}
|
|
}
|
|
export default {
|
|
async fetch() { return new Response("ok"); }
|
|
}
|
|
`,
|
|
});
|
|
useDispose(remote);
|
|
|
|
await remote.ready;
|
|
// Workflow `create()` is queued asynchronously by the engine; routing
|
|
// goes engine (local) → ExternalServiceProxy → dev registry → remote
|
|
// MyWorkflow.run. Verify the instance is created with the expected id.
|
|
await vi.waitFor(
|
|
async () => {
|
|
const res = await local.dispatchFetch("http://placeholder");
|
|
expect(res.status).toBe(200);
|
|
expect(await res.json()).toEqual({ id: "cross-worker-instance" });
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
});
|
|
|
|
test("DO RPC survives remote worker restart", async ({ expect }) => {
|
|
const unsafeDevRegistryPath = await useTmp();
|
|
|
|
const remote = new Miniflare({
|
|
name: "remote-worker",
|
|
unsafeDevRegistryPath,
|
|
compatibilityFlags: ["experimental"],
|
|
durableObjects: {
|
|
DO: {
|
|
className: "MyDurableObject",
|
|
scriptName: "remote-worker",
|
|
},
|
|
},
|
|
modules: true,
|
|
script: `
|
|
import { DurableObject } from "cloudflare:workers";
|
|
export class MyDurableObject extends DurableObject {
|
|
ping() { return "v1"; }
|
|
}
|
|
export default { fetch() { return new Response("ok"); } }
|
|
`,
|
|
});
|
|
useDispose(remote);
|
|
await remote.ready;
|
|
|
|
const local = new Miniflare({
|
|
name: "local-worker",
|
|
unsafeDevRegistryPath,
|
|
durableObjects: {
|
|
DO: {
|
|
className: "MyDurableObject",
|
|
scriptName: "remote-worker",
|
|
},
|
|
},
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
// Use idFromName so the same DO instance is reused across requests —
|
|
// this ensures the cached _cachedFetcher is exercised on the second call.
|
|
script: `
|
|
export default {
|
|
async fetch(request, env) {
|
|
try {
|
|
const id = env.DO.idFromName("stable");
|
|
const stub = env.DO.get(id);
|
|
const result = await stub.ping();
|
|
return new Response("Response: " + result);
|
|
} catch (ex) {
|
|
return new Response(ex.message, { status: 500 });
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(local);
|
|
|
|
// First request — caches the fetcher in the DO proxy instance
|
|
await vi.waitFor(
|
|
async () => {
|
|
const res = await local.dispatchFetch("http://placeholder");
|
|
expect(await res.text()).toBe("Response: v1");
|
|
expect(res.status).toBe(200);
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
|
|
// Restart remote — gets a new debug port, registry file updates
|
|
await remote.setOptions({
|
|
name: "remote-worker",
|
|
unsafeDevRegistryPath,
|
|
compatibilityFlags: ["experimental"],
|
|
durableObjects: {
|
|
DO: {
|
|
className: "MyDurableObject",
|
|
scriptName: "remote-worker",
|
|
},
|
|
},
|
|
modules: true,
|
|
script: `
|
|
import { DurableObject } from "cloudflare:workers";
|
|
export class MyDurableObject extends DurableObject {
|
|
ping() { return "v2"; }
|
|
}
|
|
export default { fetch() { return new Response("ok"); } }
|
|
`,
|
|
});
|
|
|
|
// Second request — same DO instance (idFromName("stable")),
|
|
// but remote debug port has changed. The cached _cachedFetcher
|
|
// points to the old dead connection.
|
|
await vi.waitFor(
|
|
async () => {
|
|
const res = await local.dispatchFetch("http://placeholder");
|
|
expect(await res.text()).toBe("Response: v2");
|
|
expect(res.status).toBe(200);
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
});
|
|
|
|
test("service binding RPC survives remote worker restart", async ({
|
|
expect,
|
|
}) => {
|
|
const unsafeDevRegistryPath = await useTmp();
|
|
|
|
const remote = new Miniflare({
|
|
name: "remote-worker",
|
|
unsafeDevRegistryPath,
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
export default class extends WorkerEntrypoint {
|
|
ping() { return "v1"; }
|
|
}
|
|
`,
|
|
});
|
|
useDispose(remote);
|
|
await remote.ready;
|
|
|
|
const local = new Miniflare({
|
|
name: "local-worker",
|
|
unsafeDevRegistryPath,
|
|
serviceBindings: {
|
|
SERVICE: "remote-worker",
|
|
},
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
export default {
|
|
async fetch(request, env) {
|
|
try {
|
|
const result = await env.SERVICE.ping();
|
|
return new Response("Response: " + result);
|
|
} catch (e) {
|
|
return new Response(e.message, { status: 500 });
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(local);
|
|
|
|
// First request — ExternalServiceProxy resolves from registry
|
|
await vi.waitFor(
|
|
async () => {
|
|
const res = await local.dispatchFetch("http://placeholder");
|
|
expect(await res.text()).toBe("Response: v1");
|
|
expect(res.status).toBe(200);
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
|
|
// Restart remote — gets a new debug port, registry file updates
|
|
await remote.setOptions({
|
|
name: "remote-worker",
|
|
unsafeDevRegistryPath,
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
export default class extends WorkerEntrypoint {
|
|
ping() { return "v2"; }
|
|
}
|
|
`,
|
|
});
|
|
|
|
// Second request — ExternalServiceProxy is re-instantiated per request,
|
|
// so it reads the updated registry Map and connects to the new debug port.
|
|
await vi.waitFor(
|
|
async () => {
|
|
const res = await local.dispatchFetch("http://placeholder");
|
|
expect(await res.text()).toBe("Response: v2");
|
|
expect(res.status).toBe(200);
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
});
|
|
|
|
test("scheduled to default entrypoint", async ({ expect }) => {
|
|
const unsafeDevRegistryPath = await useTmp();
|
|
const remote = new Miniflare({
|
|
name: "remote-worker",
|
|
unsafeDevRegistryPath,
|
|
unsafeTriggerHandlers: true,
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
let resolve, reject;
|
|
const promise = new Promise((res, rej) => {
|
|
resolve = res;
|
|
reject = rej;
|
|
});
|
|
export default {
|
|
async fetch() {
|
|
try {
|
|
const event = await Promise.race([
|
|
promise,
|
|
new Promise((_, cancel) => setTimeout(cancel, 1000))
|
|
]);
|
|
return Response.json(event);
|
|
} catch {
|
|
return new Response("No scheduled event received", { status: 500 });
|
|
}
|
|
},
|
|
scheduled(e) {
|
|
resolve({ cron: e.cron, scheduledTime: e.scheduledTime });
|
|
}
|
|
};
|
|
`,
|
|
});
|
|
useDispose(remote);
|
|
await remote.ready;
|
|
|
|
const local = new Miniflare({
|
|
name: "local-worker",
|
|
unsafeDevRegistryPath,
|
|
serviceBindings: {
|
|
REMOTE: "remote-worker",
|
|
},
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
export default {
|
|
async fetch(request, env) {
|
|
await env.REMOTE.scheduled({ cron: "*/5 * * * *" });
|
|
return new Response("scheduled event dispatched");
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(local);
|
|
|
|
// Check scheduled() was dispatched
|
|
await (await local.dispatchFetch("http://placeholder")).text();
|
|
const res = await remote.dispatchFetch("http://placeholder");
|
|
const json = (await res.json()) as { cron: string; scheduledTime: number };
|
|
expect(json).toMatchObject({ cron: "*/5 * * * *" });
|
|
expect(typeof json.scheduledTime).toBe("number");
|
|
});
|
|
|
|
test("tail to default entrypoint", async ({ expect }) => {
|
|
const unsafeDevRegistryPath = await useTmp();
|
|
const remote = new Miniflare({
|
|
name: "remote-worker",
|
|
unsafeDevRegistryPath,
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
let resolve, reject;
|
|
const promise = new Promise((res, rej) => {
|
|
resolve = res;
|
|
reject = rej;
|
|
});
|
|
export default {
|
|
async fetch() {
|
|
try {
|
|
const event = await Promise.race([
|
|
promise,
|
|
new Promise((_, cancel) => setTimeout(cancel, 1000))
|
|
]);
|
|
return Response.json(event[0].logs[0].message);
|
|
} catch {
|
|
return new Response("No tail event received", { status: 500 });
|
|
}
|
|
},
|
|
tail(e) {
|
|
resolve(e);
|
|
}
|
|
};
|
|
`,
|
|
});
|
|
useDispose(remote);
|
|
|
|
await remote.ready;
|
|
|
|
const local = new Miniflare({
|
|
name: "local-worker",
|
|
unsafeDevRegistryPath,
|
|
tails: ["remote-worker"],
|
|
serviceBindings: {
|
|
remote: "remote-worker",
|
|
},
|
|
handleRuntimeStdio: () => {},
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
export default {
|
|
async fetch(request, env) {
|
|
if (request.url.includes("remote-worker")) {
|
|
return env.remote.fetch(request)
|
|
}
|
|
console.log("DevReg: log event")
|
|
return new Response("Hello from local-worker!");
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(local);
|
|
|
|
const res = await local.dispatchFetch("http://example.com");
|
|
const result = await res.text();
|
|
expect(result).toBe("Hello from local-worker!");
|
|
|
|
const res2 = await local.dispatchFetch("http://example.com/remote-worker");
|
|
const result2 = await res2.text();
|
|
|
|
expect(result2).toEqual(`["DevReg: log event"]`);
|
|
});
|
|
|
|
test("tail to unknown worker", async ({ expect }) => {
|
|
const unsafeDevRegistryPath = await useTmp();
|
|
const mf = new Miniflare({
|
|
name: "local-worker",
|
|
unsafeDevRegistryPath,
|
|
tails: ["remote-worker"],
|
|
serviceBindings: {
|
|
remote: "remote-worker",
|
|
},
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
handleRuntimeStdio: () => {},
|
|
script: `
|
|
export default {
|
|
async fetch(request, env) {
|
|
if (request.url.includes("remote-worker")) {
|
|
return env.remote.fetch(request)
|
|
}
|
|
console.log("DevReg: log event 2")
|
|
return new Response("Hello from local-worker!");
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(mf);
|
|
|
|
const res = await mf.dispatchFetch("http://example.com");
|
|
const result = await res.text();
|
|
expect(result).toBe("Hello from local-worker!");
|
|
|
|
const res2 = await mf.dispatchFetch("http://example.com/remote-worker");
|
|
const result2 = await res2.text();
|
|
|
|
expect(result2).toEqual(
|
|
`Worker "remote-worker" not found. Make sure it is running locally.`
|
|
);
|
|
});
|
|
|
|
test("tail consumer props are propagated across instances", async ({
|
|
expect,
|
|
}) => {
|
|
const unsafeDevRegistryPath = await useTmp();
|
|
const remote = new Miniflare({
|
|
name: "remote-worker",
|
|
unsafeDevRegistryPath,
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
let resolve;
|
|
const captured = new Promise((res) => { resolve = res; });
|
|
export default {
|
|
async fetch() {
|
|
try {
|
|
const result = await Promise.race([
|
|
captured,
|
|
new Promise((_, cancel) =>
|
|
setTimeout(() => cancel(new Error("no tail event received")), 2000)
|
|
)
|
|
]);
|
|
return Response.json(result);
|
|
} catch (e) {
|
|
return new Response(e.message, { status: 500 });
|
|
}
|
|
}
|
|
};
|
|
export class TailCollector extends WorkerEntrypoint {
|
|
async tail() {
|
|
resolve({ props: this.ctx.props ?? null });
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(remote);
|
|
await remote.ready;
|
|
|
|
const local = new Miniflare({
|
|
name: "local-worker",
|
|
unsafeDevRegistryPath,
|
|
tails: [
|
|
{
|
|
name: "remote-worker",
|
|
entrypoint: "TailCollector",
|
|
props: { tailKey: "from-tail-binding" },
|
|
},
|
|
],
|
|
handleRuntimeStdio: () => {},
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
export default {
|
|
async fetch() {
|
|
console.log("DevReg: trigger tail event");
|
|
return new Response("ok");
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(local);
|
|
|
|
await vi.waitFor(
|
|
async () => {
|
|
// Fire a request that produces a log, which triggers a tail event
|
|
// targeting the remote worker's TailCollector entrypoint.
|
|
const trigger = await local.dispatchFetch("http://example.com");
|
|
expect(await trigger.text()).toBe("ok");
|
|
|
|
// Read the captured ctx.props back via the remote's default fetch.
|
|
const res = await remote.dispatchFetch("http://placeholder");
|
|
const json = await res.json();
|
|
expect(res.status).toBe(200);
|
|
expect(json).toEqual({
|
|
props: { tailKey: "from-tail-binding" },
|
|
});
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
});
|
|
|
|
test("miniflare with different registry path", async ({ expect }) => {
|
|
const unsafeDevRegistryPath = await useTmp();
|
|
const unsafeDevRegistryPath2 = await useTmp();
|
|
const localOptions: MiniflareOptions = {
|
|
name: "local-worker",
|
|
serviceBindings: {
|
|
SERVICE: {
|
|
name: "remote-worker",
|
|
},
|
|
},
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
export default {
|
|
async fetch(request, env, ctx) {
|
|
try {
|
|
const result = await env.SERVICE.ping();
|
|
return new Response("Response from remote worker: " + result);
|
|
} catch (e) {
|
|
return new Response(e.message, { status: 500 });
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
};
|
|
const remoteOptions: MiniflareOptions = {
|
|
name: "remote-worker",
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
export default class TestEntrypoint extends WorkerEntrypoint {
|
|
ping() { return "pong"; }
|
|
}
|
|
`,
|
|
};
|
|
|
|
const local = new Miniflare({
|
|
...localOptions,
|
|
unsafeDevRegistryPath,
|
|
});
|
|
useDispose(local);
|
|
|
|
const res = await local.dispatchFetch("http://placeholder");
|
|
expect(await res.text()).toBe(
|
|
`Worker "remote-worker" not found. Make sure it is running locally.`
|
|
);
|
|
expect(res.status).toBe(500);
|
|
|
|
const remote = new Miniflare({
|
|
...remoteOptions,
|
|
unsafeDevRegistryPath,
|
|
});
|
|
useDispose(remote);
|
|
|
|
await remote.ready;
|
|
await vi.waitFor(
|
|
async () => {
|
|
const res = await local.dispatchFetch("http://placeholder");
|
|
const result = await res.text();
|
|
expect(result).toBe("Response from remote worker: pong");
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
|
|
// Change remote's registry path to a different value
|
|
await remote.setOptions({
|
|
...remoteOptions,
|
|
unsafeDevRegistryPath: unsafeDevRegistryPath2,
|
|
});
|
|
await vi.waitFor(
|
|
async () => {
|
|
const res = await local.dispatchFetch("http://placeholder");
|
|
expect(await res.text()).toBe(
|
|
`Worker "remote-worker" not found. Make sure it is running locally.`
|
|
);
|
|
expect(res.status).toBe(500);
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
|
|
// Change local's registry path to the same path as remote's
|
|
await local.setOptions({
|
|
...localOptions,
|
|
unsafeDevRegistryPath: unsafeDevRegistryPath2,
|
|
});
|
|
await vi.waitFor(
|
|
async () => {
|
|
const res = await local.dispatchFetch("http://placeholder");
|
|
const result = await res.text();
|
|
expect(result).toBe("Response from remote worker: pong");
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
});
|
|
|
|
test("fetch to module worker with https enabled", async ({ expect }) => {
|
|
const unsafeDevRegistryPath = await useTmp();
|
|
const local = new Miniflare({
|
|
name: "local-worker",
|
|
unsafeDevRegistryPath,
|
|
serviceBindings: {
|
|
SERVICE: {
|
|
name: "remote-worker",
|
|
},
|
|
},
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
export default {
|
|
async fetch(request, env, ctx) {
|
|
const response = await env.SERVICE.fetch(request.url);
|
|
const text = await response.text();
|
|
|
|
return new Response("Response from remote worker: " + text, {
|
|
status: response.status,
|
|
});
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(local);
|
|
|
|
const res = await local.dispatchFetch("https://example.com?name=World");
|
|
|
|
expect(await res.text()).toBe(
|
|
`Response from remote worker: Worker "remote-worker" not found. Make sure it is running locally.`
|
|
);
|
|
expect(res.status).toBe(503);
|
|
|
|
const remote = new Miniflare({
|
|
name: "remote-worker",
|
|
unsafeDevRegistryPath,
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
https: true,
|
|
script: `
|
|
export default {
|
|
async fetch(request, env, ctx) {
|
|
const url = new URL(request.url);
|
|
const name = url.searchParams.get("name") ?? 'anonymous';
|
|
|
|
return new Response("Hello " + name);
|
|
}
|
|
}
|
|
`,
|
|
// No direct sockets so that local will connect to the entry worker instead
|
|
});
|
|
useDispose(remote);
|
|
|
|
await remote.ready;
|
|
await vi.waitFor(
|
|
async () => {
|
|
const res = await local.dispatchFetch("https://example.com?name=World");
|
|
const result = await res.text();
|
|
expect(result).toBe("Response from remote worker: Hello World");
|
|
expect(res.status).toBe(200);
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
});
|
|
|
|
test("fetch to durable object with https enabled", async ({ expect }) => {
|
|
const unsafeDevRegistryPath = await useTmp();
|
|
const local = new Miniflare({
|
|
name: "local-worker",
|
|
unsafeDevRegistryPath,
|
|
|
|
durableObjects: {
|
|
DO: {
|
|
className: "MyDurableObject",
|
|
scriptName: "remote-worker",
|
|
},
|
|
},
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
export default {
|
|
async fetch(request, env, ctx) {
|
|
const ns = env.DO;
|
|
const id = ns.newUniqueId();
|
|
const stub = ns.get(id);
|
|
const response = await stub.fetch(request);
|
|
|
|
return response;
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(local);
|
|
|
|
const res = await local.dispatchFetch("http://placeholder");
|
|
expect(await res.text()).toBe(
|
|
`Worker "remote-worker" not found. Make sure it is running locally.`
|
|
);
|
|
expect(res.status).toBe(503);
|
|
|
|
const remote = new Miniflare({
|
|
name: "remote-worker",
|
|
unsafeDevRegistryPath,
|
|
|
|
https: true,
|
|
compatibilityFlags: ["experimental"],
|
|
durableObjects: {
|
|
DO: {
|
|
className: "MyDurableObject",
|
|
},
|
|
},
|
|
modules: true,
|
|
script: `
|
|
import { DurableObject } from "cloudflare:workers";
|
|
export class MyDurableObject extends DurableObject {
|
|
fetch() {
|
|
return new Response('Hello from Durable Object!');
|
|
}
|
|
};
|
|
|
|
export default {
|
|
async fetch(request, env, ctx) {
|
|
return new Response("Hello from the default Worker Entrypoint!");
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(remote);
|
|
|
|
await remote.ready;
|
|
await vi.waitFor(
|
|
async () => {
|
|
const res = await local.dispatchFetch("http://placeholder");
|
|
expect(await res.text()).toBe("Hello from Durable Object!");
|
|
expect(res.status).toBe(200);
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
});
|
|
|
|
test("handleDevRegistryUpdate callback", async ({ expect }) => {
|
|
const unsafeDevRegistryPath = await useTmp();
|
|
const firstCallbackInvocations: Array<{
|
|
registry: WorkerRegistry;
|
|
}> = [];
|
|
const secondCallbackInvocations: Array<{
|
|
registry: WorkerRegistry;
|
|
}> = [];
|
|
|
|
// Create local Worker with service binding and callback
|
|
const local = new Miniflare({
|
|
name: "local-worker",
|
|
unsafeDevRegistryPath,
|
|
unsafeHandleDevRegistryUpdate(registry) {
|
|
firstCallbackInvocations.push({ registry });
|
|
},
|
|
serviceBindings: {
|
|
SERVICE: {
|
|
name: "remote-worker",
|
|
},
|
|
},
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
export default {
|
|
async fetch(request, env, ctx) {
|
|
try {
|
|
const result = await env.SERVICE.ping();
|
|
return new Response("Response from remote Worker: " + result);
|
|
} catch (e) {
|
|
return new Response(e.message, { status: 500 });
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(local);
|
|
|
|
// Callback should not be triggered initially since no external services exist
|
|
expect(firstCallbackInvocations.length).toBe(0);
|
|
expect(secondCallbackInvocations.length).toBe(0);
|
|
|
|
// Create an unrelated Worker - callback should NOT be triggered
|
|
const unrelated = new Miniflare({
|
|
name: "unrelated-worker",
|
|
unsafeDevRegistryPath,
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
export default {
|
|
async fetch() {
|
|
return new Response("Hello from unrelated-worker!");
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
useDispose(unrelated);
|
|
|
|
await unrelated.ready;
|
|
|
|
// Callback should not be triggered since we're not bound to unrelated-worker
|
|
expect(firstCallbackInvocations.length).toBe(0);
|
|
expect(secondCallbackInvocations.length).toBe(0);
|
|
|
|
// Create remote worker (one we're actually bound to) - this should trigger the callback
|
|
const remote = new Miniflare({
|
|
name: "remote-worker",
|
|
unsafeDevRegistryPath,
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
export default class TestEntrypoint extends WorkerEntrypoint {
|
|
ping() { return "pong"; }
|
|
}
|
|
`,
|
|
});
|
|
onTestFinished(async () => {
|
|
try {
|
|
await remote.dispose();
|
|
} catch {
|
|
// Ignore if already disposed
|
|
}
|
|
});
|
|
|
|
await remote.ready;
|
|
|
|
// Wait for the callback to be triggered
|
|
await vi.waitFor(
|
|
async () => {
|
|
// Callback should be triggered when bound Worker starts
|
|
expect(firstCallbackInvocations.length).toBe(1);
|
|
// Second callback should not be triggered yet
|
|
expect(secondCallbackInvocations.length).toBe(0);
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
|
|
// Verify the callback was called with the correct registry data
|
|
await vi.waitFor(
|
|
async () => {
|
|
const latestInvocation = firstCallbackInvocations.at(-1);
|
|
// Registry should contain remote-worker
|
|
expect(latestInvocation?.registry["remote-worker"]).toBeDefined();
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
|
|
// Update unsafeHandleDevRegistryUpdate callback to push to a different array
|
|
await local.setOptions({
|
|
name: "local-worker",
|
|
unsafeDevRegistryPath,
|
|
unsafeHandleDevRegistryUpdate(registry) {
|
|
secondCallbackInvocations.push({
|
|
registry,
|
|
});
|
|
},
|
|
serviceBindings: {
|
|
SERVICE: {
|
|
name: "remote-worker",
|
|
},
|
|
},
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
script: `
|
|
export default {
|
|
async fetch(request, env, ctx) {
|
|
try {
|
|
const result = await env.SERVICE.ping();
|
|
return new Response("Response from updated local Worker: " + result);
|
|
} catch (e) {
|
|
return new Response(e.message, { status: 500 });
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
|
|
// Test disposal
|
|
await remote.dispose();
|
|
|
|
// Wait for callback to be triggered by the update
|
|
await vi.waitFor(
|
|
async () => {
|
|
// First callback should not be triggered again after update
|
|
expect(firstCallbackInvocations.length).toBe(1);
|
|
// Second callback should be triggered after update
|
|
expect(secondCallbackInvocations.length).toBe(1);
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
|
|
// Verify if the remote worker is no longer in the registry
|
|
await vi.waitFor(
|
|
async () => {
|
|
const latestInvocation = secondCallbackInvocations.at(-1);
|
|
// Registry should not contain remote-worker
|
|
expect(latestInvocation?.registry["remote-worker"]).toBeUndefined();
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
});
|
|
|
|
test("advertises consumed queues in the worker's registry entry", async ({
|
|
expect,
|
|
}) => {
|
|
const unsafeDevRegistryPath = await useTmp();
|
|
const sharedOptions = {
|
|
name: "consumer-worker",
|
|
unsafeDevRegistryPath,
|
|
compatibilityFlags: ["experimental"],
|
|
modules: true,
|
|
} satisfies Partial<MiniflareOptions>;
|
|
|
|
const mf = new Miniflare({
|
|
...sharedOptions,
|
|
// Consumed queues are advertised on the worker's own registry entry so
|
|
// producers in other processes can resolve this process's broker.
|
|
queueConsumers: ["my-queue"],
|
|
script: `export default { async queue() {} }`,
|
|
});
|
|
useDispose(mf);
|
|
await mf.ready;
|
|
|
|
await vi.waitFor(
|
|
() => {
|
|
const registry = getWorkerRegistry(unsafeDevRegistryPath);
|
|
expect(registry["consumer-worker"]?.queueConsumers).toEqual([
|
|
"my-queue",
|
|
]);
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
|
|
// Remove the consumer on reload so its advertisement is withdrawn and
|
|
// doesn't misdirect producers.
|
|
await mf.setOptions({
|
|
...sharedOptions,
|
|
script: `export default { async fetch() { return new Response("ok"); } }`,
|
|
});
|
|
|
|
await vi.waitFor(
|
|
() => {
|
|
const registry = getWorkerRegistry(unsafeDevRegistryPath);
|
|
// The worker's own entry is still advertised, without the queue.
|
|
expect(registry["consumer-worker"]).toBeDefined();
|
|
expect(registry["consumer-worker"].queueConsumers).toBeUndefined();
|
|
},
|
|
{ timeout: 10_000, interval: 100 }
|
|
);
|
|
});
|
|
});
|