swruntime: implement fetch event

This commit is contained in:
velzie 2024-08-02 21:57:33 -04:00
parent 14a0305bdb
commit 15bc9598c9
No known key found for this signature in database
GPG key ID: 048413F95F0DDE1F
9 changed files with 273 additions and 31 deletions

View file

@ -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);

View file

@ -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;

View file

@ -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(
{

View file

@ -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();
}
}

View file

@ -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];

View file

@ -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>
) => {
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;

View file

@ -1,3 +1,56 @@
import { type MessageW2R, type MessageR2W } from "../client/swruntime";
export class FakeServiceWorker {
constructor(public handle: MessagePort) {}
syncToken = 0;
promises: Record<number, (val?: MessageR2W) => 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<Response> {
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,
});
}
}

View file

@ -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")) {

View file

@ -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;