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

feat(workerd): experiment with serialization #28

Draft
wants to merge 20 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions examples/react-ssr-workerd/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ export default defineConfig((_env) => ({
"react/jsx-runtime",
"react/jsx-dev-runtime",
"react-dom/server.edge",
// [feedback]: esm also needs to be optimized? otherwise I get a following error:
// Error: Vite Internal Error: registerMissingImport is not supported in dev workerd
// at Object.registerMissingImport (file:///home/hiroshi/code/personal/vite-environment-examples/node_modules/.pnpm/vite@6.0.0-alpha.1_@types+node@20.11.30/node_modules/vite/dist/node/chunks/dep-gq9_cnPm.js:57557:19)
"seroval",
"seroval-plugins/web",
],
},
},
Expand Down
3 changes: 3 additions & 0 deletions examples/react-ssr/vite.config.workerd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default defineConfig((_env) => ({
entry: "/src/adapters/workerd.ts",
miniflare: {
log: new Log(),
compatibilityDate: "2024-01-01",
},
}),
vitePluginVirtualIndexHtml(),
Expand All @@ -33,6 +34,8 @@ export default defineConfig((_env) => ({
"react/jsx-runtime",
"react/jsx-dev-runtime",
"react-dom/server.edge",
"seroval",
"seroval-plugins/web",
],
},
},
Expand Down
4 changes: 3 additions & 1 deletion examples/workerd-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,17 @@ async function main() {
cmd = `return ${cmd}`;
}
// TODO: we can invalidate virtual entry after eval
const entrySource = `export default async function (env) { ${cmd} }`;
const entrySource = `export default async (env) => { ${cmd} }`;
const entry = "virtual:eval/" + encodeURI(entrySource);
await devEnv.api.eval({
entry,
data: null,
fn: async ({ mod, env }) => {
const result = await mod.default(env);
if (typeof result !== "undefined") {
console.log(result);
}
return null;
},
});
}
Expand Down
10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
"lint": "prettier -w --cache .",
"lint-check": "prettier -c --cache ."
},
"dependencies": {
"miniflare": "^3.20240404.0",
"seroval": "^1.0.5",
"seroval-plugins": "^1.0.5",
"vite": "6.0.0-alpha.1"
},
"devDependencies": {
"@hattip/adapter-node": "^0.0.44",
"@hiogawa/utils": "1.6.4-pre.1",
Expand All @@ -17,12 +23,10 @@
"@types/node": "^20.11.30",
"@vitejs/plugin-react": "^4.2.1",
"esbuild": "^0.20.2",
"miniflare": "^3.20240404.0",
"prettier": "^3.2.5",
"tsup": "^8.0.2",
"tsx": "^4.7.1",
"typescript": "^5.4.3",
"vite": "6.0.0-alpha.1"
"typescript": "^5.4.3"
},
"packageManager": "pnpm@8.15.5+sha256.4b4efa12490e5055d59b9b9fc9438b7d581a6b7af3b5675eb5c5f447cee1a589",
"volta": {
Expand Down
152 changes: 147 additions & 5 deletions packages/workerd/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ interface WorkerdEnvironmentOptions {
};
}

//
// traditional middleware plugin
//

export function vitePluginWorkerd(pluginOptions: WorkerdPluginOptions): Plugin {
return {
name: vitePluginWorkerd.name,
Expand All @@ -58,10 +62,51 @@ export function vitePluginWorkerd(pluginOptions: WorkerdPluginOptions): Plugin {
return;
}
const devEnv = server.environments["workerd"] as WorkerdDevEnvironment;

// implement dispatchFetch based on eval
const dispatchFetch = async (request: Request): Promise<Response> => {
const seroval = await import("seroval");
const serovalPlugins = await import("seroval-plugins/web");

// TODO: stream directly request/response body
const serovalRequest = await seroval.toJSONAsync(request, {
plugins: [await createSerovalRequestPlugin()],
});
const serovalResponse = await devEnv.api.eval({
entry,
data: { serovalRequest },
// cusotmSerialize: true,
fn: async ({ mod, data: { serovalRequest }, env, runner }) => {
const seroval =
await runner.import<typeof import("seroval")>("seroval");
const serovalPlugins = await runner.import<
typeof import("seroval-plugins/web")
>("seroval-plugins/web");

const request: Request = await seroval.fromJSON(serovalRequest, {
plugins: [serovalPlugins.RequestPlugin],
});
const response: Response = await mod.default.fetch(request, env);
const serovalResponse = await seroval.toJSONAsync(response, {
plugins: [serovalPlugins.ResponsePlugin],
});
return serovalResponse;
},
});
const response: Response = await seroval.fromJSON(serovalResponse, {
plugins: [serovalPlugins.ResponsePlugin],
});
return response;
};

const nodeMiddleware = createMiddleware(
(ctx) => devEnv.api.dispatchFetch(entry, ctx.request),
(ctx) => dispatchFetch(ctx.request),
{ alwaysCallNext: false },
);
// const nodeMiddleware = createMiddleware(
// (ctx) => devEnv.api.dispatchFetch(entry, ctx.request),
// { alwaysCallNext: false },
// );
return () => {
server.middlewares.use(nodeMiddleware);
};
Expand Down Expand Up @@ -101,8 +146,13 @@ export async function createWorkerdDevEnvironment(
const devEnv = server.environments["workerd"];
tinyassert(devEnv);
const args = await request.json();
const result = await devEnv.fetchModule(...(args as [any, any]));
return new MiniflareResponse(JSON.stringify(result));
try {
const result = await devEnv.fetchModule(...(args as [any, any]));
return new MiniflareResponse(JSON.stringify(result));
} catch (e) {
console.error(e);
throw e;
}
},
},
bindings: {
Expand Down Expand Up @@ -205,9 +255,12 @@ export async function createWorkerdDevEnvironment(
JSON.stringify({
entry: ctx.entry,
fnString: ctx.fn.toString(),
cusotmSerialize: ctx.cusotmSerialize,
} satisfies EvalMetadata),
);
const body = JSON.stringify(ctx.data ?? (null as any));
const body: any = ctx.cusotmSerialize
? ctx.data
: JSON.stringify(ctx.data as any);
const fetch_ = runnerObject.fetch as any as typeof fetch; // fix web/undici types
const response = await fetch_(ANY_URL + RUNNER_EVAL_PATH, {
method: "POST",
Expand All @@ -217,7 +270,9 @@ export async function createWorkerdDevEnvironment(
duplex: "half",
});
tinyassert(response.ok);
const result = await response.json();
const result = ctx.cusotmSerialize
? response.body
: await response.json();
return result as any;
},
};
Expand Down Expand Up @@ -273,3 +328,90 @@ function createSimpleHMRChannel(options: {
},
};
}

// copied from https://github.com/lxsmnsyc/seroval/blob/63003d77889b06b73bab0365d296bcdd029219fb/packages/plugins/web/request.ts#L40
// to strip off unsupported workerd Request properties

import type { SerovalNode } from "seroval";

async function createSerovalRequestPlugin() {
const seroval = await import("seroval");
const serovalPlugins = await import("seroval-plugins/web");

function createRequestOptions(
current: Request,
body: ArrayBuffer | ReadableStream | null,
): RequestInit {
return {
body,
// cache: current.cache,
// credentials: current.credentials,
headers: current.headers,
integrity: current.integrity,
keepalive: current.keepalive,
method: current.method,
// mode: current.mode,
redirect: current.redirect,
// referrer: current.referrer,
// referrerPolicy: current.referrerPolicy,
};
}

interface RequestNode {
url: SerovalNode;
options: SerovalNode;
}

const RequestPlugin = /* @__PURE__ */ seroval.createPlugin<
Request,
RequestNode
>({
tag: "seroval-plugins/web/Request",
extends: [
serovalPlugins.ReadableStreamPlugin,
serovalPlugins.HeadersPlugin,
],
test(value) {
if (typeof Request === "undefined") {
return false;
}
return value instanceof Request;
},
parse: {
async async(value, ctx) {
return {
url: await ctx.parse(value.url),
options: await ctx.parse(
createRequestOptions(
value,
value.body ? await value.clone().arrayBuffer() : null,
),
),
};
},
stream(value, ctx) {
return {
url: ctx.parse(value.url),
options: ctx.parse(createRequestOptions(value, value.clone().body)),
};
},
},
serialize(node, ctx) {
return (
"new Request(" +
ctx.serialize(node.url) +
"," +
ctx.serialize(node.options) +
")"
);
},
deserialize(node, ctx) {
return new Request(
ctx.deserialize(node.url) as string,
ctx.deserialize(node.options) as RequestInit,
);
},
});

return RequestPlugin;
}
Empty file.
6 changes: 4 additions & 2 deletions packages/workerd/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,20 @@ export type FetchMetadata = {

export type EvalFn<In = any, Out = any> = (ctx: {
mod: any;
data?: In;
data: In;
env: any;
runner: ModuleRunner;
}) => Promise<Out> | Out;

export type EvalApi = <In = any, Out = any>(request: {
entry: string;
data?: In;
fn: EvalFn<In, Out>;
data: In;
cusotmSerialize?: boolean;
}) => Promise<Awaited<Out>>;

export type EvalMetadata = {
entry: string;
fnString: string;
cusotmSerialize?: boolean;
};
4 changes: 2 additions & 2 deletions packages/workerd/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,13 @@ export class RunnerObject implements DurableObject {
request.headers.get("x-vite-eval")!,
) as EvalMetadata;
const mod = await this.#runner.import(meta.entry);
const data = await request.json();
const data = meta.cusotmSerialize ? request.body : await request.json();
const env = objectPickBy(this.#env, (_v, k) => !k.startsWith("__vite"));
const fn: EvalFn = this.#env.__viteUnsafeEval.eval(
`() => ${meta.fnString}`,
)();
const result = await fn({ mod, data, env, runner: this.#runner });
const body = JSON.stringify(result ?? null);
const body = meta.cusotmSerialize ? result : JSON.stringify(result);
return new Response(body);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/workerd/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ export default [
format: ["esm"],
platform: "node",
dts: true,
external: ["vite", "miniflare"],
external: ["seroval", "seroval-plugins"],
}),
];
Loading