diff --git a/src/client/client.ts b/src/client/client.ts index 46457c1..3656b34 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -46,6 +46,17 @@ export class ScramjetClient { windowProxy: any; locationProxy: any; + eventcallbacks: WeakMap< + any, + [ + { + event: string; + originalCallback: AnyFunction; + proxiedCallback: AnyFunction; + }, + ] + > = new WeakMap(); + constructor(public global: typeof globalThis) { if ("document" in self) { this.documentProxy = createDocumentProxy(this, global); diff --git a/src/client/dom/origin.ts b/src/client/dom/origin.ts index f2e23ff..d89e16d 100644 --- a/src/client/dom/origin.ts +++ b/src/client/dom/origin.ts @@ -29,6 +29,15 @@ export default function (client: ScramjetClient, self: typeof window) { }, }); + client.Trap("document.documentURI", { + get() { + return decodeUrl(self.location.href); + }, + set() { + return false; + }, + }); + client.Trap("document.domain", { get() { return client.url.hostname; diff --git a/src/client/dom/serviceworker.ts b/src/client/dom/serviceworker.ts index 5fd2ad2..1b3a133 100644 --- a/src/client/dom/serviceworker.ts +++ b/src/client/dom/serviceworker.ts @@ -13,7 +13,7 @@ export default function (client: ScramjetClient, self: Self) { }, }); - client.Proxy("addEventListener", { + client.Proxy("EventTarget.prototype.addEventListener", { apply(ctx) { if (fakeregistrations.has(ctx.this)) { // do nothing @@ -22,7 +22,7 @@ export default function (client: ScramjetClient, self: Self) { }, }); - client.Proxy("removeEventListener", { + client.Proxy("EventTarget.prototype.removeEventListener", { apply(ctx) { if (fakeregistrations.has(ctx.this)) { // do nothing @@ -38,9 +38,10 @@ export default function (client: ScramjetClient, self: Self) { if (ctx.args[1] && ctx.args[1].type === "module") { url += "&type=module"; } - let worker = new SharedWorker(url); - let handle = worker.port; + const worker = new SharedWorker(url); + + const handle = worker.port; navigator.serviceWorker.controller.postMessage( { diff --git a/src/client/index.ts b/src/client/index.ts index 7407c13..532bc5e 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -1,6 +1,7 @@ // entrypoint for scramjet.client.js import { ScramjetClient } from "./client"; +import { ScramjetServiceWorkerRuntime } from "./swruntime"; export const iswindow = "window" in self; export const isworker = "WorkerGlobalScope" in self; @@ -13,4 +14,9 @@ dbg.log("scrammin"); if (!(ScramjetClient.SCRAMJET in self)) { const client = new ScramjetClient(self); client.hook(); + + if (issw) { + const runtime = new ScramjetServiceWorkerRuntime(client); + runtime.hook(); + } } diff --git a/src/client/shared/event.ts b/src/client/shared/event.ts index 176e578..221c314 100644 --- a/src/client/shared/event.ts +++ b/src/client/shared/event.ts @@ -7,6 +7,14 @@ const realOnEvent = Symbol.for("scramjet original onevent function"); export default function (client: ScramjetClient, self: Self) { const handlers = { message: { + _init() { + if (typeof this.data === "object" && "$scramjet$type" in this.data) { + // this is a ctl message + return false; + } + + return true; + }, origin() { if (typeof this.data === "object" && "$scramjet$origin" in this.data) return this.data.$scramjet$origin; @@ -33,7 +41,12 @@ export default function (client: ScramjetClient, self: Self) { const type = realEvent.type; if (type in handlers) { - let handler = handlers[type]; + const handler = handlers[type]; + + if (handler._init) { + if (handler._init.call(realEvent) === false) return; + } + argArray[0] = new Proxy(realEvent, { get(_target, prop, reciever) { if (prop in handler) { @@ -54,9 +67,41 @@ export default function (client: ScramjetClient, self: Self) { client.Proxy("EventTarget.prototype.addEventListener", { apply(ctx) { unproxy(ctx, client); - // if (ctx.args[0] === "message" && iswindow) debugger; - if (typeof ctx.args[1] === "function") - ctx.args[1] = wraplistener(ctx.args[1]); + if (typeof ctx.args[1] !== "function") return; + + const origlistener = ctx.args[1]; + const proxylistener = wraplistener(origlistener); + + ctx.args[1] = proxylistener; + + let arr = client.eventcallbacks.get(ctx.this); + arr ||= [] as any; + arr.push({ + event: ctx.args[0] as string, + originalCallback: origlistener, + proxiedCallback: proxylistener, + }); + client.eventcallbacks.set(ctx.this, arr); + }, + }); + + client.Proxy("EventTarget.prototype.removeEventListener", { + apply(ctx) { + unproxy(ctx, client); + if (typeof ctx.args[1] !== "function") return; + + const arr = client.eventcallbacks.get(ctx.this); + if (!arr) return; + + const i = arr.findIndex( + (e) => e.event === ctx.args[0] && e.originalCallback === ctx.args[1] + ); + if (i === -1) return; + + arr.splice(i, 1); + client.eventcallbacks.set(ctx.this, arr); + + ctx.args[1] = arr[i].proxiedCallback; }, }); @@ -66,8 +111,6 @@ export default function (client: ScramjetClient, self: Self) { }, }); - // TODO: removeEventListener - if (!iswindow) return; const targets = [self.window, self.HTMLElement.prototype]; diff --git a/src/client/swruntime.ts b/src/client/swruntime.ts index 4694c92..7c73c9a 100644 --- a/src/client/swruntime.ts +++ b/src/client/swruntime.ts @@ -1,21 +1,121 @@ +import { ScramjetClient } from "./client"; import { encodeUrl } from "./shared"; -class ScramjetServiceWorkerRuntime { - constructor() { - addEventListener("message", (event) => { - if ("scramjet$type" in event.data) { - event.stopImmediatePropagation(); +export class ScramjetServiceWorkerRuntime { + constructor(public client: ScramjetClient) { + addEventListener("connect", (cevent: MessageEvent) => { + const port = cevent.ports[0]; - return; - } + port.addEventListener("message", (event) => { + if ("scramjet$type" in event.data) { + handleMessage(client, event.data, port); + } + }); + + port.start(); }); } + + hook() {} } -declare global { - interface Window { - ScramjetServiceWorkerRuntime: typeof ScramjetServiceWorkerRuntime; +function handleMessage( + client: ScramjetClient, + data: MessageW2R, + port: MessagePort +) { + const type = data.scramjet$type; + const token = data.scramjet$token; + + if (type === "fetch") { + const fetchhandlers = client.eventcallbacks.get("fetch"); + if (!fetchhandlers) return; + + for (const handler of fetchhandlers) { + const request = data.scramjet$request; + const fakeRequest = new Request(request.url, { + body: request.body, + headers: new Headers(request.headers), + method: request.method, + mode: request.mode, + }); + + Object.defineProperty(fakeRequest, "destination", { + value: request.destinitation, + }); + + const fakeFetchEvent = new FetchEvent("fetch", { + request: fakeRequest, + }); + + fakeFetchEvent.respondWith = async ( + response: Response | Promise + ) => { + response = await response; + + response.body; + port.postMessage({ + scramjet$type: "fetch", + scramjet$token: token, + scramjet$response: { + body: response.body, + headers: Array.from(response.headers.entries()), + status: response.status, + statusText: response.statusText, + }, + } as MessageR2W); + }; + + handler.proxiedCallback(trustEvent(fakeFetchEvent)); + } } } -self.ScramjetServiceWorkerRuntime = ScramjetServiceWorkerRuntime; +function trustEvent(event: Event): Event { + return new Proxy(event, { + get(target, prop, reciever) { + if (prop === "isTrusted") return true; + + return Reflect.get(target, prop, reciever); + }, + }); +} + +export type TransferrableResponse = { + body: ReadableStream; + headers: [string, string][]; + status: number; + statusText: string; +}; + +export type TransferrableRequest = { + body: ReadableStream; + headers: [string, string][]; + destinitation: RequestDestination; + method: Request["method"]; + mode: Request["mode"]; + url: string; +}; + +type FetchResponseMessage = { + scramjet$type: "fetch"; + scramjet$response: TransferrableResponse; +}; + +type FetchRequestMessage = { + scramjet$type: "fetch"; + scramjet$request: TransferrableRequest; +}; + +// r2w = runtime to (service) worker + +type MessageTypeR2W = FetchResponseMessage; +type MessageTypeW2R = FetchRequestMessage; + +type MessageCommon = { + scramjet$type: string; + scramjet$token: number; +}; + +export type MessageR2W = MessageCommon & MessageTypeR2W; +export type MessageW2R = MessageCommon & MessageTypeW2R; diff --git a/src/worker/fakesw.ts b/src/worker/fakesw.ts index 2cd08c2..eabcb5c 100644 --- a/src/worker/fakesw.ts +++ b/src/worker/fakesw.ts @@ -1,3 +1,56 @@ +import { type MessageW2R, type MessageR2W } from "../client/swruntime"; + export class FakeServiceWorker { - constructor(public handle: MessagePort) {} + syncToken = 0; + promises: Record void> = {}; + + constructor( + public handle: MessagePort, + public origin: string + ) { + this.handle.start(); + + this.handle.addEventListener("message", (event) => { + if ("scramjet$type" in event.data) { + this.handleMessage(event.data); + } + }); + } + + handleMessage(data: MessageR2W) { + const cb = this.promises[data.scramjet$token]; + if (cb) { + cb(data); + delete this.promises[data.scramjet$token]; + } + } + + async fetch(request: Request): Promise { + const token = this.syncToken++; + + const message: MessageW2R = { + scramjet$type: "fetch", + scramjet$token: token, + scramjet$request: { + url: request.url, + body: request.body, + headers: Array.from(request.headers.entries()), + method: request.method, + mode: request.mode, + destinitation: request.destination, + }, + }; + + this.handle.postMessage(message); + + const { scramjet$response: r } = (await new Promise((resolve) => { + this.promises[token] = resolve; + })) as MessageR2W; + + return new Response(r.body, { + headers: r.headers, + status: r.status, + statusText: r.statusText, + }); + } } diff --git a/src/worker/fetch.ts b/src/worker/fetch.ts index 7544beb..18c9a9e 100644 --- a/src/worker/fetch.ts +++ b/src/worker/fetch.ts @@ -35,6 +35,14 @@ export async function swfetch( }); } + const activeWorker = this.serviceWorkers.find( + (w) => w.origin === new URL(request.url).origin + ); + if (activeWorker) { + // TODO: check scope + return await activeWorker.fetch(request); + } + const urlParam = new URLSearchParams(new URL(request.url).search); if (urlParam.has("url")) { diff --git a/src/worker/index.ts b/src/worker/index.ts index 2e41fd1..2c19551 100644 --- a/src/worker/index.ts +++ b/src/worker/index.ts @@ -24,24 +24,17 @@ export class ScramjetServiceWorker { this.threadpool = new ScramjetThreadpool(); - addEventListener("message", ({ data }) => { + addEventListener("message", ({ data }: { data: MessageC2W }) => { if (!("scramjet$type" in data)) return; if (data.scramjet$type === "registerServiceWorker") { - this.serviceWorkers.push(new FakeServiceWorker(data.port)); + this.serviceWorkers.push(new FakeServiceWorker(data.port, data.origin)); return; } - if (!("scramjet$token" in data)) return; - const resolve = this.syncPool[data.scramjet$token]; delete this.syncPool[data.scramjet$token]; - if (data.scramjet$type === "getLocalStorage") { - resolve(data.data); - } else if (data.scramjet$type === "setLocalStorage") { - resolve(); - } }); } @@ -117,3 +110,21 @@ export class ScramjetServiceWorker { } self.ScramjetServiceWorker = ScramjetServiceWorker; + +type RegisterServiceWorkerMessage = { + scramjet$type: "registerServiceWorker"; + port: MessagePort; + origin: string; +}; + +type MessageCommon = { + scramjet$type: string; + scramjet$token: number; +}; + +type MessageTypeC2W = RegisterServiceWorkerMessage; +type MessageTypeW2C = never; + +// c2w: client to (service) worker +export type MessageC2W = MessageCommon & MessageTypeC2W; +export type MessageW2C = MessageCommon & MessageTypeW2C;