hono-party
Version:
Use PartyServer with Hono
157 lines (154 loc) • 6.04 kB
JavaScript
import { env } from "hono/adapter";
import { createMiddleware } from "hono/factory";
import { env as env$1 } from "cloudflare:workers";
import "nanoid";
//#region ../partyserver/dist/index.js
if (!("OPEN" in WebSocket)) {
const WebSocketStatus = {
CONNECTING: WebSocket.READY_STATE_CONNECTING,
OPEN: WebSocket.READY_STATE_OPEN,
CLOSING: WebSocket.READY_STATE_CLOSING,
CLOSED: WebSocket.READY_STATE_CLOSED
};
Object.assign(WebSocket, WebSocketStatus);
Object.assign(WebSocket.prototype, WebSocketStatus);
}
/**
* Cache websocket attachments to avoid having to rehydrate them on every property access.
*/
var AttachmentCache = class {
#cache = /* @__PURE__ */ new WeakMap();
get(ws) {
let attachment = this.#cache.get(ws);
if (!attachment) {
attachment = WebSocket.prototype.deserializeAttachment.call(ws);
if (attachment !== void 0) this.#cache.set(ws, attachment);
else throw new Error("Missing websocket attachment. This is most likely an issue in PartyServer, please open an issue at https://github.com/cloudflare/partykit/issues");
}
return attachment;
}
set(ws, attachment) {
this.#cache.set(ws, attachment);
WebSocket.prototype.serializeAttachment.call(ws, attachment);
}
};
const attachments = new AttachmentCache();
const serverMapCache = /* @__PURE__ */ new WeakMap();
function camelCaseToKebabCase(str) {
if (str === str.toUpperCase() && str !== str.toLowerCase()) return str.toLowerCase().replace(/_/g, "-");
let kebabified = str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
kebabified = kebabified.startsWith("-") ? kebabified.slice(1) : kebabified;
return kebabified.replace(/_/g, "-").replace(/-$/, "");
}
/**
* A utility function for PartyKit style routing.
*/
async function routePartykitRequest(req, env$1$1 = env$1, options) {
if (!serverMapCache.has(env$1$1)) serverMapCache.set(env$1$1, Object.entries(env$1$1).reduce((acc, [k, v]) => {
if (v && typeof v === "object" && "idFromName" in v && typeof v.idFromName === "function") {
Object.assign(acc, { [camelCaseToKebabCase(k)]: v });
return acc;
}
return acc;
}, {}));
const map = serverMapCache.get(env$1$1);
const prefixParts = (options?.prefix || "parties").split("/");
const parts = new URL(req.url).pathname.split("/").filter(Boolean);
if (!prefixParts.every((part, index) => parts[index] === part) || parts.length < prefixParts.length + 2) return null;
const namespace = parts[prefixParts.length];
const name = parts[prefixParts.length + 1];
if (name && namespace) {
if (!map[namespace]) {
if (namespace === "main") {
console.warn("You appear to be migrating a PartyKit project to PartyServer.");
console.warn(`PartyServer doesn't have a "main" party by default. Try adding this to your PartySocket client:\n
party: "${camelCaseToKebabCase(Object.keys(map)[0])}"`);
} else console.error(`The url ${req.url} with namespace "${namespace}" and name "${name}" does not match any server namespace.
Did you forget to add a durable object binding to the class ${namespace[0].toUpperCase() + namespace.slice(1)} in your wrangler.jsonc?`);
return new Response("Invalid request", { status: 400 });
}
let doNamespace = map[namespace];
if (options?.jurisdiction) doNamespace = doNamespace.jurisdiction(options.jurisdiction);
const id = doNamespace.idFromName(name);
const stub = doNamespace.get(id, options);
req = new Request(req);
req.headers.set("x-partykit-room", name);
req.headers.set("x-partykit-namespace", namespace);
if (options?.jurisdiction) req.headers.set("x-partykit-jurisdiction", options.jurisdiction);
if (options?.props) req.headers.set("x-partykit-props", JSON.stringify(options?.props));
if (req.headers.get("Upgrade")?.toLowerCase() === "websocket") {
if (options?.onBeforeConnect) {
const reqOrRes = await options.onBeforeConnect(req, {
party: namespace,
name
});
if (reqOrRes instanceof Request) req = reqOrRes;
else if (reqOrRes instanceof Response) return reqOrRes;
}
} else if (options?.onBeforeRequest) {
const reqOrRes = await options.onBeforeRequest(req, {
party: namespace,
name
});
if (reqOrRes instanceof Request) req = reqOrRes;
else if (reqOrRes instanceof Response) return reqOrRes;
}
return stub.fetch(req);
} else return null;
}
//#endregion
//#region src/index.ts
/**
* Creates a middleware for handling PartyServer WebSocket and HTTP requests
* Processes both WebSocket upgrades and standard HTTP requests, delegating them to PartyServer
*/
function partyserverMiddleware(ctx) {
return createMiddleware(async (c, next) => {
try {
const response = await (isWebSocketUpgrade(c) ? handleWebSocketUpgrade : handleHttpRequest)(c, ctx?.options);
return response === null ? await next() : response;
} catch (error) {
if (ctx?.onError) {
ctx.onError(error);
return next();
}
throw error;
}
});
}
/**
* Checks if the incoming request is a WebSocket upgrade request
* Looks for the 'upgrade' header with a value of 'websocket' (case-insensitive)
*/
function isWebSocketUpgrade(c) {
return c.req.header("upgrade")?.toLowerCase() === "websocket";
}
/**
* Creates a new Request object from the Hono context
* Preserves the original request's URL, method, headers, and body
*/
function createRequestFromContext(c) {
return c.req.raw.clone();
}
/**
* Handles WebSocket upgrade requests
* Returns a WebSocket upgrade response if successful, null otherwise
*/
async function handleWebSocketUpgrade(c, options) {
const response = await routePartykitRequest(createRequestFromContext(c), env(c), options);
if (!response?.webSocket) return null;
return new Response(null, {
status: 101,
webSocket: response.webSocket
});
}
/**
* Handles standard HTTP requests
* Forwards the request to PartyServer and returns the response
*/
async function handleHttpRequest(c, options) {
return routePartykitRequest(createRequestFromContext(c), env(c), options);
}
//#endregion
export { partyserverMiddleware };
//# sourceMappingURL=index.js.map