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

Commit 9457776

Browse files
committed
global/WebSocket extend EventTarget, expose Event(Target), see #18
1 parent d9cded4 commit 9457776

25 files changed

+644
-437
lines changed

package.json

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
"vendor"
2020
],
2121
"scripts": {
22-
"build": "tsc",
23-
"dev": "tsc -w",
22+
"build": "tsc -p tsconfig.build.json",
23+
"dev": "tsc -p tsconfig.build.json -w",
2424
"test": "npm run build:fixtures && ava",
2525
"build:fixtures": "node test/fixtures/build.js",
2626
"lint": "eslint '{src,test}/**/*.ts'",
@@ -41,7 +41,7 @@
4141
"cjstoesm": "^1.1.4",
4242
"dotenv": "^8.2.0",
4343
"env-paths": "^2.2.1",
44-
"formdata-node": "^2.4.0",
44+
"formdata-node": "^2.5.0",
4545
"html-rewriter-wasm": "^0.3.2",
4646
"http-cache-semantics": "^4.1.0",
4747
"ioredis": "^4.27.6",
@@ -56,11 +56,27 @@
5656
"typescript": "^4.3.4",
5757
"typeson": "^6.1.0",
5858
"typeson-registry": "^1.0.0-alpha.39",
59-
"web-streams-polyfill": "^3.0.1",
59+
"web-streams-polyfill": "^3.1.0",
6060
"ws": "^7.5.0",
6161
"yargs": "^16.2.0",
6262
"youch": "^2.2.2"
6363
},
64+
"peerDependencies": {
65+
"cjstoesm": "1",
66+
"ioredis": "4",
67+
"typescript": "4"
68+
},
69+
"peerDependenciesMeta": {
70+
"ioredis": {
71+
"optional": true
72+
},
73+
"typescript": {
74+
"optional": true
75+
},
76+
"cjstoesm": {
77+
"optional": true
78+
}
79+
},
6480
"devDependencies": {
6581
"@cloudflare/kv-asset-handler": "^0.1.3",
6682
"@types/http-cache-semantics": "^4.0.0",
@@ -77,19 +93,22 @@
7793
"@typescript-eslint/eslint-plugin": "^4.28.0",
7894
"@typescript-eslint/parser": "^4.28.0",
7995
"ava": "^3.15.0",
96+
"cjstoesm": "^1.1.4",
8097
"esbuild": "^0.12.9",
8198
"eslint": "^7.18.0",
8299
"eslint-config-prettier": "^7.1.0",
83100
"eslint-plugin-import": "^2.22.1",
84101
"eslint-plugin-prettier": "^3.3.1",
102+
"ioredis": "^4.27.6",
85103
"prettier": "^2.2.1",
86104
"rimraf": "^3.0.2",
87105
"ts-node": "^9.1.1",
106+
"typescript": "^4.3.4",
88107
"vitepress": "^0.15.5",
89108
"which": "^2.0.2"
90109
},
91110
"engines": {
92-
"node": ">=10.12.0"
111+
"node": ">=16.5.0"
93112
},
94113
"repository": {
95114
"type": "git",
@@ -98,5 +117,8 @@
98117
"bugs": {
99118
"url": "https://github.com/mrbbot/miniflare/issues"
100119
},
101-
"homepage": "https://miniflare.dev/"
120+
"homepage": "https://miniflare.dev/",
121+
"volta": {
122+
"node": "16.6.0"
123+
}
102124
}

src/error.ts

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

src/helpers.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
export class MiniflareError extends Error {
2+
constructor(message?: string) {
3+
super(message);
4+
// Restore prototype chain:
5+
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
6+
Object.setPrototypeOf(this, new.target.prototype);
7+
this.name = new.target.name;
8+
}
9+
}
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/index.ts

Lines changed: 42 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,22 @@ import {
1111
} from "@mrbbot/node-fetch";
1212
import cron from "node-cron";
1313
import sourceMap, { UrlAndMap } from "source-map-support";
14-
import WebSocket from "ws";
14+
import StandardWebSocket from "ws";
1515
import Youch from "youch";
16-
import { MiniflareError } from "./error";
16+
import { MiniflareError } from "./helpers";
1717
import { Cache, KVStorageNamespace } from "./kv";
1818
import { ConsoleLog, Log, NoOpLog, logResponse } from "./log";
1919
import { ResponseWaitUntil } from "./modules";
2020
import { DurableObjectConstructor, DurableObjectNamespace } from "./modules/do";
21-
import { ModuleFetchListener, ModuleScheduledListener } from "./modules/events";
21+
import {
22+
ModuleFetchListener,
23+
ModuleScheduledListener,
24+
addModuleFetchListenerSymbol,
25+
addModuleScheduledListenerSymbol,
26+
dispatchFetchSymbol,
27+
dispatchScheduledSymbol,
28+
} from "./modules/events";
29+
import { ServiceWorkerGlobalScope } from "./modules/events";
2230
import { Context } from "./modules/module";
2331
import * as modules from "./modules/modules";
2432
import { terminateWebSocket } from "./modules/ws";
@@ -28,6 +36,7 @@ import {
2836
ModuleScriptInstance,
2937
ScriptLinker,
3038
ScriptScriptInstance,
39+
createScriptContext,
3140
} from "./scripts";
3241

3342
type ModuleName = keyof typeof modules;
@@ -50,12 +59,11 @@ export class Miniflare {
5059
readonly #watcher: OptionsWatcher;
5160
#options?: ProcessedOptions;
5261

53-
#sandbox: Context;
54-
#environment: Context;
62+
#globalScope?: ServiceWorkerGlobalScope;
5563
#scheduledTasks?: cron.ScheduledTask[];
5664
#extraSourceMaps?: Map<string, string>;
5765

58-
readonly #wss: WebSocket.Server;
66+
readonly #wss: StandardWebSocket.Server;
5967

6068
constructor(options: Options = {}) {
6169
if (options.sourceMap) {
@@ -77,12 +85,8 @@ export class Miniflare {
7785
{} as Modules
7886
);
7987

80-
// Defaults never used, will be overridden in #watchCallback
81-
this.#sandbox = {};
82-
this.#environment = {};
83-
8488
// Initialise web socket server
85-
this.#wss = new WebSocket.Server({ noServer: true });
89+
this.#wss = new StandardWebSocket.Server({ noServer: true });
8690
this.#wss.addListener(
8791
"connection",
8892
this.#webSocketConnectionListener.bind(this)
@@ -104,20 +108,20 @@ export class Miniflare {
104108
this.#options = options;
105109
// Build sandbox and environment
106110
const modules = Object.values(this.#modules);
107-
this.#sandbox = modules.reduce(
111+
const sandbox = modules.reduce(
108112
(sandbox, module) => Object.assign(sandbox, module.buildSandbox(options)),
109113
{} as Context
110114
);
111-
this.#environment = modules.reduce(
115+
const environment = modules.reduce(
112116
(environment, module) =>
113117
Object.assign(environment, module.buildEnvironment(options)),
114118
{} as Context
115119
);
116120
// Assign bindings last so they can override modules if required
117-
Object.assign(this.#environment, options.bindings);
121+
Object.assign(environment, options.bindings);
118122

119123
this.#reloadScheduled();
120-
await this.#reloadWorker();
124+
await this.#reloadWorker(sandbox, environment);
121125
}
122126

123127
#reloadScheduled(): void {
@@ -137,7 +141,7 @@ export class Miniflare {
137141
);
138142
}
139143

140-
async #reloadWorker(): Promise<void> {
144+
async #reloadWorker(sandbox: Context, environment: Context): Promise<void> {
141145
// Only called in #watchCallback() after #options set and scripts and
142146
// processedModulesRules are always set in this
143147
assert(this.#options?.scripts && this.#options.processedModulesRules);
@@ -146,20 +150,20 @@ export class Miniflare {
146150
const linker = new ScriptLinker(this.#options.processedModulesRules);
147151
this.#extraSourceMaps = linker.extraSourceMaps;
148152

153+
// Build new global scope (sandbox), this inherits from EventTarget
154+
const globalScope = new ServiceWorkerGlobalScope(
155+
this.log,
156+
sandbox,
157+
environment,
158+
this.#options.modules
159+
);
160+
this.#globalScope = globalScope;
161+
const context = createScriptContext(globalScope);
162+
149163
// Reset state
150-
this.#modules.EventsModule.resetEventListeners();
151164
this.#modules.DurableObjectsModule.resetInstances();
152165
this.#modules.StandardsModule.resetWebSockets();
153166

154-
// Build sandbox with global self-references, only including environment
155-
// in global scope if not using modules
156-
const sandbox = this.#options.modules
157-
? { ...this.#sandbox }
158-
: { ...this.#sandbox, ...this.#environment };
159-
sandbox.global = sandbox;
160-
sandbox.globalThis = sandbox;
161-
sandbox.self = sandbox;
162-
163167
// Parse and run all scripts
164168
const moduleExports: Record<string, ModuleExports> = {};
165169
for (const script of Object.values(this.#options.scripts)) {
@@ -169,8 +173,8 @@ export class Miniflare {
169173
let instance: ScriptScriptInstance | ModuleScriptInstance<ModuleExports>;
170174
try {
171175
instance = this.#options.modules
172-
? await script.buildModule(sandbox, linker.linker)
173-
: await script.buildScript(sandbox);
176+
? await script.buildModule(context, linker.linker)
177+
: await script.buildScript(context);
174178
} catch (e) {
175179
// If this is because --experimental-vm-modules disabled, rethrow
176180
if (e instanceof MiniflareError) throw e;
@@ -202,18 +206,12 @@ export class Miniflare {
202206
if (script.fileName === this.#options.scriptPath) {
203207
const fetchListener = instance.exports?.default?.fetch;
204208
if (fetchListener) {
205-
this.#modules.EventsModule.addModuleFetchListener(
206-
fetchListener,
207-
this.#environment
208-
);
209+
globalScope[addModuleFetchListenerSymbol](fetchListener);
209210
}
210211

211212
const scheduledListener = instance.exports?.default?.scheduled;
212213
if (scheduledListener) {
213-
this.#modules.EventsModule.addModuleScheduledListener(
214-
scheduledListener,
215-
this.#environment
216-
);
214+
globalScope[addModuleScheduledListenerSymbol](scheduledListener);
217215
}
218216
}
219217
}
@@ -231,10 +229,7 @@ export class Miniflare {
231229
);
232230
}
233231
}
234-
this.#modules.DurableObjectsModule.setContext(
235-
constructors,
236-
this.#environment
237-
);
232+
this.#modules.DurableObjectsModule.setContext(constructors, environment);
238233

239234
// Watch module referenced paths
240235
assert(this.#watcher !== undefined);
@@ -263,7 +258,9 @@ export class Miniflare {
263258
init?: RequestInit
264259
): Promise<ResponseWaitUntil<WaitUntil>> {
265260
await this.#watcher.initPromise;
266-
return this.#modules.EventsModule.dispatchFetch<WaitUntil>(
261+
const globalScope = this.#globalScope;
262+
assert(globalScope);
263+
return globalScope[dispatchFetchSymbol]<WaitUntil>(
267264
new Request(input, init),
268265
this.#options?.upstreamUrl
269266
);
@@ -274,10 +271,9 @@ export class Miniflare {
274271
cron?: string
275272
): Promise<WaitUntil> {
276273
await this.#watcher.initPromise;
277-
return this.#modules.EventsModule.dispatchScheduled<WaitUntil>(
278-
scheduledTime,
279-
cron
280-
);
274+
const globalScope = this.#globalScope;
275+
assert(globalScope);
276+
return globalScope[dispatchScheduledSymbol]<WaitUntil>(scheduledTime, cron);
281277
}
282278

283279
async getOptions(): Promise<ProcessedOptions> {
@@ -448,7 +444,7 @@ export class Miniflare {
448444
}
449445

450446
async #webSocketConnectionListener(
451-
ws: WebSocket,
447+
ws: StandardWebSocket,
452448
req: http.IncomingMessage
453449
): Promise<void> {
454450
// Handle request in worker

src/log.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import http from "http";
22
import * as colors from "kleur/colors";
3-
import { MiniflareError } from "./error";
3+
import { MiniflareError } from "./helpers";
44

55
export interface Log {
66
log(data: string): void;

src/modules/cache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import path from "path";
2-
import { MiniflareError } from "../error";
2+
import { MiniflareError } from "../helpers";
33
import { Cache, NoOpCache } from "../kv";
44
import { KVStorageFactory } from "../kv/helpers";
55
import { Log } from "../log";

src/modules/do.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
11
import assert from "assert";
22
import crypto from "crypto";
33
import path from "path";
4-
import {
5-
Request,
6-
RequestInfo,
7-
RequestInit,
8-
Response,
9-
} from "@mrbbot/node-fetch";
10-
import { MiniflareError } from "../error";
4+
import { RequestInfo, RequestInit } from "@mrbbot/node-fetch";
5+
import { MiniflareError } from "../helpers";
116
import { DurableObjectStorage } from "../kv";
127
import { abortAllSymbol } from "../kv/do";
138
import { KVStorageFactory } from "../kv/helpers";
149
import { Log } from "../log";
1510
import { ProcessedOptions } from "../options";
1611
import { Context, Module } from "./module";
12+
import { Request, Response } from "./standards";
1713

1814
// Ideally we would store the storage on the DurableObject instance itself,
1915
// but we don't know what the user's Durable Object code does, so we store it

0 commit comments

Comments
 (0)