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

Commit 0321c2f

Browse files
committed
Shim Event & EventTarget for compatibility with old Node versions
1 parent 9457776 commit 0321c2f

File tree

9 files changed

+100
-6992
lines changed

9 files changed

+100
-6992
lines changed

package-lock.json

Lines changed: 47 additions & 6891 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "miniflare",
3-
"version": "1.3.3",
3+
"version": "1.4.0",
44
"description": "Fun, full-featured, fully-local simulator for Cloudflare Workers",
55
"keywords": [
66
"cloudflare",
@@ -35,12 +35,13 @@
3535
},
3636
"dependencies": {
3737
"@iarna/toml": "^2.2.5",
38-
"@mrbbot/node-fetch": "^4.4.0",
38+
"@mrbbot/node-fetch": "^4.5.0",
3939
"@peculiar/webcrypto": "^1.1.4",
4040
"chokidar": "^3.5.1",
4141
"cjstoesm": "^1.1.4",
4242
"dotenv": "^8.2.0",
4343
"env-paths": "^2.2.1",
44+
"event-target-shim": "^6.0.2",
4445
"formdata-node": "^2.5.0",
4546
"html-rewriter-wasm": "^0.3.2",
4647
"http-cache-semantics": "^4.1.0",
@@ -93,22 +94,19 @@
9394
"@typescript-eslint/eslint-plugin": "^4.28.0",
9495
"@typescript-eslint/parser": "^4.28.0",
9596
"ava": "^3.15.0",
96-
"cjstoesm": "^1.1.4",
9797
"esbuild": "^0.12.9",
9898
"eslint": "^7.18.0",
9999
"eslint-config-prettier": "^7.1.0",
100100
"eslint-plugin-import": "^2.22.1",
101101
"eslint-plugin-prettier": "^3.3.1",
102-
"ioredis": "^4.27.6",
103102
"prettier": "^2.2.1",
104103
"rimraf": "^3.0.2",
105104
"ts-node": "^9.1.1",
106-
"typescript": "^4.3.4",
107105
"vitepress": "^0.15.5",
108106
"which": "^2.0.2"
109107
},
110108
"engines": {
111-
"node": ">=16.5.0"
109+
"node": ">=10.12.0"
112110
},
113111
"repository": {
114112
"type": "git",
@@ -119,6 +117,6 @@
119117
},
120118
"homepage": "https://miniflare.dev/",
121119
"volta": {
122-
"node": "16.6.0"
120+
"node": "14.17.1"
123121
}
124122
}

src/helpers.ts

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,3 @@ export class MiniflareError extends Error {
77
this.name = new.target.name;
88
}
99
}
10-
11-
export type TypedEventListener<Event> =
12-
| ((e: Event) => void)
13-
| { handleEvent(e: Event): void };
14-
15-
export interface TypedEventTargetInterface<
16-
EventMap extends Record<string, Event>
17-
> extends EventTarget {
18-
addEventListener<EventType extends keyof EventMap>(
19-
type: EventType,
20-
listener: TypedEventListener<EventMap[EventType]> | null,
21-
options?: AddEventListenerOptions | boolean
22-
): void;
23-
24-
removeEventListener<EventType extends keyof EventMap>(
25-
type: EventType,
26-
listener: TypedEventListener<EventMap[EventType]> | null,
27-
options?: EventListenerOptions | boolean
28-
): void;
29-
}
30-
31-
export function typedEventTarget<EventMap extends Record<string, Event>>(): {
32-
prototype: TypedEventTargetInterface<EventMap>;
33-
new (): TypedEventTargetInterface<EventMap>;
34-
} {
35-
return EventTarget as any;
36-
}

src/modules/events.ts

Lines changed: 22 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { URL } from "url";
22
import fetch from "@mrbbot/node-fetch";
3-
import { TypedEventListener, typedEventTarget } from "../helpers";
3+
import { Event, EventTarget } from "event-target-shim";
44
import { Log } from "../log";
55
import { Context, Module } from "./module";
66
import { FetchError, Request, Response } from "./standards";
@@ -11,7 +11,7 @@ export const responseSymbol = Symbol("response");
1111
export const passThroughSymbol = Symbol("passThrough");
1212
export const waitUntilSymbol = Symbol("waitUntil");
1313

14-
export class FetchEvent extends Event {
14+
export class FetchEvent extends Event<"fetch"> {
1515
[responseSymbol]?: Promise<Response>;
1616
[passThroughSymbol] = false;
1717
readonly [waitUntilSymbol]: Promise<any>[] = [];
@@ -34,7 +34,7 @@ export class FetchEvent extends Event {
3434
}
3535
}
3636

37-
export class ScheduledEvent extends Event {
37+
export class ScheduledEvent extends Event<"scheduled"> {
3838
readonly [waitUntilSymbol]: Promise<any>[] = [];
3939

4040
constructor(
@@ -79,11 +79,11 @@ type EventMap = {
7979
fetch: FetchEvent;
8080
scheduled: ScheduledEvent;
8181
};
82-
export class ServiceWorkerGlobalScope extends typedEventTarget<EventMap>() {
82+
export class ServiceWorkerGlobalScope extends EventTarget<EventMap> {
8383
readonly #log: Log;
8484
readonly #environment: Context;
8585
readonly #wrappedListeners = new WeakMap<
86-
EventListenerOrEventListenerObject,
86+
EventTarget.EventListener<this, any>,
8787
EventListener
8888
>();
8989
#wrappedError?: Error;
@@ -116,44 +116,23 @@ export class ServiceWorkerGlobalScope extends typedEventTarget<EventMap>() {
116116
this.dispatchEvent = this.dispatchEvent.bind(this);
117117
}
118118

119-
#wrap(listener: TypedEventListener<Event> | null): EventListener | null {
119+
#wrap<T extends keyof EventMap>(
120+
listener?: EventTarget.EventListener<this, EventMap[T]> | null
121+
): EventTarget.CallbackFunction<this, EventMap[T]> | null | undefined {
120122
// When an event listener throws, we want dispatching to stop and the
121123
// error to be thrown so we can catch it and display a nice error page.
122-
// Unfortunately, Node's event target emits uncaught exceptions when a
123-
// listener throws, which are tricky to catch without breaking tests (AVA
124-
// also registers an uncaught exception handler).
125-
//
126-
// Node 16.6.0's internal implementation contains this line to throw
127-
// uncaught exceptions: `process.nextTick(() => { throw err; });`.
128-
// A potential solution would be to monkey-patch `process.nextTick` and call
129-
// the callback immediately:
130-
//
131-
// ```js
132-
// const originalNextTick = process.nextTick;
133-
// process.nextTick = (callback) => callback();
134-
// try {
135-
// this.dispatchEvent(event);
136-
// } finally {
137-
// process.nextTick = originalNextTick;
138-
// }
139-
// ```
140-
//
141-
// However, this relies on internal behaviour that may change at any point
142-
// and may prevent the EventTarget from doing required clean up. Hence,
143-
// we wrap event listeners instead, storing the wrapped versions in a
144-
// WeakMap so they can be removed when the original listener is passed to
145-
// removeEventListener.
146-
124+
if (listener === undefined) return undefined;
147125
if (listener === null) return null;
148126
const wrappedListeners = this.#wrappedListeners;
149127
let wrappedListener = wrappedListeners.get(listener);
150128
if (wrappedListener) return wrappedListener;
151129
wrappedListener = (event) => {
152130
try {
153131
if ("handleEvent" in listener) {
154-
listener.handleEvent(event);
132+
listener.handleEvent(event as EventMap[T]);
155133
} else {
156-
listener(event);
134+
// @ts-expect-error "this" type is definitely correct
135+
listener(event as EventMap[T]);
157136
}
158137
} catch (error) {
159138
event.stopImmediatePropagation();
@@ -164,20 +143,20 @@ export class ServiceWorkerGlobalScope extends typedEventTarget<EventMap>() {
164143
return wrappedListener;
165144
}
166145

167-
addEventListener<EventType extends keyof EventMap>(
168-
type: EventType,
169-
listener: TypedEventListener<EventMap[EventType]> | null,
170-
options?: AddEventListenerOptions | boolean
146+
addEventListener<T extends keyof EventMap>(
147+
type: T,
148+
listener?: EventTarget.EventListener<this, EventMap[T]> | null,
149+
options?: EventTarget.AddOptions | boolean
171150
): void {
172-
super.addEventListener(type, this.#wrap(listener as any), options);
151+
super.addEventListener(type, this.#wrap(listener), options as any);
173152
}
174153

175-
removeEventListener<EventType extends keyof EventMap>(
176-
type: EventType,
177-
listener: TypedEventListener<EventMap[EventType]> | null,
178-
options?: EventListenerOptions | boolean
154+
removeEventListener<T extends string & keyof EventMap>(
155+
type: T,
156+
listener?: EventTarget.EventListener<this, EventMap[T]> | null,
157+
options?: EventTarget.Options | boolean
179158
): void {
180-
super.removeEventListener(type, this.#wrap(listener as any), options);
159+
super.removeEventListener(type, this.#wrap(listener), options as any);
181160
}
182161

183162
dispatchEvent(event: Event): boolean {

src/modules/ws.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
import assert from "assert";
2+
import { Event, EventTarget } from "event-target-shim";
23
import StandardWebSocket from "ws";
3-
import { MiniflareError, typedEventTarget } from "../helpers";
4+
import { MiniflareError } from "../helpers";
45
import { Context, Module } from "./module";
56

6-
export class MessageEvent extends Event {
7+
export class MessageEvent extends Event<"message"> {
78
constructor(public readonly data: string) {
89
super("message");
910
}
1011
}
1112

12-
export class CloseEvent extends Event {
13+
export class CloseEvent extends Event<"close"> {
1314
constructor(public readonly code?: number, public readonly reason?: string) {
1415
super("close");
1516
}
1617
}
1718

18-
export class ErrorEvent extends Event {
19+
export class ErrorEvent extends Event<"error"> {
1920
constructor(public readonly error?: Error) {
2021
super("error");
2122
}
@@ -31,7 +32,7 @@ type EventMap = {
3132
close: CloseEvent;
3233
error: ErrorEvent;
3334
};
34-
export class WebSocket extends typedEventTarget<EventMap>() {
35+
export class WebSocket extends EventTarget<EventMap> {
3536
public static CONNECTING = 0;
3637
public static OPEN = 1;
3738
public static CLOSING = 2;
@@ -102,7 +103,6 @@ export class WebSocket extends typedEventTarget<EventMap>() {
102103
this.#readyState = WebSocket.CLOSING;
103104
pair.#readyState = WebSocket.CLOSING;
104105

105-
// TODO: PR Node.js lib/internal/event_target.js
106106
this.dispatchEvent(new CloseEvent(code, reason));
107107
pair.dispatchEvent(new CloseEvent(code, reason));
108108

src/scripts.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
ScriptTarget,
1010
transpileModule,
1111
} from "typescript";
12-
import { MiniflareError } from "./error";
12+
import { MiniflareError } from "./helpers";
1313
import { ProcessedModuleRule, stringScriptPath } from "./options";
1414

1515
export function createScriptContext(sandbox: vm.Context): vm.Context {

src/suppress.ts

Lines changed: 0 additions & 16 deletions
This file was deleted.

test/modules/events.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,24 @@ test("buildSandbox: adds module scheduled event listener", async (t) => {
183183
t.is(res[1], "30 * * * *");
184184
t.is(res[2], "value");
185185
});
186+
test("buildSandbox: removes event listeners", async (t) => {
187+
const script = `(${(() => {
188+
const sandbox = self as any;
189+
const waitUntilOne = (e: FetchEvent) => {
190+
e.waitUntil(Promise.resolve(1));
191+
};
192+
sandbox.addEventListener("fetch", waitUntilOne);
193+
sandbox.addEventListener("fetch", (e: FetchEvent) => {
194+
sandbox.removeEventListener("fetch", waitUntilOne);
195+
e.respondWith(new sandbox.Response(e.request.url));
196+
});
197+
}).toString()})()`;
198+
const mf = new Miniflare({ script });
199+
let res = await mf.dispatchFetch(new Request("http://localhost:8787/"));
200+
t.deepEqual(await res.waitUntil(), [1]);
201+
res = await mf.dispatchFetch(new Request("http://localhost:8787/"));
202+
t.deepEqual(await res.waitUntil(), []);
203+
});
186204

187205
test("dispatchFetch: dispatches event", async (t) => {
188206
const { globalScope } = t.context;

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"compilerOptions": {
33
"module": "commonjs",
44
"target": "esnext",
5-
"lib": ["esnext"],
5+
"lib": ["esnext", "webworker"],
66
"strict": true,
77
"moduleResolution": "node",
88
"esModuleInterop": true,

0 commit comments

Comments
 (0)