Skip to content

Commit

Permalink
global/WebSocket extend EventTarget, expose Event(Target), see #18
Browse files Browse the repository at this point in the history
  • Loading branch information
mrbbot committed Oct 20, 2021
1 parent 5f3748e commit 4c3cb14
Show file tree
Hide file tree
Showing 13 changed files with 109 additions and 225 deletions.
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
"vendor"
],
"scripts": {
"build": "tsc",
"dev": "tsc -w",
"build": "tsc -p tsconfig.build.json",
"dev": "tsc -p tsconfig.build.json -w",
"test": "npm run build:fixtures && ava",
"build:fixtures": "node test/fixtures/build.js",
"lint": "eslint '{src,test}/**/*.ts'",
Expand All @@ -35,7 +35,7 @@
},
"dependencies": {
"@iarna/toml": "^2.2.5",
"@mrbbot/node-fetch": "^5.0.2",
"@mrbbot/node-fetch": "^5.0.3",
"chokidar": "^3.5.1",
"dotenv": "^8.2.0",
"env-paths": "^2.2.1",
Expand Down
18 changes: 1 addition & 17 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "./suppress";
import { promises as fs } from "fs";
import path from "path";
import fetch from "@mrbbot/node-fetch";
Expand Down Expand Up @@ -304,23 +305,6 @@ export async function updateCheck({
}

if (module === require.main) {
// Suppress experimental warnings
const originalEmitWarning = process.emitWarning;
// @ts-expect-error this works, but overloads are funky in typescript
process.emitWarning = (warning, ctorTypeOptions, ctorCode, ctor) => {
if (ctorTypeOptions === "ExperimentalWarning") {
const warningString = warning.toString();
if (
warningString.startsWith("VM Modules") ||
warningString.startsWith("stream/web") ||
warningString.startsWith("buffer.Blob")
) {
return;
}
}
originalEmitWarning(warning, ctorTypeOptions, ctorCode, ctor);
};

const options = parseArgv(process.argv.slice(2));
const mf = new Miniflare(options);

Expand Down
15 changes: 0 additions & 15 deletions src/helpers.ts

This file was deleted.

16 changes: 2 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import cron from "node-cron";
import sourceMap, { UrlAndMap } from "source-map-support";
import StandardWebSocket from "ws";
import Youch from "youch";
import { MiniflareError, formatSize } from "./helpers";
import { MiniflareError } from "./helpers";
import { Cache, KVStorageNamespace } from "./kv";
import { ConsoleLog, Log, NoOpLog, logResponse } from "./log";
import { ResponseWaitUntil } from "./modules";
Expand Down Expand Up @@ -146,9 +146,6 @@ export class Miniflare {
// processedModulesRules are always set in this
assert(this.#options?.scripts && this.#options.processedModulesRules);

// Keep track of the size in bytes of all scripts
let size = 0;

// Build modules linker maintaining set of referenced paths for watching
const linker = new ScriptLinker(this.#options.processedModulesRules);
this.#extraSourceMaps = linker.extraSourceMaps;
Expand All @@ -173,7 +170,6 @@ export class Miniflare {
this.log.debug(`Reloading ${path.relative("", script.fileName)}...`);

// Parse script and build instance
size += Buffer.byteLength(script.code, "utf8");
let instance: ScriptScriptInstance | ModuleScriptInstance<ModuleExports>;
try {
instance = this.#options.modules
Expand Down Expand Up @@ -244,15 +240,7 @@ export class Miniflare {
ws.close(1012, "Service Restart");
}

// Log total size of worker with warning if required
size += linker.referencedPathsTotalSize;
this.log.info(`Worker reloaded! (${formatSize(size)})`);
if (size > 1_048_576)
this.log.warn(
"Worker's uncompressed size exceeds 1MiB!" +
"Note that your worker will be compressed during upload " +
"so you may still be able to deploy it."
);
this.log.info("Worker reloaded!");
}

/** @deprecated Since 1.2.0, this is just an alias for reloadOptions() */
Expand Down
65 changes: 43 additions & 22 deletions src/modules/events.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { URL } from "url";
import fetch from "@mrbbot/node-fetch";
import { Event, EventTarget } from "event-target-shim";
import { TypedEventListener, typedEventTarget } from "../helpers";
import { Log } from "../log";
import { Context, Module } from "./module";
import { FetchError, Request, Response } from "./standards";
Expand All @@ -11,7 +11,7 @@ export const responseSymbol = Symbol("response");
export const passThroughSymbol = Symbol("passThrough");
export const waitUntilSymbol = Symbol("waitUntil");

export class FetchEvent extends Event<"fetch"> {
export class FetchEvent extends Event {
[responseSymbol]?: Promise<Response>;
[passThroughSymbol] = false;
readonly [waitUntilSymbol]: Promise<any>[] = [];
Expand All @@ -34,7 +34,7 @@ export class FetchEvent extends Event<"fetch"> {
}
}

export class ScheduledEvent extends Event<"scheduled"> {
export class ScheduledEvent extends Event {
readonly [waitUntilSymbol]: Promise<any>[] = [];

constructor(
Expand Down Expand Up @@ -79,11 +79,11 @@ type EventMap = {
fetch: FetchEvent;
scheduled: ScheduledEvent;
};
export class ServiceWorkerGlobalScope extends EventTarget<EventMap> {
export class ServiceWorkerGlobalScope extends typedEventTarget<EventMap>() {
readonly #log: Log;
readonly #environment: Context;
readonly #wrappedListeners = new WeakMap<
EventTarget.EventListener<this, any>,
EventListenerOrEventListenerObject,
EventListener
>();
#wrappedError?: Error;
Expand Down Expand Up @@ -116,23 +116,44 @@ export class ServiceWorkerGlobalScope extends EventTarget<EventMap> {
this.dispatchEvent = this.dispatchEvent.bind(this);
}

#wrap<T extends keyof EventMap>(
listener?: EventTarget.EventListener<this, EventMap[T]> | null
): EventTarget.CallbackFunction<this, EventMap[T]> | null | undefined {
#wrap(listener: TypedEventListener<Event> | null): EventListener | null {
// When an event listener throws, we want dispatching to stop and the
// error to be thrown so we can catch it and display a nice error page.
if (listener === undefined) return undefined;
// Unfortunately, Node's event target emits uncaught exceptions when a
// listener throws, which are tricky to catch without breaking tests (AVA
// also registers an uncaught exception handler).
//
// Node 16.6.0's internal implementation contains this line to throw
// uncaught exceptions: `process.nextTick(() => { throw err; });`.
// A potential solution would be to monkey-patch `process.nextTick` and call
// the callback immediately:
//
// ```js
// const originalNextTick = process.nextTick;
// process.nextTick = (callback) => callback();
// try {
// this.dispatchEvent(event);
// } finally {
// process.nextTick = originalNextTick;
// }
// ```
//
// However, this relies on internal behaviour that may change at any point
// and may prevent the EventTarget from doing required clean up. Hence,
// we wrap event listeners instead, storing the wrapped versions in a
// WeakMap so they can be removed when the original listener is passed to
// removeEventListener.

if (listener === null) return null;
const wrappedListeners = this.#wrappedListeners;
let wrappedListener = wrappedListeners.get(listener);
if (wrappedListener) return wrappedListener;
wrappedListener = (event) => {
try {
if ("handleEvent" in listener) {
listener.handleEvent(event as EventMap[T]);
listener.handleEvent(event);
} else {
// @ts-expect-error "this" type is definitely correct
listener(event as EventMap[T]);
listener(event);
}
} catch (error) {
event.stopImmediatePropagation();
Expand All @@ -143,20 +164,20 @@ export class ServiceWorkerGlobalScope extends EventTarget<EventMap> {
return wrappedListener;
}

addEventListener<T extends keyof EventMap>(
type: T,
listener?: EventTarget.EventListener<this, EventMap[T]> | null,
options?: EventTarget.AddOptions | boolean
addEventListener<EventType extends keyof EventMap>(
type: EventType,
listener: TypedEventListener<EventMap[EventType]> | null,
options?: AddEventListenerOptions | boolean
): void {
super.addEventListener(type, this.#wrap(listener), options as any);
super.addEventListener(type, this.#wrap(listener as any), options);
}

removeEventListener<T extends string & keyof EventMap>(
type: T,
listener?: EventTarget.EventListener<this, EventMap[T]> | null,
options?: EventTarget.Options | boolean
removeEventListener<EventType extends keyof EventMap>(
type: EventType,
listener: TypedEventListener<EventMap[EventType]> | null,
options?: EventListenerOptions | boolean
): void {
super.removeEventListener(type, this.#wrap(listener), options as any);
super.removeEventListener(type, this.#wrap(listener as any), options);
}

dispatchEvent(event: Event): boolean {
Expand Down
17 changes: 11 additions & 6 deletions src/modules/standards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,32 @@ import {
import { URL, URLSearchParams } from "url";
import { TextDecoder, TextEncoder } from "util";
import originalFetch, {
FetchError,
Headers,
Response as RawResponse,
Request,
RequestInfo,
RequestInit,
Response,
} from "@mrbbot/node-fetch";
import FormData from "formdata-node";
import WebSocket from "ws";
import StandardWebSocket from "ws";
import { Log } from "../log";
import { Context, Module } from "./module";
import { WebSocketPair, terminateWebSocket } from "./ws";
import { WebSocket, WebSocketPair, terminateWebSocket } from "./ws";

// Parameterise Response with correct WebSocket type
export type Response = RawResponse<WebSocket>;
export const Response = RawResponse;

export {
URL,
URLSearchParams,
TextDecoder,
TextEncoder,
FetchError,
Headers,
FormData,
Request,
Response,
ByteLengthQueuingStrategy,
CountQueuingStrategy,
ReadableByteStreamController,
Expand Down Expand Up @@ -83,7 +88,7 @@ crypto.subtle.digest = function (algorithm, data) {
};

export class StandardsModule extends Module {
private webSockets: WebSocket[];
private webSockets: StandardWebSocket[];
private readonly sandbox: Context;

constructor(log: Log) {
Expand Down Expand Up @@ -199,7 +204,7 @@ export class StandardsModule extends Module {
for (const [key, value] of request.headers.entries()) {
headers[key] = value;
}
const ws = new WebSocket(request.url, {
const ws = new StandardWebSocket(request.url, {
followRedirects: request.redirect === "follow",
maxRedirects: request.follow,
headers,
Expand Down
13 changes: 6 additions & 7 deletions src/modules/ws.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import assert from "assert";
import { Event, EventTarget } from "event-target-shim";
import StandardWebSocket from "ws";
import { MiniflareError } from "../helpers";
import { MiniflareError, typedEventTarget } from "../helpers";
import { Context, Module } from "./module";

export class MessageEvent extends Event<"message"> {
export class MessageEvent extends Event {
constructor(public readonly data: string) {
super("message");
}
}

export class CloseEvent extends Event<"close"> {
export class CloseEvent extends Event {
constructor(public readonly code?: number, public readonly reason?: string) {
super("close");
}
}

export class ErrorEvent extends Event<"error"> {
export class ErrorEvent extends Event {
constructor(public readonly error?: Error) {
super("error");
}
Expand All @@ -32,7 +31,7 @@ type EventMap = {
close: CloseEvent;
error: ErrorEvent;
};
export class WebSocket extends EventTarget<EventMap> {
export class WebSocket extends typedEventTarget<EventMap>() {
public static CONNECTING = 0;
public static OPEN = 1;
public static CLOSING = 2;
Expand Down Expand Up @@ -103,6 +102,7 @@ export class WebSocket extends EventTarget<EventMap> {
this.#readyState = WebSocket.CLOSING;
pair.#readyState = WebSocket.CLOSING;

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

Expand Down Expand Up @@ -183,7 +183,6 @@ export class WebSocketsModule extends Module {
MessageEvent,
CloseEvent,
ErrorEvent,
WebSocket,
WebSocketPair,
};
}
Expand Down
Loading

0 comments on commit 4c3cb14

Please sign in to comment.