Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit ff17f0c

Browse files
mrbbotRamIdeas
andauthored
[Miniflare 3] Add getFetcher() for dispatching fetch/scheduled/queue events (#708)
* Add `getFetcher()` for dispatching `fetch`/`scheduled`/`queue` events This change adds back support for dispatching `scheduled` and `queue` events directly. Miniflare 2 previously provided similar `dispatchScheduled()` and `dispatchQueue()` methods, but these implemented an inconsistent, non-standard API. With `getFetcher()`, we're able to reuse magic proxy code to support arbitrary Workers APIs. This is important for `queue()`, which supports sending any structured serialisable as a message `body`. `getFetcher()` also provides an idiomatic Miniflare API for dealing with multiple workers, matching that provided by `getBindings()`. * rename getFetcher to getWorker --------- Co-authored-by: Rahul Sethi <[email protected]>
1 parent 7ec7fa3 commit ff17f0c

File tree

4 files changed

+150
-5
lines changed

4 files changed

+150
-5
lines changed

packages/miniflare/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,18 @@ defined at the top-level.
598598
bindings, for all bindings in the Worker with the specified `workerName`. If
599599
`workerName` is not specified, defaults to the entrypoint Worker.
600600

601+
- `getWorker(workerName?: string): Promise<Fetcher>`
602+
603+
Returns a `Promise` that resolves with a
604+
[`Fetcher`](https://workers-types.pages.dev/experimental/#Fetcher) pointing to
605+
the specified `workerName`. If `workerName` is not specified, defaults to the
606+
entrypoint Worker. Note this `Fetcher` uses the experimental
607+
[`service_binding_extra_handlers`](https://github.com/cloudflare/workerd/blob/1d9158af7ca1389474982c76ace9e248320bec77/src/workerd/io/compatibility-date.capnp#L290-L297)
608+
compatibility flag to expose
609+
[`scheduled()`](https://workers-types.pages.dev/experimental/#Fetcher.scheduled)
610+
and [`queue()`](https://workers-types.pages.dev/experimental/#Fetcher.queue)
611+
methods for dispatching `scheduled` and `queue` events.
612+
601613
- `getCaches(): Promise<CacheStorage>`
602614

603615
Returns a `Promise` that resolves with the

packages/miniflare/src/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {
1414
CacheStorage,
1515
D1Database,
1616
DurableObjectNamespace,
17+
Fetcher,
1718
KVNamespace,
1819
Queue,
1920
R2Bucket,
@@ -1399,6 +1400,21 @@ export class Miniflare {
13991400

14001401
return bindings as Env;
14011402
}
1403+
async getWorker(workerName?: string): Promise<ReplaceWorkersTypes<Fetcher>> {
1404+
const proxyClient = await this._getProxyClient();
1405+
1406+
// Find worker by name, defaulting to entrypoint worker if none specified
1407+
const workerIndex = this.#findAndAssertWorkerIndex(workerName);
1408+
const workerOpts = this.#workerOpts[workerIndex];
1409+
workerName = workerOpts.core.name ?? "";
1410+
1411+
// Get a `Fetcher` to that worker (NOTE: the `ProxyServer` Durable Object
1412+
// shares its `env` with Miniflare's entry worker, so has access to routes)
1413+
const bindingName = CoreBindings.SERVICE_USER_ROUTE_PREFIX + workerName;
1414+
const fetcher = proxyClient.env[bindingName];
1415+
assert(fetcher !== undefined);
1416+
return fetcher as ReplaceWorkersTypes<Fetcher>;
1417+
}
14021418

14031419
async #getProxy<T>(
14041420
pluginName: string,

packages/miniflare/src/plugins/core/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -519,8 +519,7 @@ export function getGlobalServices({
519519
name: CoreBindings.DURABLE_OBJECT_NAMESPACE_PROXY,
520520
durableObjectNamespace: { className: "ProxyServer" },
521521
},
522-
// Add `proxyBindings` here, they'll be added to the `ProxyServer` `env`.
523-
// TODO(someday): consider making the proxy server a separate worker
522+
// Add `proxyBindings` here, they'll be added to the `ProxyServer` `env`
524523
...proxyBindings,
525524
];
526525
if (sharedOptions.upstream !== undefined) {

packages/miniflare/test/index.spec.ts

Lines changed: 121 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,112 @@ test("Miniflare: getBindings() returns all bindings", async (t) => {
794794
};
795795
t.throws(() => bindings.KV.get("key"), expectations);
796796
});
797+
test("Miniflare: getWorker() allows dispatching events directly", async (t) => {
798+
const mf = new Miniflare({
799+
modules: true,
800+
script: `
801+
let lastScheduledController;
802+
let lastQueueBatch;
803+
export default {
804+
async fetch(request, env, ctx) {
805+
const { pathname } = new URL(request.url);
806+
if (pathname === "/scheduled") {
807+
return Response.json({
808+
scheduledTime: lastScheduledController?.scheduledTime,
809+
cron: lastScheduledController?.cron,
810+
});
811+
} else if (pathname === "/queue") {
812+
return Response.json({
813+
queue: lastQueueBatch.queue,
814+
messages: lastQueueBatch.messages.map((message) => ({
815+
id: message.id,
816+
timestamp: message.timestamp.getTime(),
817+
body: message.body,
818+
bodyType: message.body.constructor.name,
819+
})),
820+
});
821+
} else {
822+
return new Response(null, { status: 404 });
823+
}
824+
},
825+
async scheduled(controller, env, ctx) {
826+
lastScheduledController = controller;
827+
if (controller.cron === "* * * * *") controller.noRetry();
828+
},
829+
async queue(batch, env, ctx) {
830+
lastQueueBatch = batch;
831+
if (batch.queue === "needy") batch.retryAll();
832+
for (const message of batch.messages) {
833+
if (message.id === "perfect") message.ack();
834+
}
835+
}
836+
}`,
837+
});
838+
t.teardown(() => mf.dispose());
839+
const fetcher = await mf.getWorker();
840+
841+
// Check `Fetcher#scheduled()` (implicitly testing `Fetcher#fetch()`)
842+
let scheduledResult = await fetcher.scheduled({
843+
cron: "* * * * *",
844+
});
845+
t.deepEqual(scheduledResult, { outcome: "ok", noRetry: true });
846+
scheduledResult = await fetcher.scheduled({
847+
scheduledTime: new Date(1000),
848+
cron: "30 * * * *",
849+
});
850+
t.deepEqual(scheduledResult, { outcome: "ok", noRetry: false });
851+
852+
let res = await fetcher.fetch("http://localhost/scheduled");
853+
const scheduledController = await res.json();
854+
t.deepEqual(scheduledController, {
855+
scheduledTime: 1000,
856+
cron: "30 * * * *",
857+
});
858+
859+
// Check `Fetcher#queue()`
860+
let queueResult = await fetcher.queue("needy", [
861+
{ id: "a", timestamp: new Date(1000), body: "a" },
862+
{ id: "b", timestamp: new Date(2000), body: { b: 1 } },
863+
]);
864+
t.deepEqual(queueResult, {
865+
outcome: "ok",
866+
retryAll: true,
867+
ackAll: false,
868+
explicitRetries: [],
869+
explicitAcks: [],
870+
});
871+
queueResult = await fetcher.queue("queue", [
872+
{ id: "c", timestamp: new Date(3000), body: new Uint8Array([1, 2, 3]) },
873+
{ id: "perfect", timestamp: new Date(4000), body: new Date(5000) },
874+
]);
875+
t.deepEqual(queueResult, {
876+
outcome: "ok",
877+
retryAll: false,
878+
ackAll: false,
879+
explicitRetries: [],
880+
explicitAcks: ["perfect"],
881+
});
882+
883+
res = await fetcher.fetch("http://localhost/queue");
884+
const queueBatch = await res.json();
885+
t.deepEqual(queueBatch, {
886+
queue: "queue",
887+
messages: [
888+
{
889+
id: "c",
890+
timestamp: 3000,
891+
body: { 0: 1, 1: 2, 2: 3 },
892+
bodyType: "Uint8Array",
893+
},
894+
{
895+
id: "perfect",
896+
timestamp: 4000,
897+
body: "1970-01-01T00:00:05.000Z",
898+
bodyType: "Date",
899+
},
900+
],
901+
});
902+
});
797903
test("Miniflare: getBindings() and friends return bindings for different workers", async (t) => {
798904
const mf = new Miniflare({
799905
workers: [
@@ -802,7 +908,7 @@ test("Miniflare: getBindings() and friends return bindings for different workers
802908
modules: true,
803909
script: `
804910
export class DurableObject {}
805-
export default { fetch() { return new Response(null, { status: 404 }); } }
911+
export default { fetch() { return new Response("a"); } }
806912
`,
807913
d1Databases: ["DB"],
808914
durableObjects: { DO: "DurableObject" },
@@ -811,14 +917,14 @@ test("Miniflare: getBindings() and friends return bindings for different workers
811917
// 2nd worker unnamed, to validate that not specifying a name when
812918
// getting bindings gives the entrypoint, not the unnamed worker
813919
script:
814-
'addEventListener("fetch", (event) => event.respondWith(new Response(null, { status: 404 })));',
920+
'addEventListener("fetch", (event) => event.respondWith(new Response("unnamed")));',
815921
kvNamespaces: ["KV"],
816922
queueProducers: ["QUEUE"],
817923
},
818924
{
819925
name: "b",
820926
script:
821-
'addEventListener("fetch", (event) => event.respondWith(new Response(null, { status: 404 })));',
927+
'addEventListener("fetch", (event) => event.respondWith(new Response("b")));',
822928
r2Buckets: ["BUCKET"],
823929
},
824930
],
@@ -837,6 +943,18 @@ test("Miniflare: getBindings() and friends return bindings for different workers
837943
message: '"c" worker not found',
838944
});
839945

946+
// Check `getWorker()`
947+
let fetcher = await mf.getWorker();
948+
t.is(await (await fetcher.fetch("http://localhost")).text(), "a");
949+
fetcher = await mf.getWorker("");
950+
t.is(await (await fetcher.fetch("http://localhost")).text(), "unnamed");
951+
fetcher = await mf.getWorker("b");
952+
t.is(await (await fetcher.fetch("http://localhost")).text(), "b");
953+
await t.throwsAsync(() => mf.getWorker("c"), {
954+
instanceOf: TypeError,
955+
message: '"c" worker not found',
956+
});
957+
840958
const unboundExpectations = (name: string): ThrowsExpectation<TypeError> => ({
841959
instanceOf: TypeError,
842960
message: `"${name}" unbound in "c" worker`,

0 commit comments

Comments
 (0)