Skip to content

Commit e364aa0

Browse files
committed
[scramjet/core] create plugin api, add fetch and rewriter.html hooks
1 parent dc5c14d commit e364aa0

File tree

6 files changed

+234
-102
lines changed

6 files changed

+234
-102
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
type Description = {
2+
context?: object;
3+
props?: object;
4+
};
5+
6+
type Callback<T extends Description> = (
7+
context: T["context"],
8+
props: T["props"]
9+
) => void | Promise<void>;
10+
11+
type Sorter = (other: Plugin) => number;
12+
13+
type CallbackInfo<T extends Description> = {
14+
callback: Callback<T>;
15+
plugin: Plugin;
16+
sorter: Sorter;
17+
};
18+
19+
type InternalHookDescription = {
20+
tap: TapInternal;
21+
key: string;
22+
};
23+
24+
type TapInternal = {
25+
callbacks: Record<string, CallbackInfo<Description>[]>;
26+
};
27+
28+
export type TapInstance<T extends Record<string, Description>> = {
29+
[K in keyof T]: T[K] & InternalHookDescription;
30+
};
31+
32+
export class Plugin {
33+
constructor(public name: string) {}
34+
35+
tap<T extends Description>(
36+
hook: T,
37+
callback: Callback<T>,
38+
sorter?: Sorter
39+
): void {
40+
sorter ??= () => 0;
41+
Tap.tap(hook, callback, this, sorter);
42+
}
43+
}
44+
45+
export class Tap {
46+
static dispatch<T extends Description>(
47+
hook: T,
48+
context: T["context"],
49+
props: T["props"]
50+
): Promise<void[]> {
51+
let internal = hook as unknown as InternalHookDescription;
52+
let callbacks = internal.tap.callbacks[internal.key];
53+
if (!callbacks || callbacks.length === 0) return;
54+
55+
callbacks = [...callbacks];
56+
callbacks.sort((a, b) => a.sorter(b.plugin));
57+
58+
const results = callbacks.map((cb) => cb.callback(context, props));
59+
return Promise.all(results);
60+
}
61+
62+
static tap<T extends Description>(
63+
hook: T,
64+
callback: Callback<T>,
65+
plugin: Plugin,
66+
sorter: Sorter
67+
) {
68+
let internal = hook as unknown as InternalHookDescription;
69+
let callbacks = internal.tap.callbacks;
70+
if (!callbacks[internal.key]) callbacks[internal.key] = [];
71+
callbacks[internal.key]!.push({
72+
callback,
73+
plugin,
74+
sorter,
75+
});
76+
}
77+
78+
static create<T extends Record<string, Description>>(): TapInstance<T> {
79+
const internal: TapInternal = {
80+
callbacks: {},
81+
};
82+
const hooks: Record<string, InternalHookDescription> = {};
83+
84+
return new Proxy(internal as unknown as TapInstance<T>, {
85+
get(target, key: string) {
86+
if (key === "callbacks") return internal.callbacks;
87+
if (!hooks[key]) {
88+
hooks[key] = { tap: internal, key };
89+
}
90+
return hooks[key];
91+
},
92+
});
93+
}
94+
}

packages/scramjet/packages/core/src/client/client.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ import { createLocationProxy } from "@client/location";
99
import { createWrapFn } from "@client/shared/wrap";
1010
import { NavigateEvent } from "@client/events";
1111
import { rewriteUrl, unrewriteUrl, type URLMeta } from "@rewriters/url";
12-
import { flagEnabled, ScramjetContext, ScramjetInterface } from "@/shared";
12+
import {
13+
flagEnabled,
14+
HtmlRewriterTap,
15+
ScramjetContext,
16+
ScramjetInterface,
17+
} from "@/shared";
1318
import { CookieJar } from "@/shared/cookie";
1419
import { iswindow } from "./entry";
1520
import { SingletonBox } from "./singletonbox";
@@ -135,6 +140,12 @@ export class ScramjetClient {
135140

136141
context: ScramjetContext;
137142

143+
hooks = {
144+
rewriter: {
145+
html: HtmlRewriterTap,
146+
},
147+
};
148+
138149
constructor(
139150
public global: typeof globalThis,
140151
public init: ScramjetClientInit
@@ -160,6 +171,10 @@ export class ScramjetClient {
160171
this.box.registerClient(this, global as Self);
161172

162173
this.context = init.context;
174+
this.context.hooks = {
175+
rewriter: this.hooks.rewriter,
176+
};
177+
163178
this.bare = new BareCompatibleClient(init.transport);
164179

165180
this.serviceWorker = this.global.navigator.serviceWorker;

packages/scramjet/packages/core/src/fetch/index.ts

Lines changed: 67 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@ import {
1515
} from "@rewriters/url";
1616
import { rewriteJs } from "@rewriters/js";
1717
import { ScramjetHeaders } from "@/shared/headers";
18-
import { flagEnabled, ScramjetContext } from "@/shared";
18+
import { flagEnabled, HtmlRewriterHooks, ScramjetContext } from "@/shared";
1919
import { rewriteHtml } from "@rewriters/html";
2020
import { rewriteCss } from "@rewriters/css";
2121
import { rewriteWorkers } from "@rewriters/worker";
2222
import { ScramjetConfig } from "@/types";
2323
import DomHandler from "domhandler";
24+
import { Tap, TapInstance } from "@/Tap";
2425

2526
export interface ScramjetFetchRequest {
2627
rawUrl: URL;
@@ -66,6 +67,13 @@ export class ScramjetFetchHandler extends EventTarget {
6667
public crossOriginIsolated: boolean = false;
6768
public context: ScramjetContext;
6869

70+
public hooks: {
71+
rewriter: {
72+
html: TapInstance<HtmlRewriterHooks>;
73+
};
74+
fetch: TapInstance<FetchHooks>;
75+
};
76+
6977
public fetchDataUrl: (dataUrl: string) => Promise<Response>;
7078
public fetchBlobUrl: (blobUrl: string) => Promise<Response>;
7179
public sendSetCookie: (url: URL, cookie: string) => Promise<void>;
@@ -78,6 +86,15 @@ export class ScramjetFetchHandler extends EventTarget {
7886
this.sendSetCookie = init.sendSetCookie;
7987
this.fetchDataUrl = init.fetchDataUrl;
8088
this.fetchBlobUrl = init.fetchBlobUrl;
89+
this.hooks = {
90+
rewriter: {
91+
html: Tap.create<HtmlRewriterHooks>(),
92+
},
93+
fetch: Tap.create<FetchHooks>(),
94+
};
95+
this.context.hooks = {
96+
rewriter: this.hooks.rewriter,
97+
};
8198
}
8299

83100
async handleFetch(
@@ -113,22 +130,20 @@ async function doHandleFetch(
113130
redirect: "manual",
114131
} as BareRequestInit;
115132

116-
const req = new ScramjetRequestEvent(
133+
let reqcontext: typeof handler.hooks.fetch.request.context = {
134+
client: handler.client,
117135
request,
118-
parsed.url,
119136
parsed,
137+
};
138+
let reqprops: typeof handler.hooks.fetch.request.props = {
120139
init,
121-
handler.client
122-
);
123-
handler.dispatchEvent(req);
124-
140+
url: parsed.url,
141+
};
142+
await Tap.dispatch(handler.hooks.fetch.request, reqcontext, reqprops);
125143
let response: BareResponse;
126144

127-
if (req._response) {
128-
let resp = req._response;
129-
if ("then" in resp) {
130-
resp = await resp;
131-
}
145+
if (reqprops.earlyResponse) {
146+
let resp = reqprops.earlyResponse;
132147
if ("rawHeaders" in resp) {
133148
// it's a bare response
134149
response = resp;
@@ -137,7 +152,7 @@ async function doHandleFetch(
137152
response = BareResponse.fromNativeResponse(resp);
138153
}
139154
} else {
140-
response = await handler.client.fetch(req.url, req.init);
155+
response = await handler.client.fetch(reqprops.url, reqprops.init);
141156
}
142157

143158
let responseBody: BodyType;
@@ -191,18 +206,22 @@ async function doHandleFetch(
191206
// await cleanTracker(parsed.url.toString());
192207
// }
193208

194-
const resp = new ScramjetResponseEvent(request, parsed, {
195-
body: responseBody,
196-
headers: responseHeaders,
197-
status: response.status,
198-
statusText: response.statusText,
199-
});
200-
handler.dispatchEvent(resp);
209+
let respcontext: typeof handler.hooks.fetch.response.context = {
210+
request,
211+
parsed,
212+
};
213+
let respprops: typeof handler.hooks.fetch.response.props = {
214+
response: {
215+
body: responseBody,
216+
headers: responseHeaders,
217+
status: response.status,
218+
statusText: response.statusText,
219+
},
220+
};
201221

202-
let r = resp.response;
203-
if (resp._response) r = await resp._response;
222+
await Tap.dispatch(handler.hooks.fetch.response, respcontext, respprops);
204223

205-
return r;
224+
return respprops.response;
206225
}
207226

208227
function isRedirect(response: BareResponse) {
@@ -682,23 +701,7 @@ async function rewriteBody(
682701
await response.text(),
683702
handler.context,
684703
parsed.meta,
685-
true,
686-
(domhandler) => {
687-
const evt = new ScramjetHTMLPreRewriteEvent(
688-
domhandler,
689-
request,
690-
parsed
691-
);
692-
handler.dispatchEvent(evt);
693-
},
694-
(domhandler) => {
695-
const evt = new ScramjetHTMLPostRewriteEvent(
696-
domhandler,
697-
request,
698-
parsed
699-
);
700-
handler.dispatchEvent(evt);
701-
}
704+
true
702705
);
703706
} else {
704707
return response.body;
@@ -729,60 +732,28 @@ async function rewriteBody(
729732
}
730733
}
731734

732-
type BodyType = string | ArrayBuffer | Blob | ReadableStream<any>;
733-
734-
export class ScramjetHTMLPreRewriteEvent extends Event {
735-
constructor(
736-
public handler: DomHandler,
737-
public context: ScramjetFetchRequest,
738-
public parsed: ScramjetFetchParsed
739-
) {
740-
super("htmlPreRewrite");
741-
}
742-
}
743-
744-
export class ScramjetHTMLPostRewriteEvent extends Event {
745-
constructor(
746-
public handler: DomHandler,
747-
public context: ScramjetFetchRequest,
748-
public parsed: ScramjetFetchParsed
749-
) {
750-
super("htmlPostRewrite");
751-
}
752-
}
753-
754-
export class ScramjetResponseEvent extends Event {
755-
_response?: ScramjetFetchResponse | Promise<ScramjetFetchResponse>;
756-
constructor(
757-
public context: ScramjetFetchRequest,
758-
public parsed: ScramjetFetchParsed,
759-
public response: ScramjetFetchResponse
760-
) {
761-
super("handleResponse");
762-
}
763-
respondWith(
764-
response: ScramjetFetchResponse | Promise<ScramjetFetchResponse>
765-
) {
766-
this._response = response;
767-
}
768-
}
735+
export type FetchHooks = {
736+
request: {
737+
context: {
738+
request: ScramjetFetchRequest;
739+
parsed: ScramjetFetchParsed;
740+
client: BareCompatibleClient;
741+
};
742+
props: {
743+
init: BareRequestInit;
744+
url: URL;
745+
earlyResponse?: BareResponse;
746+
};
747+
};
748+
response: {
749+
context: {
750+
request: ScramjetFetchRequest;
751+
parsed: ScramjetFetchParsed;
752+
};
753+
props: {
754+
response: ScramjetFetchResponse;
755+
};
756+
};
757+
};
769758

770-
export class ScramjetRequestEvent extends Event {
771-
_response?:
772-
| BareResponse
773-
| Promise<BareResponse>
774-
| Response
775-
| Promise<Response>;
776-
constructor(
777-
public context: ScramjetFetchRequest,
778-
public url: URL,
779-
public parsed: ScramjetFetchParsed,
780-
public init: BareRequestInit,
781-
public client: BareCompatibleClient
782-
) {
783-
super("request");
784-
}
785-
respondWith(response: BareResponse | Promise<BareResponse>) {
786-
this._response = response;
787-
}
788-
}
759+
type BodyType = string | ArrayBuffer | Blob | ReadableStream<any>;

packages/scramjet/packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export * from "./shared";
1010
export * from "./symbols";
1111
export * from "./types";
1212
export * from "./fetch";
13+
export * from "./Tap";
1314

1415
declare const REWRITERWASM: string | undefined;
1516

0 commit comments

Comments
 (0)