Skip to content

Commit 4f56868

Browse files
authored
[scramjet/core] create plugin api, add fetch and rewriter.html hooks (#43)
1 parent 5d7e0aa commit 4f56868

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
import { sniffEncoding } from "@/shared/sniffEncoding";
2526

2627
export interface ScramjetFetchRequest {
@@ -67,6 +68,13 @@ export class ScramjetFetchHandler extends EventTarget {
6768
public crossOriginIsolated: boolean = false;
6869
public context: ScramjetContext;
6970

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

84101
async handleFetch(
@@ -127,22 +144,20 @@ async function doHandleFetch(
127144
redirect: "manual",
128145
} as BareRequestInit;
129146

130-
const req = new ScramjetRequestEvent(
147+
let reqcontext: typeof handler.hooks.fetch.request.context = {
148+
client: handler.client,
131149
request,
132-
parsed.url,
133150
parsed,
151+
};
152+
let reqprops: typeof handler.hooks.fetch.request.props = {
134153
init,
135-
handler.client
136-
);
137-
handler.dispatchEvent(req);
138-
154+
url: parsed.url,
155+
};
156+
await Tap.dispatch(handler.hooks.fetch.request, reqcontext, reqprops);
139157
let response: BareResponse;
140158

141-
if (req._response) {
142-
let resp = req._response;
143-
if ("then" in resp) {
144-
resp = await resp;
145-
}
159+
if (reqprops.earlyResponse) {
160+
let resp = reqprops.earlyResponse;
146161
if ("rawHeaders" in resp) {
147162
// it's a bare response
148163
response = resp;
@@ -151,7 +166,7 @@ async function doHandleFetch(
151166
response = BareResponse.fromNativeResponse(resp);
152167
}
153168
} else {
154-
response = await handler.client.fetch(req.url, req.init);
169+
response = await handler.client.fetch(reqprops.url, reqprops.init);
155170
}
156171

157172
let responseBody: BodyType;
@@ -210,18 +225,22 @@ async function doHandleFetch(
210225
// await cleanTracker(parsed.url.toString());
211226
// }
212227

213-
const resp = new ScramjetResponseEvent(request, parsed, {
214-
body: responseBody,
215-
headers: responseHeaders,
216-
status: response.status,
217-
statusText: response.statusText,
218-
});
219-
handler.dispatchEvent(resp);
228+
let respcontext: typeof handler.hooks.fetch.response.context = {
229+
request,
230+
parsed,
231+
};
232+
let respprops: typeof handler.hooks.fetch.response.props = {
233+
response: {
234+
body: responseBody,
235+
headers: responseHeaders,
236+
status: response.status,
237+
statusText: response.statusText,
238+
},
239+
};
220240

221-
let r = resp.response;
222-
if (resp._response) r = await resp._response;
241+
await Tap.dispatch(handler.hooks.fetch.response, respcontext, respprops);
223242

224-
return r;
243+
return respprops.response;
225244
}
226245

227246
function isRedirect(response: BareResponse) {
@@ -698,23 +717,7 @@ async function rewriteBody(
698717
htmlContent,
699718
handler.context,
700719
parsed.meta,
701-
true,
702-
(domhandler) => {
703-
const evt = new ScramjetHTMLPreRewriteEvent(
704-
domhandler,
705-
request,
706-
parsed
707-
);
708-
handler.dispatchEvent(evt);
709-
},
710-
(domhandler) => {
711-
const evt = new ScramjetHTMLPostRewriteEvent(
712-
domhandler,
713-
request,
714-
parsed
715-
);
716-
handler.dispatchEvent(evt);
717-
}
720+
true
718721
);
719722
} else {
720723
return response.body;
@@ -745,60 +748,28 @@ async function rewriteBody(
745748
}
746749
}
747750

748-
type BodyType = string | ArrayBuffer | Blob | ReadableStream<any>;
749-
750-
export class ScramjetHTMLPreRewriteEvent extends Event {
751-
constructor(
752-
public handler: DomHandler,
753-
public context: ScramjetFetchRequest,
754-
public parsed: ScramjetFetchParsed
755-
) {
756-
super("htmlPreRewrite");
757-
}
758-
}
759-
760-
export class ScramjetHTMLPostRewriteEvent extends Event {
761-
constructor(
762-
public handler: DomHandler,
763-
public context: ScramjetFetchRequest,
764-
public parsed: ScramjetFetchParsed
765-
) {
766-
super("htmlPostRewrite");
767-
}
768-
}
769-
770-
export class ScramjetResponseEvent extends Event {
771-
_response?: ScramjetFetchResponse | Promise<ScramjetFetchResponse>;
772-
constructor(
773-
public context: ScramjetFetchRequest,
774-
public parsed: ScramjetFetchParsed,
775-
public response: ScramjetFetchResponse
776-
) {
777-
super("handleResponse");
778-
}
779-
respondWith(
780-
response: ScramjetFetchResponse | Promise<ScramjetFetchResponse>
781-
) {
782-
this._response = response;
783-
}
784-
}
751+
export type FetchHooks = {
752+
request: {
753+
context: {
754+
request: ScramjetFetchRequest;
755+
parsed: ScramjetFetchParsed;
756+
client: BareCompatibleClient;
757+
};
758+
props: {
759+
init: BareRequestInit;
760+
url: URL;
761+
earlyResponse?: BareResponse;
762+
};
763+
};
764+
response: {
765+
context: {
766+
request: ScramjetFetchRequest;
767+
parsed: ScramjetFetchParsed;
768+
};
769+
props: {
770+
response: ScramjetFetchResponse;
771+
};
772+
};
773+
};
785774

786-
export class ScramjetRequestEvent extends Event {
787-
_response?:
788-
| BareResponse
789-
| Promise<BareResponse>
790-
| Response
791-
| Promise<Response>;
792-
constructor(
793-
public context: ScramjetFetchRequest,
794-
public url: URL,
795-
public parsed: ScramjetFetchParsed,
796-
public init: BareRequestInit,
797-
public client: BareCompatibleClient
798-
) {
799-
super("request");
800-
}
801-
respondWith(response: BareResponse | Promise<BareResponse>) {
802-
this._response = response;
803-
}
804-
}
775+
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)