Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve: reliability of the library when windows reload #7

Merged
merged 2 commits into from
Apr 13, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
improve: reliability of library's shutdown routine
  • Loading branch information
3p3r committed Apr 12, 2024
commit 7caa6cf6b3ecb6d99d9afdc04d417d82946d5621
2,052 changes: 1,232 additions & 820 deletions package-lock.json

Large diffs are not rendered by default.

30 changes: 15 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fakettp",
"version": "1.9.0",
"version": "1.9.1",
"description": "sandbox node core http module in a service worker.",
"main": "dist/fakettp.js",
"scripts": {
Expand All @@ -26,33 +26,33 @@
"author": "Sepehr Laal",
"license": "MIT",
"devDependencies": {
"@types/debug": "^4.1.11",
"@types/node": "^20.9.0",
"@types/debug": "^4.1.12",
"@types/node": "^20.12.7",
"@types/webpack": "^5.28.5",
"@wdio/cli": "^8.21.0",
"@wdio/local-runner": "^8.21.0",
"@wdio/mocha-framework": "^8.21.0",
"@wdio/spec-reporter": "^8.21.0",
"@wdio/cli": "^8.35.1",
"@wdio/local-runner": "^8.35.1",
"@wdio/mocha-framework": "^8.35.0",
"@wdio/spec-reporter": "^8.32.4",
"assert": "*",
"buffer": "*",
"copy-webpack-plugin": "^11.0.0",
"copy-webpack-plugin": "^12.0.2",
"cors": "^2.8.5",
"debug": "*",
"events": "*",
"html-webpack-plugin": "^5.5.3",
"html-webpack-plugin": "^5.6.0",
"null-loader": "^4.0.1",
"process": "*",
"stream-browserify": "*",
"stream-http": "*",
"terser-webpack-plugin": "^5.3.9",
"ts-loader": "^9.5.0",
"ts-node": "^10.9.1",
"typescript": "^5.2.2",
"terser-webpack-plugin": "^5.3.10",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"typescript": "^5.4.5",
"url": "*",
"util": "*",
"webpack": "^5.89.0",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1",
"webpack-dev-server": "^5.0.4",
"webpack-shell-plugin-next": "^2.3.1"
},
"peerDependencies": {
Expand Down
1 change: 0 additions & 1 deletion src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import debug from "debug";
const log = debug("fakettp:sw");

export const FIN = "\x00" as const;
export const ARM = "\x01" as const;

export function getBundledWorkerFileName() {
return process.env.WEBPACK_FILENAME || "fakettp.js";
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import debug from "debug";

import { createProxyClient } from "./sw";
import { createProxyServer, IncomingMessage, ServerResponse } from "./mt";
import { isRunningInBrowserWindow, isRunningInServiceWorker } from "./common";
import { createProxyServer, IncomingMessage, ServerResponse, unload } from "./mt";

import type { RequestListener } from "http";

Expand All @@ -22,6 +22,7 @@ if (isRunningInServiceWorker()) createProxyClient();

const http = {
..._http,
unload,
ServerResponse,
IncomingMessage,
createServer: isRunningInBrowserWindow()
Expand Down
153 changes: 64 additions & 89 deletions src/mt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { EventEmitter } from "events";
import type { RequestListener } from "http";
import { Writable, Duplex, Readable } from "stream";
import {
ARM,
FIN,
uniqueId,
Singleton,
Expand Down Expand Up @@ -308,7 +307,7 @@ class Server extends EventEmitter {
}
log("listening on address: %o", this.address());
const _last = args.pop();
const _done = typeof _last === "function" ? (_last as (error?: Error) => void) : () => { };
const _done = typeof _last === "function" ? (_last as (error?: Error) => void) : () => {};
this.once("error", _done);
this.once("listening", _done);
if (proxyInstance.get.armed) {
Expand All @@ -318,131 +317,107 @@ class Server extends EventEmitter {
} else {
log("starting to believe...");
proxyInstance.get
.disarm()
.then(() => proxyInstance.get.arm())
.then(() => proxyInstance.get.sw)
.then((sw) => {
.arm()
.then(async () => {
while (true) {
try {
const response = await fetch(`/__status__`);
if (response.status === 200) break;
} catch (error) {
log("error fetching /__status__: %o", error);
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
})
.then(() => {
log("service worker ready");
sw.postMessage([this._host, this._port.toString()]);
this.once("close", () => {
log("closing service worker");
proxyInstance.get.disarm();
});
navigator.serviceWorker.addEventListener(
"message",
(event: MessageEvent<SerializedRequest | typeof ARM | typeof FIN>) => {
if (event.data === ARM) {
log("message received from service worker: ARM");
this.emit("listening");
return;
}
if (event.data === FIN) {
log("message received from service worker: FIN");
return;
}
log("message received from service worker");
const responseChannel = new MessageChannel();
const responsePort = responseChannel.port1;
const requestPort = event.ports[0];
const request = deserializeRequest(event.data);
const socket = new Socket(request, { responsePort, requestPort });
this.emit("connection", socket);
const message = new IncomingMessage(request, socket);
const response = new ServerResponse(request, socket);
const _wrapUp = () => {
if (response.headersSent) return;
log("closing request stream");
response.headersSent = true;
log("responding to service worker");
event.source.postMessage(
{
id: request.id,
status: response.statusCode,
statusText: response.statusMessage,
headers: response.getHeaders(),
} as SerializedResponse,
{
transfer: [responseChannel.port2],
targetOrigin: event.origin,
}
);
};
response.once("finish", () => {
if (message.complete) _wrapUp();
else message.once("end", _wrapUp);
});
if (this.listenerCount("request") === 0) {
response.writeHead(418, "I'm a teapot");
response.end();
} else {
this.emit("request", message, response);
}
this.emit("listening");
navigator.serviceWorker.addEventListener("message", (event: MessageEvent<SerializedRequest>) => {
log("message received from service worker");
const responseChannel = new MessageChannel();
const responsePort = responseChannel.port1;
const requestPort = event.ports[0];
const request = deserializeRequest(event.data);
const socket = new Socket(request, { responsePort, requestPort });
this.emit("connection", socket);
const message = new IncomingMessage(request, socket);
const response = new ServerResponse(request, socket);
const _wrapUp = () => {
if (response.headersSent) return;
log("closing request stream");
response.headersSent = true;
log("responding to service worker");
event.source.postMessage(
{
id: request.id,
status: response.statusCode,
statusText: response.statusMessage,
headers: response.getHeaders(),
} as SerializedResponse,
{
transfer: [responseChannel.port2],
targetOrigin: event.origin,
}
);
};
response.once("finish", () => {
if (message.complete) _wrapUp();
else message.once("end", _wrapUp);
});
if (this.listenerCount("request") === 0) {
response.writeHead(418, "I'm a teapot");
response.end();
} else {
this.emit("request", message, response);
}
);
});
});
}
return this;
}
close(callback?: (error?: Error) => void) {
proxyInstance.get
.disarm()
.then(() => callback?.())
.then(() => callback?.(undefined))
.catch(callback);
return this;
}
}

function createProxyInstance(): ProxyWindowInstance {
let armed = false;
let sw: Promise<ServiceWorker | null> = null;
return {
get armed() {
return armed;
return false;
},
mt: globalThis,
get sw() {
if (sw) return sw;
sw = navigator.serviceWorker.ready.then((registration) => {
return registration.active;
return navigator.serviceWorker.ready.then((registration) => {
return registration.active || registration.installing || registration.waiting;
});
return sw;
},
async arm() {
if (armed) return;
await navigator.serviceWorker.register(getBundledWorkerFileName());
const sw = await this.sw;
sw.postMessage(ARM);
armed = true;
},
async disarm() {
sw = null;
armed = false;
const registration = await navigator.serviceWorker.getRegistration(getBundledWorkerFileName());
async function _postFinAndWait(worker?: ServiceWorker | null) {
if (!worker) return;
const barrier = new Promise((resolve) => {
worker.addEventListener("message", (event: MessageEvent<typeof FIN>) => {
if (event.data === FIN) resolve(undefined);
});
});
const timeout = new Promise((resolve) => setTimeout(resolve, 100));
worker.postMessage(FIN);
await Promise.race([barrier, timeout]);
}
await Promise.all([registration?.active, registration?.waiting, registration?.installing].map(_postFinAndWait));
await registration?.unregister();
await unload();
},
};
}

const proxyInstance = isRunningInBrowserWindow()
? new Singleton(() => {
const proxyInstance = createProxyInstance();
return proxyInstance;
})
const proxyInstance = createProxyInstance();
return proxyInstance;
})
: null;

proxyInstance?.get.disarm();
export async function unload() {
await navigator.serviceWorker.register("nosw.js");
}

export function createProxyServer(requestListener?: RequestListener): Server {
const server = new Server();
Expand Down
26 changes: 26 additions & 0 deletions src/nosw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/// <reference lib="webworker" />

// https://developer.chrome.com/docs/workbox/remove-buggy-service-workers

self.addEventListener("install", () => {
// Skip over the "waiting" lifecycle state, to ensure that our
// new service worker is activated immediately, even if there's
// another tab open controlled by our older service worker code.
(self as unknown as ServiceWorkerGlobalScope).skipWaiting();
});

self.addEventListener("activate", () => {
// Optional: Get a list of all the current open windows/tabs under
// our service worker's control, and force them to reload.
// This can "unbreak" any open windows/tabs as soon as the new
// service worker activates, rather than users having to manually reload.
(self as unknown as ServiceWorkerGlobalScope).clients
.matchAll({
type: "window",
})
.then((windowClients) => {
windowClients.forEach((windowClient) => {
windowClient.navigate(windowClient.url);
});
});
});
Loading