vite-plugin-react-server
Version:
Vite plugin for React Server Components (RSC)
175 lines (172 loc) • 4.71 kB
text/typescript
import type { ServerStreamHandlers } from "../types.js";
import { sendMessage } from "../sendMessage.js";
import { serializeError } from "../../error/serializeError.js";
import { serializeErrorInfo } from "../../error/serializeErrorInfo.js";
import { MessagePortWritable } from "../../stream/MessagePortWritable.js";
import type { MessagePort } from "node:worker_threads";
/**
* Creates handlers for two-port communication: fromWorker for streaming data out, toWorker for control messages in
* Following zero-copy streaming pattern: fromWorker (worker → main), toWorker (main → worker)
*/
export function createHandlers(fromWorker?: MessagePort, toWorker?: MessagePort): ServerStreamHandlers {
// Create writable stream for fromWorker if available
const messagePortWritable = fromWorker ? new MessagePortWritable(fromWorker, toWorker) : null;
return {
onRscRender: (id) => {
if (toWorker) {
toWorker.postMessage({
type: "RSC_RENDER_START",
id: id,
});
} else {
sendMessage({
type: "RSC_RENDER_START",
id: id,
});
}
},
onError: (id, error, errorInfo) => {
if (toWorker) {
toWorker.postMessage({
type: "ERROR",
id: id,
errorInfo: serializeErrorInfo(errorInfo),
error: serializeError(error),
});
} else {
sendMessage({
type: "ERROR",
id: id,
errorInfo: serializeErrorInfo(errorInfo),
error: serializeError(error),
});
}
},
onShellError: (id, error) => {
if (toWorker) {
toWorker.postMessage({
type: "SHELL_ERROR",
id: id,
error: serializeError(error),
});
} else {
sendMessage({
type: "SHELL_ERROR",
id: id,
error: serializeError(error),
});
}
},
onData: (id, data) => {
// In two-port mode, data goes through the writable stream
// In single-port mode, use the old message approach
if (messagePortWritable) {
// Data is handled by piping to messagePortWritable
// This method is called but the actual data flow is through the stream
} else {
sendMessage({
type: "RSC_CHUNK",
id: id,
chunk: data,
});
}
},
onEnd: (id) => {
// Mirror HTML worker pattern: send null through fromWorker, then END through toWorker
if (fromWorker) {
try {
fromWorker.postMessage(null);
} catch (error) {
// Port may be closed, ignore
}
}
if (toWorker) {
toWorker.postMessage({
type: "RSC_END",
id: id,
});
} else {
sendMessage({
type: "RSC_END",
id: id,
});
}
},
onMetrics: (id, metrics) => {
if (metrics.type === "html" || metrics.type === "worker-startup" || metrics.type === "module-resolution") {
return;
}
if (toWorker) {
toWorker.postMessage({
type: "RSC_METRICS",
id: id,
metrics: metrics as any,
});
} else {
sendMessage({
type: "RSC_METRICS",
id: id,
metrics: metrics as any,
});
}
},
// Expose the writable stream for direct piping in two-port mode
...(messagePortWritable && { getWritable: () => messagePortWritable }),
onHmrAccept: (id, routes) => {
sendMessage({
type: "HMR_ACCEPT",
id: id,
routes: routes,
});
},
onHmrUpdate: (id, routes) => {
sendMessage({
type: "HMR_UPDATE",
id: id,
routes: routes,
});
},
onShutdown: (id) => {
sendMessage({
type: "HMR_CLEANUP",
id: id,
});
},
onCssFile: (id, code) => {
sendMessage({
type: "CSS_FILE",
id: id,
content: code,
});
},
onCleanup: (id) => {
sendMessage({
type: "HMR_CLEANUP",
id: id,
});
},
onShellReady: (id) => {
sendMessage({
type: "SHELL_READY",
id: id,
});
},
onAllReady: (id) => {
sendMessage({
type: "RSC_END",
id: id,
});
},
onServerActionResponse: (id, result, error) => {
sendMessage({
type: "SERVER_ACTION_RESPONSE",
id: id,
result: result,
// Only include error if it's truthy - prevents serializeError(undefined)
...(error ? { error } : {}),
});
},
};
}
// Default handlers for backward compatibility
export const handlers = createHandlers();