rwsdk
Version:
Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime
206 lines (205 loc) • 8.14 kB
JavaScript
// context(justinvdm, 14 Aug 2025): `react-server-dom-webpack` uses this globa ___webpack_require__ global,
// so we need to import our client entry point (which sets it), before importing
// prettier-ignore
import { initClient } from "../../client/client";
// prettier-ignore
import { createFromReadableStream } from "react-server-dom-webpack/client.browser";
// prettier-ignore
import { MESSAGE_TYPE } from "./shared";
// prettier-ignore
import { packMessage, unpackMessage, } from "./protocol";
const DEFAULT_KEY = "default";
export const initRealtimeClient = ({ key = DEFAULT_KEY, handleResponse, } = {}) => {
const transport = realtimeTransport({ key, handleResponse });
return initClient({ transport, handleResponse });
};
export const realtimeTransport = ({ key = DEFAULT_KEY, handleResponse, }) => (transportContext) => {
let ws = null;
let isConnected = false;
const clientId = crypto.randomUUID();
const clientUrl = new URL(window.location.href);
const isHttps = clientUrl.protocol === "https:";
clientUrl.protocol = "";
clientUrl.host = "";
const setupWebSocket = async () => {
if (ws)
return;
const protocol = isHttps ? "wss" : "ws";
ws = new WebSocket(`${protocol}://${window.location.host}/__realtime?` +
`key=${encodeURIComponent(key)}&` +
`url=${encodeURIComponent(clientUrl.toString())}&` +
`clientId=${encodeURIComponent(clientId)}&` +
`shouldForwardResponses=${encodeURIComponent(handleResponse ? "true" : "false")}`);
ws.binaryType = "arraybuffer";
ws.addEventListener("open", () => {
isConnected = true;
});
ws.addEventListener("error", (event) => {
console.error("[Realtime] WebSocket error", event);
});
ws.addEventListener("close", () => {
console.warn("[Realtime] WebSocket closed, attempting to reconnect...");
ws = null;
isConnected = false;
setTimeout(setupWebSocket, 5000);
});
listenForUpdates(ws, (response) => {
processResponse(response);
});
};
const ensureWs = () => {
if (!ws && isConnected) {
throw new Error("Inconsistent state: WebSocket is null but marked as connected");
}
if (!ws || !isConnected) {
throw new Error("WebSocket is not connected");
}
return ws;
};
const realtimeCallServer = async (id, args) => {
try {
const socket = ensureWs();
const { encodeReply } = await import("react-server-dom-webpack/client.browser");
// Note(peterp, 2025-07-02): We need to send the "current URL" per message,
// in case the user has enabled client side navigation.
const clientUrl = new URL(window.location.href);
clientUrl.protocol = "";
clientUrl.host = "";
const encodedArgs = args != null ? await encodeReply(args) : null;
const requestId = crypto.randomUUID();
const message = packMessage({
type: MESSAGE_TYPE.ACTION_REQUEST,
id,
args: encodedArgs,
requestId,
clientUrl: clientUrl.toString(),
});
const promisedResponse = respondToRequest(requestId, socket);
socket.send(message);
return await processResponse(await promisedResponse);
}
catch (e) {
console.error("[Realtime] Error calling server", e);
return null;
}
};
const processResponse = async (response) => {
try {
let streamForRsc;
let shouldContinue = true;
if (transportContext.handleResponse) {
const [stream1, stream2] = response.body.tee();
const clonedResponse = new Response(stream1, response);
streamForRsc = stream2;
shouldContinue = transportContext.handleResponse(clonedResponse);
}
else {
streamForRsc = response.body;
}
if (!shouldContinue) {
return null;
}
const rscPayload = createFromReadableStream(streamForRsc, {
callServer: realtimeCallServer,
});
transportContext.setRscPayload(rscPayload);
return (await rscPayload).actionResult;
}
catch (err) {
throw err;
}
};
setupWebSocket();
return realtimeCallServer;
};
function respondToRequest(requestId, socket) {
const messageTypes = {
start: MESSAGE_TYPE.ACTION_START,
chunk: MESSAGE_TYPE.ACTION_CHUNK,
end: MESSAGE_TYPE.ACTION_END,
error: MESSAGE_TYPE.ACTION_ERROR,
};
return new Promise((resolve, reject) => {
const handler = (event) => {
const unpacked = unpackMessage(new Uint8Array(event.data));
if (unpacked.type === MESSAGE_TYPE.ACTION_REQUEST) {
return;
}
if (unpacked.id !== requestId) {
return;
}
if (unpacked.type === messageTypes.start) {
const message = unpacked;
socket.removeEventListener("message", handler);
const stream = createUpdateStreamFromSocket(requestId, socket, messageTypes, reject);
const response = new Response(stream, {
status: message.status,
headers: { "Content-Type": "text/plain" },
});
resolve(response);
}
};
socket.addEventListener("message", handler);
});
}
function listenForUpdates(socket, onUpdate) {
const messageTypes = {
start: MESSAGE_TYPE.RSC_START,
chunk: MESSAGE_TYPE.RSC_CHUNK,
end: MESSAGE_TYPE.RSC_END,
};
const handler = async (event) => {
const unpacked = unpackMessage(new Uint8Array(event.data));
if (unpacked.type === MESSAGE_TYPE.ACTION_REQUEST ||
unpacked.type === MESSAGE_TYPE.ACTION_CHUNK ||
unpacked.type === MESSAGE_TYPE.ACTION_END ||
unpacked.type === MESSAGE_TYPE.ACTION_ERROR) {
return;
}
if (unpacked.type === messageTypes.start) {
const message = unpacked;
const stream = createUpdateStreamFromSocket(message.id, socket, messageTypes, (error) => {
console.error("[Realtime] Error creating update stream", error);
});
const response = new Response(stream, {
status: message.status,
headers: { "Content-Type": "text/plain" },
});
onUpdate(response);
}
};
socket.addEventListener("message", handler);
}
const createUpdateStreamFromSocket = (id, socket, messageTypes, onError) => {
let deferredStreamController = Promise.withResolvers();
const stream = new ReadableStream({
start(controller) {
deferredStreamController.resolve(controller);
},
});
const handler = async (event) => {
const unpacked = unpackMessage(new Uint8Array(event.data));
if (unpacked.type === MESSAGE_TYPE.ACTION_REQUEST) {
return;
}
if (unpacked.id !== id) {
return;
}
const streamController = await deferredStreamController.promise;
if (unpacked.type === messageTypes.chunk) {
const message = unpacked;
streamController.enqueue(message.payload);
}
else if (unpacked.type === messageTypes.end) {
streamController.close();
socket.removeEventListener("message", handler);
}
else if (messageTypes.error && unpacked.type === messageTypes.error) {
const message = unpacked;
onError(new Error(message.error));
socket.removeEventListener("message", handler);
}
};
socket.addEventListener("message", handler);
return stream;
};