agents
Version:
A home for your AI agents
1,005 lines (1,004 loc) • 37.6 kB
JavaScript
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