UNPKG

partyserver

Version:

Build real-time applications powered by [Durable Objects](https://developers.cloudflare.com/durable-objects/), inspired by [PartyKit](https://www.partykit.io/).

858 lines (857 loc) 32.4 kB
import { DurableObject, env } from "cloudflare:workers"; import { nanoid } from "nanoid"; //#region src/connection.ts 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); } function tryGetPartyServerMeta(ws) { try { const attachment = WebSocket.prototype.deserializeAttachment.call(ws); if (!attachment || typeof attachment !== "object") return null; if (!("__pk" in attachment)) return null; const pk = attachment.__pk; if (!pk || typeof pk !== "object") return null; const { id, tags } = pk; if (typeof id !== "string") return null; const { uri } = pk; return { id, tags: Array.isArray(tags) ? tags : [], uri: typeof uri === "string" ? uri : void 0 }; } catch { return null; } } function isPartyServerWebSocket(ws) { return tryGetPartyServerMeta(ws) !== null; } /** * 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 connections = /* @__PURE__ */ new WeakSet(); const isWrapped = (ws) => { return connections.has(ws); }; /** * Wraps a WebSocket with Connection fields that rehydrate the * socket attachments lazily only when requested. */ const createLazyConnection = (ws) => { if (isWrapped(ws)) return ws; let initialState; if ("state" in ws) { initialState = ws.state; delete ws.state; } const connection = Object.defineProperties(ws, { id: { configurable: true, get() { return attachments.get(ws).__pk.id; } }, uri: { configurable: true, get() { return attachments.get(ws).__pk.uri ?? null; } }, tags: { configurable: true, get() { return attachments.get(ws).__pk.tags ?? []; } }, socket: { configurable: true, get() { return ws; } }, state: { configurable: true, get() { return ws.deserializeAttachment(); } }, setState: { configurable: true, value: function setState(setState) { let state; if (setState instanceof Function) state = setState(this.state); else state = setState; ws.serializeAttachment(state); return state; } }, deserializeAttachment: { configurable: true, value: function deserializeAttachment() { return attachments.get(ws).__user ?? null; } }, serializeAttachment: { configurable: true, value: function serializeAttachment(attachment) { const setting = { ...attachments.get(ws), __user: attachment ?? null }; attachments.set(ws, setting); } } }); if (initialState) connection.setState(initialState); connections.add(connection); return connection; }; var HibernatingConnectionIterator = class { index = 0; sockets; constructor(state, tag) { this.state = state; this.tag = tag; } [Symbol.iterator]() { return this; } next() { const sockets = this.sockets ?? (this.sockets = this.state.getWebSockets(this.tag)); let socket; while (socket = sockets[this.index++]) if (socket.readyState === WebSocket.READY_STATE_OPEN) { if (!isPartyServerWebSocket(socket)) continue; return { done: false, value: createLazyConnection(socket) }; } return { done: true, value: void 0 }; } }; /** * Deduplicate and validate connection tags. * Returns the final tag array (always includes the connection id as the first tag). */ function prepareTags(connectionId, userTags) { const tags = [connectionId, ...userTags.filter((t) => t !== connectionId)]; if (tags.length > 10) throw new Error("A connection can only have 10 tags, including the default id tag."); for (const tag of tags) { if (typeof tag !== "string") throw new Error(`A connection tag must be a string. Received: ${tag}`); if (tag === "") throw new Error("A connection tag must not be an empty string."); if (tag.length > 256) throw new Error("A connection tag must not exceed 256 characters"); } return tags; } /** * When not using hibernation, we track active connections manually. */ var InMemoryConnectionManager = class { #connections = /* @__PURE__ */ new Map(); tags = /* @__PURE__ */ new WeakMap(); getCount() { return this.#connections.size; } getConnection(id) { return this.#connections.get(id); } *getConnections(tag) { if (!tag) { yield* this.#connections.values().filter((c) => c.readyState === WebSocket.READY_STATE_OPEN); return; } for (const connection of this.#connections.values()) if ((this.tags.get(connection) ?? []).includes(tag)) yield connection; } accept(connection, options) { connection.accept(); const tags = prepareTags(connection.id, options.tags); this.#connections.set(connection.id, connection); this.tags.set(connection, tags); Object.defineProperty(connection, "tags", { get: () => tags, configurable: true }); const removeConnection = () => { this.#connections.delete(connection.id); connection.removeEventListener("close", removeConnection); connection.removeEventListener("error", removeConnection); }; connection.addEventListener("close", removeConnection); connection.addEventListener("error", removeConnection); return connection; } }; /** * When opting into hibernation, the platform tracks connections for us. */ var HibernatingConnectionManager = class { constructor(controller) { this.controller = controller; } getCount() { let count = 0; for (const ws of this.controller.getWebSockets()) if (isPartyServerWebSocket(ws)) count++; return count; } getConnection(id) { const matching = this.controller.getWebSockets(id).filter((ws) => { return tryGetPartyServerMeta(ws)?.id === id; }); if (matching.length === 0) return void 0; if (matching.length === 1) return createLazyConnection(matching[0]); throw new Error(`More than one connection found for id ${id}. Did you mean to use getConnections(tag) instead?`); } getConnections(tag) { return new HibernatingConnectionIterator(this.controller, tag); } accept(connection, options) { const tags = prepareTags(connection.id, options.tags); this.controller.acceptWebSocket(connection, tags); connection.serializeAttachment({ __pk: { id: connection.id, tags, uri: connection.uri ?? void 0 }, __user: null }); return createLazyConnection(connection); } }; //#endregion //#region src/index.ts const NAME_STORAGE_KEY = "__ps_name"; /** * Reserved WebSocket close codes the runtime synthesizes when there * was no real Close frame from the peer: * - 1005 (NoStatusReceived) — peer's frame had no status code. * - 1006 (AbnormalClosure) — peer dropped the underlying transport * without sending a Close frame at all. * - 1015 (TLSHandshake) — TLS failure during connection setup. * * These cannot legally appear in an outgoing Close frame, and — more * importantly for our reciprocation path — there is no peer left to * receive a reciprocating Close frame. Trying to send one anyway can * succeed synchronously but fail asynchronously inside the runtime * with "WebSocket peer disconnected" / "Network connection lost", * which escapes a synchronous try/catch and surfaces as an unhandled * promise rejection. */ function isReservedCloseCode(code) { return code === 1005 || code === 1006 || code === 1015; } /** * Reciprocate a peer-initiated Close frame to complete the handshake. * * Best-effort: swallows synchronous errors from invalid codes, * oversize reasons, or sockets that have already been closed by user * code. Skips the reciprocation entirely when the peer didn't * actually send a Close frame (reserved codes 1005/1006/1015) — in * those cases the underlying transport is already gone and writing * to it would fail asynchronously, which we can't catch here. * * Used by both the hibernating and non-hibernating close handlers to * ensure the close handshake always completes when there is one to * complete. */ function closeQuietly(ws, code, reason) { if (isReservedCloseCode(code)) return; try { ws.close(code, reason); } catch {} } const serverMapCache = /* @__PURE__ */ new WeakMap(); const bindingNameCache = /* @__PURE__ */ new WeakMap(); const DEFAULT_ROUTING_RETRY_OPTIONS = { maxAttempts: 3, baseDelayMs: 100, maxDelayMs: 800 }; function durableObjectGetOptions(options) { return options?.locationHint ? { locationHint: options.locationHint } : void 0; } function validatePositiveInteger(value, name) { if (!Number.isFinite(value) || value < 1) throw new Error(`${name} must be >= 1`); if (!Number.isInteger(value)) throw new Error(`${name} must be an integer`); } function validatePositiveNumber(value, name) { if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be > 0`); } function resolveRoutingRetryOptions(options) { if (options === false) return null; const resolved = { maxAttempts: options?.maxAttempts ?? DEFAULT_ROUTING_RETRY_OPTIONS.maxAttempts, baseDelayMs: options?.baseDelayMs ?? DEFAULT_ROUTING_RETRY_OPTIONS.baseDelayMs, maxDelayMs: options?.maxDelayMs ?? DEFAULT_ROUTING_RETRY_OPTIONS.maxDelayMs, onRetry: options?.onRetry }; validatePositiveInteger(resolved.maxAttempts, "routingRetry.maxAttempts"); validatePositiveNumber(resolved.baseDelayMs, "routingRetry.baseDelayMs"); validatePositiveNumber(resolved.maxDelayMs, "routingRetry.maxDelayMs"); if (resolved.baseDelayMs > resolved.maxDelayMs) throw new Error("routingRetry.baseDelayMs must be <= maxDelayMs"); return resolved; } function isRetryableDurableObjectError(error) { if (typeof error !== "object" || error === null) return false; const typed = error; return typed.retryable === true && typed.overloaded !== true; } function routingRetryDelayMs(attempt, options) { const upperBoundMs = Math.min(options.maxDelayMs, options.baseDelayMs * 2 ** (attempt - 1)); return Math.floor(Math.random() * upperBoundMs); } async function retryDurableObjectOperation(operation, context, retryOptions) { const resolved = resolveRoutingRetryOptions(retryOptions); if (!resolved) return await operation(); let attempt = 1; while (true) try { return await operation(); } catch (error) { const nextAttempt = attempt + 1; if (nextAttempt > resolved.maxAttempts || !isRetryableDurableObjectError(error)) throw error; const delayMs = routingRetryDelayMs(attempt, resolved); try { await resolved.onRetry?.({ error, attempt, maxAttempts: resolved.maxAttempts, delayMs, name: context.name, className: context.className }); } catch (callbackError) { console.warn("PartyServer routingRetry onRetry callback failed:", callbackError); } await new Promise((resolve) => setTimeout(resolve, delayMs)); attempt = nextAttempt; } } /** * For a given server namespace, create a server with a name. * * Makes an RPC that awaits the DO's `onStart()` before returning, so callers * can invoke user-defined RPC methods on the returned stub and trust that * `onStart()` has completed. (User-defined RPC methods don't * otherwise pass through `Server.fetch()`, which is where initialization * would normally be triggered.) * * `this.name` inside the DO is always populated from `ctx.id.name`, so * the RPC no longer needs to carry the name for bookkeeping; it exists * purely to synchronize `onStart()` and to deliver `props`. */ async function getServerByName(serverNamespace, name, options) { if (options?.jurisdiction) serverNamespace = serverNamespace.jurisdiction(options.jurisdiction); const id = serverNamespace.idFromName(name); const getOptions = durableObjectGetOptions(options); await retryDurableObjectOperation(() => serverNamespace.get(id, getOptions).setName(name, options?.props), { name }, options?.routingRetry); return serverNamespace.get(id, getOptions); } 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(/-$/, ""); } /** * Resolve CORS options into a concrete headers object (or null if CORS is disabled). */ function resolveCorsHeaders(cors) { if (cors === true) return { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS", "Access-Control-Allow-Headers": "*", "Access-Control-Max-Age": "86400" }; if (cors && typeof cors === "object") { const h = new Headers(cors); const record = {}; h.forEach((value, key) => { record[key] = value; }); return record; } return null; } async function routePartykitRequest(req, env$1 = env, options) { if (!serverMapCache.has(env$1)) { const namespaceMap = {}; const bindingNames = {}; for (const [k, v] of Object.entries(env$1)) if (v && typeof v === "object" && "idFromName" in v && typeof v.idFromName === "function") { const kebab = camelCaseToKebabCase(k); namespaceMap[kebab] = v; bindingNames[kebab] = k; } serverMapCache.set(env$1, namespaceMap); bindingNameCache.set(env$1, bindingNames); } const map = serverMapCache.get(env$1); const bindingNames = bindingNameCache.get(env$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 }); } const corsHeaders = resolveCorsHeaders(options?.cors); const isWebSocket = req.headers.get("Upgrade")?.toLowerCase() === "websocket"; function withCorsHeaders(response) { if (!corsHeaders || isWebSocket) return response; const newResponse = new Response(response.body, response); for (const [key, value] of Object.entries(corsHeaders)) newResponse.headers.set(key, value); return newResponse; } if (req.method === "OPTIONS" && corsHeaders) return new Response(null, { headers: corsHeaders }); let doNamespace = map[namespace]; if (options?.jurisdiction) doNamespace = doNamespace.jurisdiction(options.jurisdiction); const id = doNamespace.idFromName(name); const getOptions = durableObjectGetOptions(options); req = new Request(req); req.headers.set("x-partykit-namespace", namespace); if (options?.jurisdiction) req.headers.set("x-partykit-jurisdiction", options.jurisdiction); const className = bindingNames[namespace]; let partyDeprecationWarned = false; const lobby = { get party() { if (!partyDeprecationWarned) { partyDeprecationWarned = true; console.warn("lobby.party is deprecated and currently returns the kebab-case namespace (e.g. \"my-agent\"). Use lobby.className instead to get the Durable Object class name (e.g. \"MyAgent\"). In the next major version, lobby.party will return the class name."); } return namespace; }, className, name }; if (isWebSocket) { if (options?.onBeforeConnect) { const reqOrRes = await options.onBeforeConnect(req, lobby); if (reqOrRes instanceof Request) req = reqOrRes; else if (reqOrRes instanceof Response) return reqOrRes; } } else if (options?.onBeforeRequest) { const reqOrRes = await options.onBeforeRequest(req, lobby); if (reqOrRes instanceof Request) req = reqOrRes; else if (reqOrRes instanceof Response) return withCorsHeaders(reqOrRes); } if (options?.props !== void 0) req.headers.set("x-partykit-props", JSON.stringify(options.props)); const response = await retryDurableObjectOperation(() => doNamespace.get(id, getOptions).fetch(req.clone()), { name, className }, options?.routingRetry); return isWebSocket ? response : withCorsHeaders(response); } else return null; } var Server = class extends DurableObject { static options = { hibernate: false }; #status = "zero"; #ParentClass = Object.getPrototypeOf(this).constructor; #connectionManager = this.#ParentClass.options.hibernate ? new HibernatingConnectionManager(this.ctx) : new InMemoryConnectionManager(); /** * Execute SQL queries against the Server's database * @template T Type of the returned rows * @param strings SQL query template strings * @param values Values to be inserted into the query * @returns Array of query results */ sql(strings, ...values) { let query = ""; try { query = strings.reduce((acc, str, i) => acc + str + (i < values.length ? "?" : ""), ""); return [...this.ctx.storage.sql.exec(query, ...values)]; } catch (e) { console.error(`failed to execute sql query: ${query}`, e); throw this.onException(e); } } constructor(ctx, env) { super(ctx, env); } /** * Handle incoming requests to the server. */ async fetch(request) { try { const props = request.headers.get("x-partykit-props"); if (props) this.#_props = JSON.parse(props); if (!this.ctx.id.name && !this.#_name) { const room = request.headers.get("x-partykit-room"); if (room) this.#_name = room; } await this.#ensureInitialized(); if (!this.ctx.id.name && !this.#_name) throw new Error(`Cannot determine the name for ${this.#ParentClass.name}: this.ctx.id.name is undefined, no legacy __ps_name storage record is present, and no x-partykit-room header was supplied. Likely causes: 1. The stub was built via idFromString()/newUniqueId(). PartyServer requires name-based addressing (idFromName/getByName). 2. The workerd/wrangler runtime is too old to expose ctx.id.name — update to a recent wrangler release. 3. You called stub.fetch() directly without going through routePartykitRequest()/getServerByName(). Prefer those, or set the x-partykit-room header.`); const url = new URL(request.url); if (request.headers.get("Upgrade")?.toLowerCase() !== "websocket") return await this.onRequest(request); else { const { 0: clientWebSocket, 1: serverWebSocket } = new WebSocketPair(); let connectionId = url.searchParams.get("_pk"); if (!connectionId) connectionId = nanoid(); let connection = Object.assign(serverWebSocket, { id: connectionId, uri: request.url, server: this.name, tags: [], state: null, setState(setState) { let state; if (setState instanceof Function) state = setState(this.state); else state = setState; this.state = state; return this.state; } }); const ctx = { request }; const tags = await this.getConnectionTags(connection, ctx); connection = this.#connectionManager.accept(connection, { tags }); if (!this.#ParentClass.options.hibernate) this.#attachSocketEventHandlers(connection); await this.onConnect(connection, ctx); return new Response(null, { status: 101, webSocket: clientWebSocket }); } } catch (err) { console.error(`Error in ${this.#ParentClass.name}:${this.ctx.id.name ?? this.#_name ?? "<unnamed>"} fetch:`, err); if (!(err instanceof Error)) throw err; if (request.headers.get("Upgrade") === "websocket") { const pair = new WebSocketPair(); pair[1].accept(); pair[1].send(JSON.stringify({ error: err.stack })); pair[1].close(1011, "Uncaught exception during session setup"); return new Response(null, { status: 101, webSocket: pair[0] }); } else return new Response(err.stack, { status: 500 }); } } async webSocketMessage(ws, message) { if (!isPartyServerWebSocket(ws)) return; try { const connection = createLazyConnection(ws); await this.#ensureInitialized(); connection.server = this.name; return this.onMessage(connection, message); } catch (e) { console.error(`Error in ${this.#ParentClass.name}:${this.ctx.id.name ?? this.#_name ?? "<unnamed>"} webSocketMessage:`, e); } } async webSocketClose(ws, code, reason, wasClean) { if (!isPartyServerWebSocket(ws)) return; try { const connection = createLazyConnection(ws); await this.#ensureInitialized(); connection.server = this.name; await this.onClose(connection, code, reason, wasClean); } catch (e) { console.error(`Error in ${this.#ParentClass.name}:${this.ctx.id.name ?? this.#_name ?? "<unnamed>"} webSocketClose:`, e); } finally { closeQuietly(ws, code, reason); } } async webSocketError(ws, error) { if (!isPartyServerWebSocket(ws)) return; try { const connection = createLazyConnection(ws); await this.#ensureInitialized(); connection.server = this.name; return this.onError(connection, error); } catch (e) { console.error(`Error in ${this.#ParentClass.name}:${this.ctx.id.name ?? this.#_name ?? "<unnamed>"} webSocketError:`, e); } } /** * Read the legacy `__ps_name` storage record as a fallback source of * `this.name` when `ctx.id.name` is unavailable. Covers: * * 1. Alarm handlers firing on alarm records that were scheduled by * a workerd version that did not yet persist `name` into the * alarm record (see the Durable Objects ID docs: * https://developers.cloudflare.com/durable-objects/api/id/#name). * The runtime contract for current workerd populates `ctx.id.name` * in alarm handlers — see the "Raw runtime contract" tests — so * this fallback exists primarily for stale on-disk alarm records * and for defense-in-depth against future runtime changes. * 2. Legacy framework-level bootstrap patterns that write * `__ps_name` directly (or call `setName()`) before triggering * `__unsafe_ensureInitialized()` — typically DOs addressed via * `idFromString()` / `newUniqueId()` plus a name override. */ async #hydrateNameFromLegacyStorage() { if (this.#_name) return; const stored = await this.ctx.storage.get(NAME_STORAGE_KEY); if (stored) this.#_name = stored; } async #persistNameFallbackFromCtxId() { const ctxName = this.ctx.id.name; if (ctxName === void 0 || this.#_name) return; if (await this.ctx.storage.get(NAME_STORAGE_KEY) !== ctxName) await this.ctx.storage.put(NAME_STORAGE_KEY, ctxName); this.#_name = ctxName; } /** * @internal — Do not use directly. This is an escape hatch for frameworks * (like Agents) that receive calls via native DO RPC, bypassing the * standard fetch/alarm/webSocket entry points where initialization * normally happens. Calling this from application code is unsupported * and may break without notice. */ async __unsafe_ensureInitialized() { await this.#ensureInitialized(); } async #ensureInitialized() { if (this.#status === "started") return; if (this.ctx.id.name !== void 0) await this.#persistNameFallbackFromCtxId(); else if (!this.#_name) await this.#hydrateNameFromLegacyStorage(); let error; await this.ctx.blockConcurrencyWhile(async () => { this.#status = "starting"; try { await this.onStart(this.#_props); this.#status = "started"; } catch (e) { this.#status = "zero"; error = e; } }); if (error) throw error; } #attachSocketEventHandlers(connection) { const handleMessageFromClient = (event) => { this.onMessage(connection, event.data)?.catch((e) => { console.error("onMessage error:", e); }); }; const reciprocateClose = (event) => { closeQuietly(connection, event.code, event.reason); }; const handleCloseFromClient = (event) => { connection.removeEventListener("message", handleMessageFromClient); connection.removeEventListener("close", handleCloseFromClient); let result; try { result = this.onClose(connection, event.code, event.reason, event.wasClean); } catch (e) { console.error("onClose error:", e); reciprocateClose(event); return; } if (result && typeof result.then === "function") result.catch((e) => { console.error("onClose error:", e); }).finally(() => reciprocateClose(event)); else reciprocateClose(event); }; const handleErrorFromClient = (e) => { connection.removeEventListener("message", handleMessageFromClient); connection.removeEventListener("error", handleErrorFromClient); this.onError(connection, e.error)?.catch((e) => { console.error("onError error:", e); }); }; connection.addEventListener("close", handleCloseFromClient); connection.addEventListener("error", handleErrorFromClient); connection.addEventListener("message", handleMessageFromClient); } #_name; /** * The name for this server. * * Resolves from `this.ctx.id.name` — the native DO id name, populated * whenever the stub was created via `idFromName()` or `getByName()`. * This is available inside every entry point (including the constructor, * alarms, and hibernating websocket handlers). * * For alarm handlers firing on stale on-disk alarm records from * older workerd versions that didn't persist `name` into the alarm * record, the name is recovered from a storage fallback record. * * Throws if neither source is available — typically this means the DO * was addressed via `idFromString()` or `newUniqueId()`, which is not * supported by PartyServer. */ get name() { const ctxName = this.ctx.id.name; if (ctxName !== void 0) return ctxName; if (this.#_name) return this.#_name; throw new Error(`Attempting to read .name on ${this.#ParentClass.name}, but this.ctx.id.name is not set and no ${NAME_STORAGE_KEY} fallback record is available. PartyServer requires DOs to be addressed via idFromName()/getByName(), or explicitly bootstrapped with setName() when using idFromString()/newUniqueId(). If this happens in an alarm handler firing on a stale alarm record, initialize the DO from a fetch/RPC entry point first so PartyServer can persist the fallback name.`); } /** * Establish this server's name and trigger `onStart()`. * * Use cases: * * 1. **Framework-level bootstrap of DOs where `ctx.id.name` is * undefined** — e.g. DOs addressed via `idFromString()` / * `newUniqueId()`. `setName()` stashes the name in memory and * persists it under `__ps_name` so cold-wake invocations * recover it via `#ensureInitialized()`'s legacy fallback. * 2. **Delivering initial `props` to `onStart()`** via the * optional second argument. * * For DOs addressed via `idFromName()` / `getByName()`, calling * `setName()` is redundant — `this.name` is available automatically * from `ctx.id.name`. The normal initialization path also persists * a fallback record so old-compat alarm handlers can recover the name. * Throws if `name` does not match `ctx.id.name`. * * **Not appropriate for facets.** Cloudflare Agents and any other * framework using `ctx.facets.get(...)` should pass an explicit * `id` in `FacetStartupOptions` so the facet has its own * `ctx.id.name`: * * ```ts * const stub = ctx.facets.get(facetKey, () => ({ * class: ChildClass, * id: ctx.exports.SomeBoundDOClass.idFromName(facetName), * })); * ``` * * Without an explicit `id`, the facet inherits the parent DO's * `ctx.id` (including `ctx.id.name`), and `setName()` will throw * the ctx.id.name-mismatch error because the facet's intended * name differs from the parent's. See * https://developers.cloudflare.com/dynamic-workers/usage/durable-object-facets/ * for the `FacetStartupOptions.id` semantics. * * @deprecated for callers that address DOs via `idFromName()` / * `getByName()`. Still the supported API for framework-level * bootstrap of header/`newUniqueId`-addressed DOs and for * delivering initial `props` to `onStart()`. */ async setName(name, props) { if (!name) throw new Error("A name is required."); const ctxName = this.ctx.id.name; if (ctxName !== void 0 && ctxName !== name) throw new Error(`This server's Durable Object id was created for name "${ctxName}", cannot setName to "${name}".`); if (this.#_name && this.#_name !== name) throw new Error(`This server already has a name: ${this.#_name}, attempting to set to: ${name}`); if (props !== void 0) this.#_props = props; if (!this.#_name && ctxName === void 0) { await this.ctx.storage.put(NAME_STORAGE_KEY, name); this.#_name = name; } await this.#ensureInitialized(); } /** * @internal * @deprecated Retained for backward compatibility with older callers. * `routePartykitRequest` no longer uses this method; it sends props via * the `x-partykit-props` header on the underlying `fetch()` request. */ async _initAndFetch(name, props, request) { await this.setName(name, props); return this.fetch(request); } #sendMessageToConnection(connection, message) { try { connection.send(message); } catch (_e) { connection.close(1011, "Unexpected error"); } } /** Send a message to all connected clients, except connection ids listed in `without` */ broadcast(msg, without) { for (const connection of this.#connectionManager.getConnections()) if (!without || !without.includes(connection.id)) this.#sendMessageToConnection(connection, msg); } /** Get a connection by connection id */ getConnection(id) { return this.#connectionManager.getConnection(id); } /** * Get all connections. Optionally, you can provide a tag to filter returned connections. * Use `Server#getConnectionTags` to tag the connection on connect. */ getConnections(tag) { return this.#connectionManager.getConnections(tag); } /** * You can tag a connection to filter them in Server#getConnections. * Each connection supports up to 9 tags, each tag max length is 256 characters. */ getConnectionTags(connection, context) { return []; } #_props; /** * Called when the server is started for the first time. */ onStart(props) {} /** * Called when a new connection is made to the server. */ onConnect(connection, ctx) {} /** * Called when a message is received from a connection. */ onMessage(connection, message) {} /** * Called when a connection is closed. */ onClose(connection, code, reason, wasClean) {} /** * Called when an error occurs on a connection. */ onError(connection, error) { console.error(`Error on connection ${connection.id} in ${this.#ParentClass.name}:${this.name}:`, error); console.info(`Implement onError on ${this.#ParentClass.name} to handle this error.`); } /** * Called when a request is made to the server. */ onRequest(request) { console.warn(`onRequest hasn't been implemented on ${this.#ParentClass.name}:${this.name} responding to ${request.url}`); return new Response("Not implemented", { status: 404 }); } /** * Called when an exception occurs. * @param error - The error that occurred. */ onException(error) { console.error(`Exception in ${this.#ParentClass.name}:${this.name}:`, error); console.info(`Implement onException on ${this.#ParentClass.name} to handle this error.`); } onAlarm() { console.log(`Implement onAlarm on ${this.#ParentClass.name} to handle alarms.`); } async alarm() { await this.#ensureInitialized(); await this.onAlarm(); } }; //#endregion export { Server, getServerByName, routePartykitRequest }; //# sourceMappingURL=index.js.map