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

Commit 64e1b26

Browse files
committed
Add runWithMiniflareDurableObjectGates for test environments
...allowing you to use I/O gates in the Jest/Vitest environments
1 parent 9a62a02 commit 64e1b26

File tree

5 files changed

+95
-40
lines changed

5 files changed

+95
-40
lines changed

packages/durable-objects/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export {
55
DurableObjectState,
66
DurableObjectStub,
77
DurableObjectNamespace,
8+
_kRunWithGates,
89
} from "./namespace";
910
export type {
1011
DurableObject,

packages/durable-objects/src/namespace.ts

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ export interface DurableObject {
6262
}
6363

6464
export const kInstance = Symbol("kInstance");
65+
/** @internal */
66+
export const _kRunWithGates = Symbol("kRunWithGates");
6567
export const kAlarm = Symbol("kAlarm");
6668
const kFetch = Symbol("kFetch");
6769

@@ -81,41 +83,39 @@ export class DurableObjectState {
8183
return this.#inputGate.runWithClosed(closure);
8284
}
8385

84-
[kFetch](request: Request): Promise<Response> {
86+
[_kRunWithGates]<T>(closure: () => Awaitable<T>): Promise<T> {
8587
// TODO: catch, reset object on error
8688
const outputGate = new OutputGate();
87-
return outputGate.runWith(() =>
88-
this.#inputGate.runWith(() => {
89-
const instance = this[kInstance];
90-
if (!instance?.fetch) {
91-
throw new DurableObjectError(
92-
"ERR_NO_HANDLER",
93-
"No fetch handler defined in Durable Object"
94-
);
95-
}
96-
return instance.fetch(request);
97-
})
98-
);
89+
return outputGate.runWith(() => this.#inputGate.runWith(closure));
90+
}
91+
92+
[kFetch](request: Request): Promise<Response> {
93+
return this[_kRunWithGates](() => {
94+
const instance = this[kInstance];
95+
if (!instance?.fetch) {
96+
throw new DurableObjectError(
97+
"ERR_NO_HANDLER",
98+
"No fetch handler defined in Durable Object"
99+
);
100+
}
101+
return instance.fetch(request);
102+
});
99103
}
100104

101105
[kAlarm](): Promise<void> {
102-
// TODO: catch, reset object on error
103-
const outputGate = new OutputGate();
104-
return outputGate.runWith(() =>
105-
this.#inputGate.runWith(async () => {
106-
// Delete the local alarm
107-
await this.storage.deleteAlarm();
108-
// Grab the instance and call the alarm handler
109-
const instance = this[kInstance];
110-
if (!instance?.alarm) {
111-
throw new DurableObjectError(
112-
"ERR_NO_HANDLER",
113-
"No alarm handler defined in Durable Object"
114-
);
115-
}
116-
return instance.alarm();
117-
})
118-
);
106+
return this[_kRunWithGates](async () => {
107+
// Delete the local alarm
108+
await this.storage.deleteAlarm();
109+
// Grab the instance and call the alarm handler
110+
const instance = this[kInstance];
111+
if (!instance?.alarm) {
112+
throw new DurableObjectError(
113+
"ERR_NO_HANDLER",
114+
"No alarm handler defined in Durable Object"
115+
);
116+
}
117+
return instance.alarm();
118+
});
119119
}
120120
}
121121

packages/jest-environment-miniflare/test/fixtures/durableobjects.module.spec.js

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { TestObject } from "./module-worker.js";
2-
31
beforeAll(async () => {
42
const { TEST_OBJECT } = getMiniflareBindings();
53
const id = TEST_OBJECT.idFromName("test");
@@ -16,11 +14,32 @@ test("Durable Objects", async () => {
1614
});
1715

1816
test("Durable Objects direct", async () => {
17+
class Counter {
18+
constructor(state) {
19+
this.storage = state.storage;
20+
}
21+
async fetch() {
22+
const count = (await this.storage.get("count")) ?? 0;
23+
void this.storage.put("count", count + 1);
24+
return new Response(String(count));
25+
}
26+
}
27+
1928
// https://github.com/cloudflare/miniflare/issues/157
2029
const env = getMiniflareBindings();
30+
// Doesn't matter too much that we're using a different object binding here
2131
const id = env.TEST_OBJECT.idFromName("test");
2232
const state = await getMiniflareDurableObjectState(id);
23-
const object = new TestObject(state, env);
24-
const res = await object.fetch(new Request("https://object/"));
25-
expect(await res.text()).toBe("durable:https://object/:value");
33+
const object = new Counter(state, env);
34+
const [res1, res2] = await Promise.all([
35+
runWithMiniflareDurableObjectGates(state, () =>
36+
object.fetch(new Request("https://object/"))
37+
),
38+
runWithMiniflareDurableObjectGates(state, () =>
39+
object.fetch(new Request("https://object/"))
40+
),
41+
]);
42+
expect(await state.storage.get("count")).toBe(2);
43+
expect(await res1.text()).toBe("0");
44+
expect(await res2.text()).toBe("1");
2645
});

packages/shared-test-environment/src/globals.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import {
88
DurableObjectId,
99
DurableObjectState,
1010
DurableObjectStorage,
11+
_kRunWithGates,
1112
} from "@miniflare/durable-objects";
12-
import { Context } from "@miniflare/shared";
13+
import { Awaitable, Context } from "@miniflare/shared";
1314
import { MockAgent } from "undici";
1415
import { PLUGINS } from "./plugins";
1516

@@ -31,6 +32,10 @@ declare global {
3132
function getMiniflareDurableObjectState(
3233
id: DurableObjectId
3334
): Promise<DurableObjectState>;
35+
function runWithMiniflareDurableObjectGates<T>(
36+
state: DurableObjectState,
37+
closure: () => Awaitable<T>
38+
): Promise<T>;
3439
function getMiniflareFetchMock(): MockAgent;
3540
function getMiniflareWaitUntil<WaitUntil extends any[] = unknown[]>(
3641
event: FetchEvent | ScheduledEvent | ExecutionContext
@@ -48,6 +53,10 @@ export interface MiniflareEnvironmentUtilities {
4853
getMiniflareDurableObjectState(
4954
id: DurableObjectId
5055
): Promise<DurableObjectState>;
56+
runWithMiniflareDurableObjectGates<T>(
57+
state: DurableObjectState,
58+
closure: () => Awaitable<T>
59+
): Promise<T>;
5160
getMiniflareFetchMock(): MockAgent;
5261
getMiniflareWaitUntil<WaitUntil extends any[] = unknown[]>(
5362
event: FetchEvent | ScheduledEvent | ExecutionContext
@@ -78,6 +87,12 @@ export async function createMiniflareEnvironmentUtilities(
7887
const storage = plugin.getStorage(factory, id);
7988
return new DurableObjectState(id, storage);
8089
},
90+
runWithMiniflareDurableObjectGates<T>(
91+
state: DurableObjectState,
92+
closure: () => Awaitable<T>
93+
) {
94+
return state[_kRunWithGates](closure);
95+
},
8196
getMiniflareFetchMock() {
8297
return fetchMock;
8398
},

packages/vitest-environment-miniflare/test/fixtures/modules/durableobjects.module.spec.js

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { beforeAll, expect, test } from "vitest";
2-
import { TestObject } from "./module-worker.js";
32
setupMiniflareIsolatedStorage();
43

54
beforeAll(async () => {
@@ -18,11 +17,32 @@ test("Durable Objects", async () => {
1817
});
1918

2019
test("Durable Objects direct", async () => {
20+
class Counter {
21+
constructor(state) {
22+
this.storage = state.storage;
23+
}
24+
async fetch() {
25+
const count = (await this.storage.get("count")) ?? 0;
26+
void this.storage.put("count", count + 1);
27+
return new Response(String(count));
28+
}
29+
}
30+
2131
// https://github.com/cloudflare/miniflare/issues/157
2232
const env = getMiniflareBindings();
33+
// Doesn't matter too much that we're using a different object binding here
2334
const id = env.TEST_OBJECT.idFromName("test");
2435
const state = await getMiniflareDurableObjectState(id);
25-
const object = new TestObject(state, env);
26-
const res = await object.fetch(new Request("https://object/"));
27-
expect(await res.text()).toBe("durable:https://object/:value");
36+
const object = new Counter(state, env);
37+
const [res1, res2] = await Promise.all([
38+
runWithMiniflareDurableObjectGates(state, () =>
39+
object.fetch(new Request("https://object/"))
40+
),
41+
runWithMiniflareDurableObjectGates(state, () =>
42+
object.fetch(new Request("https://object/"))
43+
),
44+
]);
45+
expect(await state.storage.get("count")).toBe(2);
46+
expect(await res1.text()).toBe("0");
47+
expect(await res2.text()).toBe("1");
2848
});

0 commit comments

Comments
 (0)