UNPKG

agents

Version:

A home for your AI agents

1,005 lines (1,004 loc) 37.6 kB
import { camelCaseToKebabCase } from "./utils.js"; import "./types.js"; import { a as isCapnWebTransportUpgrade, n as CAPNWEB_TRANSPORT_SEND } from "./transport-protocol-BxyngQ_R.js"; import { i as _classPrivateFieldInitSpec, n as _classPrivateFieldSet2, r as _assertClassBrand, t as _classPrivateFieldGet2 } from "./classPrivateFieldGet2-DZBYAB34.js"; import { t as LifecycleCapability } from "./capability-B4WbF81e.js"; import { t as _classPrivateMethodInitSpec } from "./classPrivateMethodInitSpec-qMjJ6sHQ.js"; import "./lifecycle-Mm_jQh7r.js"; import { newWebSocketRpcSession } from "capnweb"; import { RpcTarget as RpcTarget$1 } from "cloudflare:workers"; import { nanoid } from "nanoid"; //#region src/websockets/connection-flags.ts /** * Internal per-connection flags, stored inside the connection's own state * under `_cf_`-prefixed keys so they ride the hibernation attachment (or a * bridged connection's synced state) and survive a wake. The state wrapper * installed by {@link ensureConnectionWrapped} hides them from * `connection.state` and preserves them across user `setState` calls. * * Owned by the WebSockets capability; `Agent` and framework mixins reach it * through the capability or, for their own keys, through * {@link registerInternalConnectionKeys}. */ /** A readonly connection may not update host state. */ const CF_READONLY_KEY = "_cf_readonly"; /** * A no-protocol connection receives no protocol text frames (identity, * state sync, MCP servers) — neither on connect nor via broadcasts. */ const CF_NO_PROTOCOL_KEY = "_cf_no_protocol"; const internalKeys = /* @__PURE__ */ new Set([CF_READONLY_KEY, CF_NO_PROTOCOL_KEY]); /** * Register additional `_cf_`-prefixed keys a host or mixin stores in * connection state, so they are hidden from `connection.state` and kept * across user `setState` calls like the capability's own flags. */ function registerInternalConnectionKeys(...keys) { for (const key of keys) internalKeys.add(key); } function rawHasInternalKeys(raw) { for (const key of Object.keys(raw)) if (internalKeys.has(key)) return true; return false; } /** A copy of `raw` without internal keys, or null when no user keys remain. */ function stripInternalKeys(raw) { const result = {}; let hasUserKeys = false; for (const key of Object.keys(raw)) if (!internalKeys.has(key)) { result[key] = raw[key]; hasUserKeys = true; } return hasUserKeys ? result : null; } /** A copy containing only the internal keys present in `raw`. */ function extractInternalFlags(raw) { const result = {}; for (const key of Object.keys(raw)) if (internalKeys.has(key)) result[key] = raw[key]; return result; } /** * Per-connection raw accessors, keyed by the live connection object. The * map is in-memory: after hibernation it is empty, and every entry point * re-wraps the connection on first use. */ const rawStateAccessors = /* @__PURE__ */ new WeakMap(); /** * Wrap `connection.state` / `connection.setState` so internal flags are * hidden from user code and preserved when user code sets state. * Idempotent, and safe to call after hibernation. */ function ensureConnectionWrapped(connection) { if (rawStateAccessors.has(connection)) return; const descriptor = Object.getOwnPropertyDescriptor(connection, "state"); let getRaw; let setRaw; if (descriptor?.get) { getRaw = descriptor.get.bind(connection); setRaw = connection.setState.bind(connection); } else { let rawState = connection.state ?? null; getRaw = () => rawState; setRaw = (state) => { rawState = state; return rawState; }; } rawStateAccessors.set(connection, { getRaw, setRaw }); Object.defineProperty(connection, "state", { configurable: true, enumerable: true, get() { const raw = getRaw(); if (raw != null && typeof raw === "object" && rawHasInternalKeys(raw)) return stripInternalKeys(raw); return raw; } }); Object.defineProperty(connection, "setState", { configurable: true, writable: true, value(stateOrFn) { const raw = getRaw(); const flags = raw != null && typeof raw === "object" ? extractInternalFlags(raw) : {}; const hasFlags = Object.keys(flags).length > 0; let newUserState; if (typeof stateOrFn === "function") newUserState = stateOrFn(hasFlags ? stripInternalKeys(raw) : raw); else newUserState = stateOrFn; if (hasFlags) { if (newUserState != null && typeof newUserState === "object") return setRaw({ ...newUserState, ...flags }); return setRaw(flags); } return setRaw(newUserState); } }); } /** The raw connection state, internal flags included. */ function getConnectionRawState(connection) { ensureConnectionWrapped(connection); return rawStateAccessors.get(connection).getRaw(); } /** Read an internal flag from the raw connection state. */ function getConnectionFlag(connection, key) { ensureConnectionWrapped(connection); return rawStateAccessors.get(connection).getRaw()?.[key]; } /** * Write an internal flag to the raw connection state. `undefined` removes * the key rather than storing a dead value; the last key removed leaves * `null`. */ function setConnectionFlag(connection, key, value) { ensureConnectionWrapped(connection); const accessors = rawStateAccessors.get(connection); const raw = accessors.getRaw() ?? {}; if (value === void 0) { const { [key]: _, ...rest } = raw; accessors.setRaw(Object.keys(rest).length > 0 ? rest : null); } else accessors.setRaw({ ...raw, [key]: value }); } function isConnectionReadonly(connection) { return !!getConnectionFlag(connection, CF_READONLY_KEY); } function setConnectionReadonly(connection, readonly) { setConnectionFlag(connection, CF_READONLY_KEY, readonly ? true : void 0); } function isConnectionProtocolEnabled(connection) { return !getConnectionFlag(connection, CF_NO_PROTOCOL_KEY); } function setConnectionProtocolEnabled(connection, enabled) { setConnectionFlag(connection, CF_NO_PROTOCOL_KEY, enabled ? void 0 : true); } //#endregion //#region src/websockets/connection.ts let _Symbol$iterator; 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 tryGetManagedWebSocketMeta(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 isManagedWebSocket(ws) { return tryGetManagedWebSocketMeta(ws) !== null; } var _cache = /* @__PURE__ */ new WeakMap(); /** * Cache websocket attachments to avoid having to rehydrate them on every property access. */ var AttachmentCache = class { constructor() { _classPrivateFieldInitSpec(this, _cache, /* @__PURE__ */ new WeakMap()); } get(ws) { let attachment = _classPrivateFieldGet2(_cache, this).get(ws); if (!attachment) { attachment = WebSocket.prototype.deserializeAttachment.call(ws); if (attachment !== void 0) _classPrivateFieldGet2(_cache, this).set(ws, attachment); else throw new Error("Missing managed WebSocket lifecycle attachment"); } return attachment; } set(ws, attachment) { _classPrivateFieldGet2(_cache, this).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 createConnection = (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 ?? []; } }, state: { configurable: true, get() { return attachments.get(ws).__user ?? null; } }, setState: { configurable: true, value: function setState(setState) { const state = setState instanceof Function ? setState(this.state) : setState; attachments.set(ws, { ...attachments.get(ws), __user: state ?? null }); return state; } } }); if (initialState) connection.setState(initialState); connections.add(connection); return connection; }; var _sockets = /* @__PURE__ */ new WeakMap(); _Symbol$iterator = Symbol.iterator; var ConnectionIterator = class { constructor(sockets, tag) { this.sockets = sockets; this.tag = tag; this.index = 0; _classPrivateFieldInitSpec(this, _sockets, void 0); } [_Symbol$iterator]() { return this; } next() { const sockets = _classPrivateFieldGet2(_sockets, this) ?? _classPrivateFieldSet2(_sockets, this, this.sockets.get(this.tag)); let socket; while (socket = sockets[this.index++]) if (socket.readyState === WebSocket.OPEN) { if (!isManagedWebSocket(socket)) continue; return { done: false, value: createConnection(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; } /** The platform-backed manager for hibernating WebSockets. */ var ConnectionManager = class { constructor(controller) { this.controller = controller; } getConnection(id) { const matching = this.controller.get(id).filter((ws) => { return tryGetManagedWebSocketMeta(ws)?.id === id; }); if (matching.length === 0) return void 0; if (matching.length === 1) return createConnection(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 ConnectionIterator(this.controller, tag); } accept(connection, options) { const tags = prepareTags(connection.id, options.tags); this.controller.accept(connection, tags); attachments.set(connection, { __pk: { id: connection.id, tags, uri: connection.uri ?? void 0 }, __user: null }); return createConnection(connection); } }; //#endregion //#region src/websockets/callables-target.ts /** * The single exposure policy for callable names. `Object.prototype` and * `RpcTarget.prototype` members are unreachable over Cap'n Web anyway * and are silently excluded; `then` is rejected loudly because exposing * it would make the remote stub thenable. */ function assertExposable(name) { if (name === "then") throw new Error("A callables target cannot expose a method named \"then\" — it would make the remote stub thenable"); if (name === "__cf_agent_send") throw new Error(`A callables target cannot expose "${CAPNWEB_TRANSPORT_SEND}"; it is the transport's frame pipe`); return !(name === "constructor" || name in Object.prototype || name in RpcTarget$1.prototype); } /** * The exposable prototype methods of a callables target, each bound to * invoke on the real instance (so private fields and `this` behave). * * Cap'n Web resolves methods on the prototype chain and rejects own * instance properties, so only prototype methods participate. The * nearest declaration wins for overridden names. * * @param target - The callables target to enumerate. * @returns Method names mapped to invokers on the target. */ function exposableMethods(target) { const methods = /* @__PURE__ */ new Map(); const seen = /* @__PURE__ */ new Set(); let prototype = Object.getPrototypeOf(target); while (prototype && prototype !== RpcTarget$1.prototype && prototype !== Object.prototype) { for (const name of Object.getOwnPropertyNames(prototype)) { if (seen.has(name)) continue; seen.add(name); if (!assertExposable(name)) continue; const descriptor = Object.getOwnPropertyDescriptor(prototype, name); if (!descriptor || typeof descriptor.value !== "function") continue; const method = descriptor.value; methods.set(name, (...args) => Reflect.apply(method, target, args)); } prototype = Object.getPrototypeOf(prototype); } return methods; } /** * Build a Cap'n Web session root exposing exactly the given methods. * * Cap'n Web resolves methods on the prototype chain, rejects own * instance properties, and breaks on Proxy-wrapped roots — so the root * is a private `RpcTarget` subclass whose prototype carries the methods * and nothing else. */ function buildRoot(methods) { class Root extends RpcTarget$1 {} for (const [name, invoke] of methods) Object.defineProperty(Root.prototype, name, { value: invoke, writable: true, configurable: true, enumerable: false }); return new Root(); } //#endregion //#region src/websockets/transport.ts /** * A non-hibernating connection with the public `Connection` contract, * backed by a Cap'n Web session instead of a hibernating socket. It lives * only in memory and disappears with the isolate. */ var CapnWebConnection = class extends EventTarget { constructor(id, uri, pipe) { super(); this.id = id; this.uri = uri; this.pipe = pipe; this.CONNECTING = WebSocket.CONNECTING; this.OPEN = WebSocket.OPEN; this.CLOSING = WebSocket.CLOSING; this.CLOSED = WebSocket.CLOSED; this.readyState = WebSocket.OPEN; this.state = null; this.tags = []; } send(message) { if (this.readyState !== WebSocket.OPEN) throw new TypeError("WebSocket send() after close"); this.pipe.send(message); } close(code, reason) { if (this.readyState >= WebSocket.CLOSING) return; this.pipe.close(code, reason); this.readyState = WebSocket.CLOSING; } setState(next) { const state = typeof next === "function" ? next(this.state) : next; this.state = state; return state; } }; /** * Accept a Cap'n Web transport upgrade. * * The session root carries the frame pipe plus the host's native * callables. The client's protocol frames arrive through the pipe and are * handed to `onMessage`; the host's frames go out through the client's * `message` callback; `useAgent().stub` calls hit the callables directly. * The socket is a plain in-memory `WebSocketPair`, so it pins the Durable * Object and does not survive hibernation. */ async function openCapnWebSession(options) { const { request, connectionId } = options; const pair = new WebSocketPair(); const server = pair[0]; server.accept(); let client; let closed = false; let close = { code: 1e3, reason: "", wasClean: true }; const connection = new CapnWebConnection(connectionId, request.url, { send: (message) => { client?.message(message).catch((error) => { if (!closed) console.error("Cap'n Web frame delivery failed:", error); }); }, close: (code = 1e3, reason = "") => { server.close(code, reason); close = { code, reason, wasClean: true }; finish(); } }); const session = { connection, dispose: () => connection.close(1001, "Session replaced") }; const finish = async () => { if (closed) return; closed = true; connection.readyState = WebSocket.CLOSED; try { client?.[Symbol.dispose](); } catch {} options.onDispose(session); await options.onClose(connection, close.code, close.reason, close.wasClean); }; const rootMethods = new Map(options.callables(connection)); rootMethods.set(CAPNWEB_TRANSPORT_SEND, (message) => options.onMessage(connection, message)); client = newWebSocketRpcSession(server, buildRoot(rootMethods)); server.addEventListener("close", (event) => { close = { code: event.code, reason: event.reason, wasClean: event.wasClean }; finish(); }, { once: true }); server.addEventListener("error", (event) => { if (closed) return; close = { ...close, wasClean: false }; const error = event instanceof ErrorEvent ? event.error ?? new Error(event.message) : /* @__PURE__ */ new Error("Cap'n Web transport socket error"); options.onError(connection, error).catch((handlerError) => { console.error("Cap'n Web onError handler failed:", handlerError); }).finally(finish); }, { once: true }); const ctx = { request }; connection.tags = prepareTags(connectionId, await options.tags(connection, ctx)); options.onOpen(session); try { await options.onConnect(connection, ctx); } catch (error) { connection.close(1011, "onConnect failed"); throw error; } return { response: new Response(null, { status: 101, webSocket: pair[1] }), session }; } //#endregion //#region src/websockets/websockets.ts /** * Reserved close codes the runtime synthesizes when there was no real * Close frame from the peer (1005 NoStatusReceived, 1006 AbnormalClosure, * 1015 TLSHandshake). They cannot appear in an outgoing Close frame, and * there is no peer left to receive a reciprocation. */ function isReservedCloseCode(code) { return code === 1005 || code === 1006 || code === 1015; } /** * Reciprocate a peer-initiated Close frame to complete the handshake, as * the Hibernation API contract requires. Best-effort: swallows errors * from already-closed sockets or invalid codes/reasons, and skips * reciprocation entirely for reserved codes (dead transport). */ function reciprocateClose(ws, code, reason) { if (isReservedCloseCode(code)) return; try { ws.close(code, reason); } catch {} } function isStateFrame(value) { if (typeof value !== "object" || value === null) return false; const frame = value; return frame.type === "cf_agent_state" && "state" in frame; } /** Parse a text frame; anything that is not JSON is `undefined`. */ function parseJson(text) { try { return JSON.parse(text); } catch { return; } } function isRpcRequest(value) { if (typeof value !== "object" || value === null) return false; const frame = value; return frame.type === "rpc" && typeof frame.id === "string" && typeof frame.method === "string" && Array.isArray(frame.args); } var _handlers = /* @__PURE__ */ new WeakMap(); var _getConnectionTags = /* @__PURE__ */ new WeakMap(); var _protocol = /* @__PURE__ */ new WeakMap(); var _readonly = /* @__PURE__ */ new WeakMap(); var _state = /* @__PURE__ */ new WeakMap(); var _connecting = /* @__PURE__ */ new WeakMap(); var _callables = /* @__PURE__ */ new WeakMap(); var _sessions = /* @__PURE__ */ new WeakMap(); var _manager = /* @__PURE__ */ new WeakMap(); var _WebSockets_brand = /* @__PURE__ */ new WeakSet(); /** * Opt-in WebSocket support for Lifecycle Objects. * * Lifecycle itself does not model WebSockets — hosts that want them * install this capability, which owns the connection subsystem end to * end: it claims upgrades, dispatches `onConnect`/`onMessage`/`onClose` * inside the host invocation boundary, reciprocates close handshakes, * and answers `getConnections()`/`getConnection()`. * * ```ts * class Room extends DurableObject<Env> { * readonly webSockets = new WebSockets({ * handlers: { * onConnect: (connection) => connection.send("welcome"), * onMessage: (connection, message) => { ... } * }, * callables: new RoomCallables() * }); * readonly lifecycle = Lifecycle.install(this).use(this.webSockets); * } * ``` * * Connections arrive on one of two wires, chosen by the client: * * - **cf-websocket** (default): accepted with the Hibernation API. Idle * clients stay connected while the Durable Object leaves memory. * - **capnweb** (`?__agents_transport=capnweb`): protocol frames through * one pipe method on a Cap'n Web session whose root also carries the * host's callables natively. Non-hibernating — the object stays pinned * while the connection is open. * * Both wires dispatch the same handlers and appear in `getConnections()`. * On both, the capability speaks the Agent protocol a plain host needs * for `useAgent` and `AgentClient`: it sends the identity frame on * connect, and it serves `callables` — as `rpc` JSON frames on the * WebSocket wire, and natively on the Cap'n Web session root, where an * `RpcTarget` result becomes a live stub and calls pipeline. `call()` * and `stub` work against a plain Durable Object exactly as against an * `Agent`. Pass a `State` capability as `state` and the hook's * `state`/`setState` work too: the current value is pushed on connect * and client updates are validated and applied; the state owner * broadcasts changes with `broadcastState()`. Per-connection readonly * and no-protocol flags live here as well, for every host. * * @experimental The API surface may change before stabilizing. */ var WebSockets = class extends LifecycleCapability { constructor(options = {}) { super("websockets"); _classPrivateMethodInitSpec(this, _WebSockets_brand); this.claims = "catch-all"; _classPrivateFieldInitSpec(this, _handlers, void 0); _classPrivateFieldInitSpec(this, _getConnectionTags, void 0); _classPrivateFieldInitSpec(this, _protocol, void 0); _classPrivateFieldInitSpec(this, _readonly, void 0); _classPrivateFieldInitSpec(this, _state, void 0); _classPrivateFieldInitSpec(this, _connecting, /* @__PURE__ */ new Set()); _classPrivateFieldInitSpec(this, _callables, void 0); _classPrivateFieldInitSpec(this, _sessions, /* @__PURE__ */ new Map()); _classPrivateFieldInitSpec(this, _manager, void 0); _classPrivateFieldSet2(_handlers, this, options.handlers); _classPrivateFieldSet2(_getConnectionTags, this, options.getConnectionTags); _classPrivateFieldSet2(_protocol, this, options.protocol ?? true); _classPrivateFieldSet2(_readonly, this, options.readonly); _classPrivateFieldSet2(_state, this, options.state); _classPrivateFieldSet2(_callables, this, options.callables ? exposableMethods(options.callables) : /* @__PURE__ */ new Map()); } /** * Claim every upgrade, never declining: a Cap'n Web transport upgrade * becomes a session, everything else a tracked hibernating connection, * whether or not handlers or callables are configured. Handlers only add * behavior on connect, message, close, and error. */ onWebSocketUpgrade({ request }) { return isCapnWebTransportUpgrade(request) ? _assertClassBrand(_WebSockets_brand, this, _acceptCapnWebSession).call(this, request) : _assertClassBrand(_WebSockets_brand, this, _acceptConnection).call(this, request); } /** Dispatch a platform message wake for a capability-owned socket. */ async onWebSocketMessage(ws, message) { if (!isManagedWebSocket(ws)) return false; await _assertClassBrand(_WebSockets_brand, this, _message).call(this, createConnection(ws), message); return true; } /** Dispatch and reciprocate a close wake for an owned socket. */ async onWebSocketClose(ws, code, reason, wasClean) { if (!isManagedWebSocket(ws)) return false; try { await _assertClassBrand(_WebSockets_brand, this, _close).call(this, createConnection(ws), code, reason, wasClean); } finally { reciprocateClose(ws, code, reason); } return true; } /** Dispatch an error wake for an owned socket. */ async onWebSocketError(ws, error) { if (!isManagedWebSocket(ws)) return false; await _assertClassBrand(_WebSockets_brand, this, _error).call(this, createConnection(ws), error); return true; } /** * Close every owned connection during explicit host destruction. The * capability owns its sockets' lifetimes, so it also owns tearing * them down. */ dispose() { for (const connection of this.getConnections()) try { connection.close(1001, "Durable Object destroyed"); } catch {} } /** Open connections on either wire, optionally by tag. */ *getConnections(tag) { for (const { connection } of _classPrivateFieldGet2(_sessions, this).values()) { if (connection.readyState !== WebSocket.OPEN) continue; if (!tag || connection.tags.includes(tag)) yield connection; } yield* _get_connectionManager.call(_assertClassBrand(_WebSockets_brand, this)).getConnections(tag); } /** One connection on either wire, by id. */ getConnection(id) { const session = _classPrivateFieldGet2(_sessions, this).get(id)?.connection; if (session && session.readyState === WebSocket.OPEN) return session; return _get_connectionManager.call(_assertClassBrand(_WebSockets_brand, this)).getConnection(id); } /** * Send the identity frame, unless the connection is no-protocol. The * defaults are the Durable Object's routed name and host class; a host * whose public identity differs — an `Agent` facet, whose routed name * is an internal encoding of its logical name — passes its own. */ sendIdentity(connection, identity = { name: this.lifecycle.name, agent: camelCaseToKebabCase(this.lifecycle.className) }) { if (!isConnectionProtocolEnabled(connection)) return; _assertClassBrand(_WebSockets_brand, this, _sendFrame).call(this, connection, { type: "cf_agent_identity", name: identity.name, agent: identity.agent }); } /** * Send the current state to one connection, unless nothing is stored * or the connection is no-protocol. Reading the state may seed the * initial value; see `#connecting`. */ sendState(connection) { if (!_classPrivateFieldGet2(_state, this) || !isConnectionProtocolEnabled(connection)) return; const current = _classPrivateFieldGet2(_state, this).get(); if (current === void 0) return; _assertClassBrand(_WebSockets_brand, this, _sendFrame).call(this, connection, { type: "cf_agent_state", state: current }); } /** * Apply a parsed `cf_agent_state` frame from a client. A readonly * connection is refused; a change the host's validator rejects is * logged in full server-side and answered with a generic * `cf_agent_state_error`. Broadcasting the accepted change is the state * owner's call, through its `onChanged` hook. Callers that drive the * protocol themselves call this inside their own invocation context; * the capability's automatic path does so via `runInHostContext`. * * @returns Whether the frame was a state frame (handled either way). */ applyStateFrame(connection, frame) { if (!_classPrivateFieldGet2(_state, this) || !isStateFrame(frame)) return false; if (isConnectionReadonly(connection)) { _assertClassBrand(_WebSockets_brand, this, _sendFrame).call(this, connection, { type: "cf_agent_state_error", error: "Connection is readonly" }); return true; } try { _classPrivateFieldGet2(_state, this).set(frame.state, connection); } catch (error) { console.error("[WebSockets] State update rejected:", error); _assertClassBrand(_WebSockets_brand, this, _sendFrame).call(this, connection, { type: "cf_agent_state_error", error: "State update rejected" }); } return true; } /** * Push the current state to every protocol-enabled connection except * the one a change came from, which already has the value it sent. * Wire a `State`'s `onChanged` to this. */ broadcastState(except) { if (!_classPrivateFieldGet2(_state, this)) return; const current = _classPrivateFieldGet2(_state, this).get(); if (current === void 0) return; const frame = { type: "cf_agent_state", state: current }; for (const connection of this.getConnections()) { if (connection === except) continue; if (_classPrivateFieldGet2(_connecting, this).has(connection)) continue; if (!isConnectionProtocolEnabled(connection)) continue; _assertClassBrand(_WebSockets_brand, this, _sendFrame).call(this, connection, frame); } } /** Whether the connection may update host state over the wire. */ isReadonly(connection) { return isConnectionReadonly(connection); } /** Mark a connection readonly, or writable again. */ setReadonly(connection, readonly = true) { setConnectionReadonly(connection, readonly); } /** Whether protocol text frames reach the connection. */ isProtocolEnabled(connection) { return isConnectionProtocolEnabled(connection); } /** Enable or suppress protocol text frames for a connection. */ setProtocolEnabled(connection, enabled) { setConnectionProtocolEnabled(connection, enabled); } }; function _get_connectionManager() { _classPrivateFieldGet2(_manager, this) ?? _classPrivateFieldSet2(_manager, this, new ConnectionManager(this.lifecycle.sockets)); return _classPrivateFieldGet2(_manager, this); } async function _connect(connection, ctx) { ensureConnectionWrapped(connection); if (_classPrivateFieldGet2(_readonly, this)?.call(this, connection, ctx)) setConnectionReadonly(connection, true); if (_classPrivateFieldGet2(_protocol, this) !== false) if (typeof _classPrivateFieldGet2(_protocol, this) === "function" ? _classPrivateFieldGet2(_protocol, this).call(this, connection, ctx) : true) { _classPrivateFieldGet2(_connecting, this).add(connection); try { this.sendIdentity(connection); this.sendState(connection); } finally { _classPrivateFieldGet2(_connecting, this).delete(connection); } } else setConnectionProtocolEnabled(connection, false); await this.lifecycle.runInHostContext(() => _classPrivateFieldGet2(_handlers, this)?.onConnect?.(connection, ctx), { connection, request: ctx.request }); } async function _message(connection, message) { if (_classPrivateFieldGet2(_state, this) && _classPrivateFieldGet2(_protocol, this) !== false && typeof message === "string") { const frame = parseJson(message); if (isStateFrame(frame)) { await this.lifecycle.runInHostContext(() => this.applyStateFrame(connection, frame), { connection }); return; } } if (_classPrivateFieldGet2(_callables, this).size > 0 && await _assertClassBrand(_WebSockets_brand, this, _answerRpc).call(this, connection, message)) return; await this.lifecycle.runInHostContext(() => _classPrivateFieldGet2(_handlers, this)?.onMessage?.(connection, message), { connection }); } function _close(connection, code, reason, wasClean) { return this.lifecycle.runInHostContext(() => _classPrivateFieldGet2(_handlers, this)?.onClose?.(connection, code, reason, wasClean), { connection }); } function _error(connection, error) { return this.lifecycle.runInHostContext(() => _classPrivateFieldGet2(_handlers, this)?.onError?.(connection, error), { connection }); } async function _acceptConnection(request) { const { 0: clientWebSocket, 1: serverWebSocket } = new WebSocketPair(); const connectionId = new URL(request.url).searchParams.get("_pk") || nanoid(); let connection = Object.assign(serverWebSocket, { id: connectionId, uri: request.url, tags: [], state: null, setState(setState) { const state = setState instanceof Function ? setState(this.state) : setState; this.state = state; return this.state; } }); const ctx = { request }; const tags = _classPrivateFieldGet2(_getConnectionTags, this) ? await _classPrivateFieldGet2(_getConnectionTags, this).call(this, connection, ctx) : []; connection = _get_connectionManager.call(_assertClassBrand(_WebSockets_brand, this)).accept(connection, { tags }); await _assertClassBrand(_WebSockets_brand, this, _connect).call(this, connection, ctx); return new Response(null, { status: 101, webSocket: clientWebSocket }); } async function _acceptCapnWebSession(request) { const connectionId = new URL(request.url).searchParams.get("_pk") || nanoid(); _classPrivateFieldGet2(_sessions, this).get(connectionId)?.dispose(); const { response } = await openCapnWebSession({ request, connectionId, tags: (connection, ctx) => _classPrivateFieldGet2(_getConnectionTags, this)?.call(this, connection, ctx) ?? [], onConnect: (connection, ctx) => _assertClassBrand(_WebSockets_brand, this, _connect).call(this, connection, ctx), onMessage: (connection, message) => _assertClassBrand(_WebSockets_brand, this, _message).call(this, connection, message), onClose: (connection, code, reason, wasClean) => _assertClassBrand(_WebSockets_brand, this, _close).call(this, connection, code, reason, wasClean).then(() => void 0), onError: (connection, error) => _assertClassBrand(_WebSockets_brand, this, _error).call(this, connection, error).then(() => void 0), callables: (connection) => _assertClassBrand(_WebSockets_brand, this, _nativeCallables).call(this, connection), onOpen: (session) => _classPrivateFieldGet2(_sessions, this).set(connectionId, session), onDispose: (ended) => { if (_classPrivateFieldGet2(_sessions, this).get(connectionId) === ended) _classPrivateFieldGet2(_sessions, this).delete(connectionId); } }); return response; } /** * Send one protocol frame. Serialization is the caller's contract — every * frame built here is plain JSON — while a send failure is tolerated: the * peer may have disconnected between the wake and the send. */ function _sendFrame(connection, frame) { _assertClassBrand(_WebSockets_brand, this, _send).call(this, connection, JSON.stringify(frame)); } /** Write one already-serialized frame, tolerating a closed peer. */ function _send(connection, text) { try { connection.send(text); } catch {} } /** * The `callables` target as native Cap'n Web methods for one session: * every method dispatches through the host boundary with this * connection in scope and emits `rpc`/`rpc:error` events. Return values * keep Cap'n Web semantics — an `RpcTarget` comes back as a live stub. */ function _nativeCallables(connection) { const methods = /* @__PURE__ */ new Map(); for (const [name, invoke] of _classPrivateFieldGet2(_callables, this)) methods.set(name, (...args) => _assertClassBrand(_WebSockets_brand, this, _dispatchCallable).call(this, name, () => invoke(...args), connection)); return methods; } /** * Answer one `rpc` frame against `callables`. A `ReadableStream` * result streams as `done: false` chunks followed by a final * `done: true` frame, matching the client's stream callbacks. * * @returns Whether the message was an `rpc` frame (answered or not). */ async function _answerRpc(connection, raw) { if (typeof raw !== "string") return false; let frame; try { frame = JSON.parse(raw); } catch { return false; } if (!isRpcRequest(frame)) return false; const { id, method, args } = frame; const invoke = _classPrivateFieldGet2(_callables, this).get(method); if (!invoke) { _assertClassBrand(_WebSockets_brand, this, _reply).call(this, connection, { type: "rpc", id, success: false, error: `Method ${method} does not exist` }); return true; } try { const result = await _assertClassBrand(_WebSockets_brand, this, _dispatchCallable).call(this, method, () => invoke(...args), connection); if (result instanceof RpcTarget$1) throw new Error(`Method ${method} returns an RpcTarget, which only the capnweb transport can carry`); if (result instanceof ReadableStream) { for await (const chunk of result) _assertClassBrand(_WebSockets_brand, this, _reply).call(this, connection, { type: "rpc", id, success: true, done: false, result: chunk }); _assertClassBrand(_WebSockets_brand, this, _reply).call(this, connection, { type: "rpc", id, success: true, done: true, result: void 0 }); } else _assertClassBrand(_WebSockets_brand, this, _reply).call(this, connection, { type: "rpc", id, success: true, done: true, result }); } catch (error) { _assertClassBrand(_WebSockets_brand, this, _reply).call(this, connection, { type: "rpc", id, success: false, error: error instanceof Error ? error.message : String(error) }); } return true; } function _reply(connection, response) { let text; try { text = JSON.stringify(response); } catch (error) { text = JSON.stringify({ type: "rpc", id: response.id, success: false, error: `Result is not JSON-serializable: ${error instanceof Error ? error.message : String(error)}` }); } _assertClassBrand(_WebSockets_brand, this, _send).call(this, connection, text); } async function _dispatchCallable(name, invoke, connection) { const services = this.lifecycle; try { const result = await services.runInHostContext(invoke, { connection }); services.events.emit("rpc", { method: name, streaming: result instanceof ReadableStream }); return result; } catch (error) { services.events.emit("rpc:error", { method: name, error: error instanceof Error ? error.message : String(error) }); throw error; } } //#endregion export { getConnectionFlag as a, isConnectionReadonly as c, setConnectionProtocolEnabled as d, setConnectionReadonly as f, ensureConnectionWrapped as i, registerInternalConnectionKeys as l, CF_NO_PROTOCOL_KEY as n, getConnectionRawState as o, CF_READONLY_KEY as r, isConnectionProtocolEnabled as s, WebSockets as t, setConnectionFlag as u }; //# sourceMappingURL=websockets-D7IzWbZb.js.map