UNPKG

@rivetkit/redis

Version:

_Lightweight Libraries for Backends_

1,583 lines (1,532 loc) 50.4 kB
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } var _class; var _chunkXNQTNCJGcjs = require('./chunk-XNQTNCJG.cjs'); var _chunkKFGA2UFQcjs = require('./chunk-KFGA2UFQ.cjs'); var _chunkRIAC4EUGcjs = require('./chunk-RIAC4EUG.cjs'); // src/mod.ts var _core = require('@rivetkit/core'); // src/config.ts var _ioredis = require('ioredis'); var _zod = require('zod'); // src/coordinate/config.ts var CoordinateDriverConfig = _zod.z.object({ actorPeer: _zod.z.object({ leaseDuration: _zod.z.number().default(3e3), renewLeaseGrace: _zod.z.number().default(1500), checkLeaseInterval: _zod.z.number().default(1e3), checkLeaseJitter: _zod.z.number().default(500), messageAckTimeout: _zod.z.number().default(1e3) }) }); // src/config.ts var RedisDriverConfig = CoordinateDriverConfig.extend({ redis: _zod.z.custom((val) => val instanceof _ioredis.Redis, { message: "Must be an instance of Redis" }).optional().default( () => new (0, _ioredis.Redis)({ host: _nullishCoalesce(process.env.REDIS_HOST, () => ( "localhost")), port: process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT, 10) : 6379, password: process.env.REDIS_PASSWORD }) ), keyPrefix: _zod.z.string().default(() => _nullishCoalesce(process.env.REDIS_KEY_PREFIX, () => ( "rivetkit"))) }); // src/coordinate.ts var _cborx = require('cbor-x'); var cbor = _interopRequireWildcard(_cborx); var _dedent = require('dedent'); var _dedent2 = _interopRequireDefault(_dedent); var RedisCoordinateDriver = class { #driverConfig; #redis; #nodeSub; constructor(driverConfig, redis) { this.#driverConfig = driverConfig; this.#redis = redis; this.#defineRedisScripts(); } async createNodeSubscriber(selfNodeId, callback) { this.#nodeSub = this.#redis.duplicate(); this.#nodeSub.on( "messageBuffer", (_channel, messageRaw) => { const message = cbor.decode(messageRaw); callback(message); } ); await this.#nodeSub.subscribe( _chunkRIAC4EUGcjs.PUBSUB.node(this.#driverConfig.keyPrefix, selfNodeId) ); } async publishToNode(targetNodeId, message) { await this.#redis.publish( _chunkRIAC4EUGcjs.PUBSUB.node(this.#driverConfig.keyPrefix, targetNodeId), cbor.encode(message) ); } async getActorLeader(actorId) { const [metadata, nodeId] = await this.#redis.mget([ // TODO: Use exists in pipeline instead of getting all data _chunkRIAC4EUGcjs.KEYS.ACTOR.metadata(this.#driverConfig.keyPrefix, actorId), _chunkRIAC4EUGcjs.KEYS.ACTOR.LEASE.node(this.#driverConfig.keyPrefix, actorId) ]); if (!metadata) { return { actor: void 0 }; } return { actor: { leaderNodeId: nodeId || void 0 } }; } async startActorAndAcquireLease(actorId, selfNodeId, leaseDuration) { const execRes = await this.#redis.multi().getBuffer(_chunkRIAC4EUGcjs.KEYS.ACTOR.metadata(this.#driverConfig.keyPrefix, actorId)).actorPeerAcquireLease( _chunkRIAC4EUGcjs.KEYS.ACTOR.LEASE.node(this.#driverConfig.keyPrefix, actorId), selfNodeId, leaseDuration ).exec(); if (!execRes) { throw new Error("Redis transaction failed"); } const [[getErr, getRes], [leaseErr, leaseRes]] = execRes; if (getErr) throw new Error(`Redis GET error: ${getErr}`); if (leaseErr) throw new Error(`Redis acquire lease error: ${leaseErr}`); const metadataRaw = getRes; const leaderNodeId = leaseRes; if (!metadataRaw) { return { actor: void 0 }; } if (!metadataRaw) throw new Error("Actor should have metadata if initialized."); const metadata = cbor.decode(metadataRaw); return { actor: { name: metadata.name, key: metadata.key, leaderNodeId } }; } async extendLease(actorId, selfNodeId, leaseDuration) { const res = await this.#redis.actorPeerExtendLease( _chunkRIAC4EUGcjs.KEYS.ACTOR.LEASE.node(this.#driverConfig.keyPrefix, actorId), selfNodeId, leaseDuration ); return { leaseValid: res === 1 }; } async attemptAcquireLease(actorId, selfNodeId, leaseDuration) { const newLeaderNodeId = await this.#redis.actorPeerAcquireLease( _chunkRIAC4EUGcjs.KEYS.ACTOR.LEASE.node(this.#driverConfig.keyPrefix, actorId), selfNodeId, leaseDuration ); return { newLeaderNodeId }; } async releaseLease(actorId, nodeId) { await this.#redis.actorPeerReleaseLease( _chunkRIAC4EUGcjs.KEYS.ACTOR.LEASE.node(this.#driverConfig.keyPrefix, actorId), nodeId ); } #defineRedisScripts() { this.#redis.defineCommand("actorPeerAcquireLease", { numberOfKeys: 1, lua: _dedent2.default` -- Get the current value of the key local currentValue = redis.call("get", KEYS[1]) -- Return the current value if an entry already exists if currentValue then return currentValue end -- Create an entry for the provided key redis.call("set", KEYS[1], ARGV[1], "PX", ARGV[2]) -- Return the value to indicate the entry was added return ARGV[1] ` }); this.#redis.defineCommand("actorPeerExtendLease", { numberOfKeys: 1, lua: _dedent2.default` -- Return 0 if an entry exists with a different lease holder if redis.call("get", KEYS[1]) ~= ARGV[1] then return 0 end -- Update the entry for the provided key redis.call("set", KEYS[1], ARGV[1], "PX", ARGV[2]) -- Return 1 to indicate the entry was updated return 1 ` }); this.#redis.defineCommand("actorPeerReleaseLease", { numberOfKeys: 1, lua: _dedent2.default` -- Only remove the entry for this lock value if redis.call("get", KEYS[1]) == ARGV[1] then redis.pcall("del", KEYS[1]) return 1 end -- Return 0 if no entry was removed. return 0 ` }); } }; // src/coordinate/node/mod.ts var _invariant = require('invariant'); var _invariant2 = _interopRequireDefault(_invariant); // src/coordinate/relay-conn.ts // src/coordinate/node/message.ts var _pretry = require('p-retry'); var _pretry2 = _interopRequireDefault(_pretry); async function publishMessageToLeader(registryConfig, driverConfig, CoordinateDriver, globalState, actorId, message, signal) { message.n = globalState.nodeId; const messageId = crypto.randomUUID(); message.m = messageId; await _pretry2.default.call(void 0, () => publishMessageToLeaderInner( registryConfig, driverConfig, CoordinateDriver, globalState, actorId, messageId, message, signal ), { signal, minTimeout: 1e3, retries: 5, onFailedAttempt: (error) => { _chunkXNQTNCJGcjs.logger.call(void 0, ).warn("error publishing message", { attempt: error.attemptNumber, error: error.message }); } } ); } async function publishMessageToLeaderNoRetry(registryConfig, driverConfig, CoordinateDriver, globalState, actorId, message, signal) { message.n = globalState.nodeId; const messageId = crypto.randomUUID(); message.m = messageId; try { await publishMessageToLeaderInner( registryConfig, driverConfig, CoordinateDriver, globalState, actorId, messageId, message, signal ); } catch (error) { if (error instanceof Error) { if (error.message === "Actor not initialized") { throw new LeaderChangedError("Actor not found"); } else if (error.message === "actor not leased, may be transferring leadership") { throw new LeaderChangedError("Leader is changing"); } else if (error.message === "Ack timed out") { throw new LeaderChangedError("Leader not responding"); } } throw error; } } var LeaderChangedError = class extends Error { constructor(message) { super(message); this.name = "LeaderChangedError"; } }; async function publishMessageToLeaderInner(registryConfig, driverConfig, CoordinateDriver, globalState, actorId, messageId, message, signal) { const { actor } = await CoordinateDriver.getActorLeader(actorId); if (!actor) throw new (0, _pretry.AbortError)("Actor not initialized"); if (!actor.leaderNodeId) { throw new Error("actor not leased, may be transferring leadership"); } _chunkXNQTNCJGcjs.logger.call(void 0, ).debug("found actor leader node", { nodeId: actor.leaderNodeId }); const { promise: ackPromise, resolve: ackResolve, reject: ackReject } = Promise.withResolvers(); globalState.messageAckResolvers.set(messageId, ackResolve); const signalListener = () => ackReject(new (0, _pretry.AbortError)("Aborted")); signal == null ? void 0 : signal.addEventListener("abort", signalListener); const timeoutId = setTimeout( () => ackReject(new Error("Ack timed out")), driverConfig.actorPeer.messageAckTimeout ); try { await CoordinateDriver.publishToNode(actor.leaderNodeId, message); _chunkXNQTNCJGcjs.logger.call(void 0, ).debug("waiting for message ack", { messageId }); await ackPromise; _chunkXNQTNCJGcjs.logger.call(void 0, ).debug("received message ack", { messageId }); } finally { globalState.messageAckResolvers.delete(messageId); signal == null ? void 0 : signal.removeEventListener("abort", signalListener); clearTimeout(timeoutId); } } // src/coordinate/relay-conn.ts var RelayConn = class { #registryConfig; #runConfig; #driverConfig; #coordinateDriver; #actorDriver; #inlineClient; #globalState; #driver; #actorId; #actorPeer; #connId; #connToken; #disposed = false; #abortController = new AbortController(); get actorId() { return this.#actorId; } get connId() { if (!this.#connId) throw new (0, _chunkRIAC4EUGcjs.InternalError)("Missing connId"); return this.#connId; } get connToken() { if (!this.#connToken) throw new (0, _chunkRIAC4EUGcjs.InternalError)("Missing connToken"); return this.#connToken; } constructor(registryConfig, runConfig, driverConfig, actorDriver, inlineClient, coordinateDriver, globalState, driver, actorId) { this.#registryConfig = registryConfig; this.#runConfig = runConfig; this.#driverConfig = driverConfig; this.#coordinateDriver = coordinateDriver; this.#actorDriver = actorDriver; this.#inlineClient = inlineClient; this.#driver = driver; this.#globalState = globalState; this.#actorId = actorId; } async start() { const connId = _core.generateConnId.call(void 0, ); const connToken = _core.generateConnToken.call(void 0, ); this.#connId = connId; this.#connToken = connToken; _chunkXNQTNCJGcjs.logger.call(void 0, ).debug("starting relay connection", { actorId: this.#actorId, connId: this.#connId }); this.#actorPeer = await _chunkXNQTNCJGcjs.ActorPeer.acquire( this.#registryConfig, this.#runConfig, this.#driverConfig, this.#actorDriver, this.#inlineClient, this.#coordinateDriver, this.#globalState, this.#actorId, connId ); this.#globalState.relayConns.set(connId, this); } async publishMessageToleader(message, retry) { if (this.#disposed) { _chunkXNQTNCJGcjs.logger.call(void 0, ).warn( "attempted to call sendMessageToLeader on disposed RelayConn" ); return; } if (retry) { await publishMessageToLeader( this.#registryConfig, this.#driverConfig, this.#coordinateDriver, this.#globalState, this.#actorId, message, this.#abortController.signal ); } else { await publishMessageToLeaderNoRetry( this.#registryConfig, this.#driverConfig, this.#coordinateDriver, this.#globalState, this.#actorId, message, this.#abortController.signal ); } } /** * Closes the connection and cleans it up. * * @param fromLeader - If this message is coming from the leader. This will prevent sending a close message back to the leader. */ async disconnect(fromLeader, reason, disconnectMessageToleader) { var _a, _b; if (this.#disposed) { _chunkXNQTNCJGcjs.logger.call(void 0, ).warn("attempted to call disconnect on disposed RelayConn"); return; } this.#disposed = true; this.#abortController.abort(); await this.#driver.disconnect(reason); if (this.#connId) { this.#globalState.relayConns.delete(this.#connId); if (!fromLeader && ((_a = this.#actorPeer) == null ? void 0 : _a.leaderNodeId)) { if (disconnectMessageToleader) { await publishMessageToLeader( this.#registryConfig, this.#driverConfig, this.#coordinateDriver, this.#globalState, this.#actorId, disconnectMessageToleader, void 0 ); } } await ((_b = this.#actorPeer) == null ? void 0 : _b.removeConnectionReference(this.#connId)); } else { _chunkXNQTNCJGcjs.logger.call(void 0, ).warn("disposing connection without connection id"); } } }; // src/coordinate/node/message-handlers/fetch.ts async function handleLeaderFetch(globalState, coordinateDriver, actorRouter, nodeId, fetch) { if (!nodeId) { _chunkXNQTNCJGcjs.logger.call(void 0, ).error("node id not provided for leader fetch"); return; } try { const actor = await _chunkXNQTNCJGcjs.ActorPeer.getLeaderActor(globalState, fetch.ai); if (!actor) { const errorMessage = { b: { ffr: { ri: fetch.ri, status: 404, headers: {}, error: "Actor not found" } } }; await coordinateDriver.publishToNode(nodeId, errorMessage); return; } const url = new URL(`http://actor${fetch.url}`); const body = fetch.body ? fetch.body instanceof Uint8Array ? fetch.body : new TextEncoder().encode(fetch.body) : void 0; const request = new Request(url, { method: fetch.method, headers: fetch.headers, body }); const response = await actorRouter.fetch(request, { actorId: actor.id }); if (!response) { throw new Error("handleFetch returned void unexpectedly"); } const responseHeaders = {}; response.headers.forEach((value, key) => { const lowerKey = key.toLowerCase(); if (lowerKey !== "content-length" && lowerKey !== "transfer-encoding") { responseHeaders[key] = value; } }); let responseBody; if (response.body) { const buffer = await response.arrayBuffer(); responseBody = new Uint8Array(buffer); } const responseMessage = { b: { ffr: { ri: fetch.ri, status: response.status, headers: responseHeaders, body: responseBody } } }; await coordinateDriver.publishToNode(nodeId, responseMessage); } catch (error) { const errorMessage = { b: { ffr: { ri: fetch.ri, status: 500, headers: {}, error: error instanceof Error ? error.message : "Internal server error" } } }; await coordinateDriver.publishToNode(nodeId, errorMessage); } } function handleFollowerFetchResponse(globalState, response) { const resolver = globalState.fetchResponseResolvers.get(response.ri); if (resolver) { resolver(response); } } // src/coordinate/node/message-handlers/websocket-follower.ts async function handleFollowerWebSocketOpen(globalState, open) { var _a, _b, _c, _d, _e, _f; _chunkXNQTNCJGcjs.logger.call(void 0, ).debug("handling follower websocket open", { websocketId: open.wi, hasRelayWebSockets: !!globalState.relayWebSockets, relayWebSocketsSize: _nullishCoalesce(((_a = globalState.relayWebSockets) == null ? void 0 : _a.size), () => ( 0)), hasFollowerWebSockets: !!globalState.followerWebSockets, followerWebSocketsSize: _nullishCoalesce(((_b = globalState.followerWebSockets) == null ? void 0 : _b.size), () => ( 0)) }); const relayWs = (_c = globalState.relayWebSockets) == null ? void 0 : _c.get(open.wi); if (relayWs) { _chunkXNQTNCJGcjs.logger.call(void 0, ).debug("calling _handleOpen on relay websocket", { websocketId: open.wi }); relayWs._handleOpen(); return; } const followerWs = (_d = globalState.followerWebSockets) == null ? void 0 : _d.get(open.wi); if (followerWs) { _chunkXNQTNCJGcjs.logger.call(void 0, ).debug("follower websocket open confirmed by leader", { websocketId: open.wi }); return; } _chunkXNQTNCJGcjs.logger.call(void 0, ).warn("received websocket open for nonexistent follower websocket", { websocketId: open.wi, allRelayWebSocketIds: Array.from(_nullishCoalesce(((_e = globalState.relayWebSockets) == null ? void 0 : _e.keys()), () => ( []))), allFollowerWebSocketIds: Array.from( _nullishCoalesce(((_f = globalState.followerWebSockets) == null ? void 0 : _f.keys()), () => ( [])) ) }); } async function handleFollowerWebSocketMessage(globalState, message) { var _a, _b; const ws = globalState.rawWebSockets.get(message.wi); if (ws) { if (message.data instanceof Uint8Array) { ws.send( message.data.buffer.slice( message.data.byteOffset, message.data.byteOffset + message.data.byteLength ) ); } else { ws.send(message.data); } return; } const relayWs = (_a = globalState.relayWebSockets) == null ? void 0 : _a.get(message.wi); if (relayWs) { relayWs._handleMessage(message.data, message.binary); return; } const followerWs = (_b = globalState.followerWebSockets) == null ? void 0 : _b.get(message.wi); if (followerWs) { _chunkXNQTNCJGcjs.logger.call(void 0, ).debug("forwarding message to follower websocket", { websocketId: message.wi, isBinary: message.binary, dataType: typeof message.data, dataLength: typeof message.data === "string" ? message.data.length : message.data.byteLength }); if (message.data instanceof Uint8Array) { followerWs.ws.send( message.data.buffer.slice( message.data.byteOffset, message.data.byteOffset + message.data.byteLength ) ); } else { followerWs.ws.send(message.data); } return; } _chunkXNQTNCJGcjs.logger.call(void 0, ).warn( "received websocket message for nonexistent follower websocket", { websocketId: message.wi } ); } async function handleFollowerWebSocketClose(globalState, close) { var _a, _b; const ws = globalState.rawWebSockets.get(close.wi); if (ws) { globalState.rawWebSockets.delete(close.wi); ws.close(close.code, close.reason); return; } const relayWs = (_a = globalState.relayWebSockets) == null ? void 0 : _a.get(close.wi); if (relayWs) { relayWs._handleClose(close.code, close.reason); globalState.relayWebSockets.delete(close.wi); return; } const followerWs = (_b = globalState.followerWebSockets) == null ? void 0 : _b.get(close.wi); if (followerWs) { followerWs.ws.close(close.code, close.reason); globalState.followerWebSockets.delete(close.wi); return; } _chunkXNQTNCJGcjs.logger.call(void 0, ).warn("received websocket close for nonexistent follower websocket", { websocketId: close.wi }); } // src/coordinate/node/message-handlers/websocket-leader.ts async function handleLeaderWebSocketOpen(globalState, coordinateDriver, runConfig, actorDriver, nodeId, open) { if (!nodeId) { _chunkXNQTNCJGcjs.logger.call(void 0, ).error("node id not provided for leader websocket open"); return; } _chunkXNQTNCJGcjs.logger.call(void 0, ).debug("handling leader websocket open", { nodeId, websocketId: open.wi, actorId: open.ai, url: open.url }); try { const actor = await _chunkXNQTNCJGcjs.ActorPeer.getLeaderActor(globalState, open.ai); if (!actor) { _chunkXNQTNCJGcjs.logger.call(void 0, ).warn("received websocket open for nonexistent actor leader", { actorId: open.ai }); return; } const url = new URL(`ws://actor${open.url}`); const path = url.pathname; const pathWithQuery = url.pathname + url.search; let wsHandler; if (path === _core.PATH_CONNECT_WEBSOCKET) { wsHandler = await _core.handleWebSocketConnect.call(void 0, void 0, runConfig, actorDriver, open.ai, open.e, open.cp, open.ad ); } else if (path.startsWith(_core.PATH_RAW_WEBSOCKET_PREFIX)) { wsHandler = await _core.handleRawWebSocketHandler.call(void 0, void 0, pathWithQuery, actorDriver, open.ai, open.ad ); } else { throw new Error(`Unreachable path: ${path}`); } const fakeWsContext = { send: (data) => { const isBinary = data instanceof ArrayBuffer || ArrayBuffer.isView(data); const encodedData = isBinary ? _core.toUint8Array.call(void 0, data) : data; const message = { b: { fwm: { wi: open.wi, data: encodedData, binary: isBinary } } }; coordinateDriver.publishToNode(nodeId, message); }, close: (code, reason) => { const message = { b: { fwc: { wi: open.wi, code, reason } } }; coordinateDriver.publishToNode(nodeId, message); } }; globalState.leaderWebSockets = globalState.leaderWebSockets || /* @__PURE__ */ new Map(); globalState.leaderWebSockets.set(open.wi, { wsHandler, wsContext: fakeWsContext, actorId: open.ai }); _chunkXNQTNCJGcjs.logger.call(void 0, ).debug("sending websocket open confirmation to follower", { websocketId: open.wi, nodeId, actorId: open.ai }); const openMessage = { b: { fwo: { wi: open.wi } } }; await coordinateDriver.publishToNode(nodeId, openMessage); _chunkXNQTNCJGcjs.logger.call(void 0, ).debug("websocket open confirmation sent", { websocketId: open.wi }); wsHandler.onOpen({}, fakeWsContext); } catch (error) { _chunkXNQTNCJGcjs.logger.call(void 0, ).warn("failed to open websocket", { error: `${error}` }); const message = { b: { fwc: { wi: open.wi, code: 1011, // Internal error reason: error instanceof Error ? error.message : "Internal server error" } } }; await coordinateDriver.publishToNode(nodeId, message); } } async function handleLeaderWebSocketMessage(globalState, message) { var _a; const wsData = (_a = globalState.leaderWebSockets) == null ? void 0 : _a.get(message.wi); if (!wsData) { _chunkXNQTNCJGcjs.logger.call(void 0, ).warn("received websocket message for nonexistent websocket", { websocketId: message.wi }); return; } const actor = await _chunkXNQTNCJGcjs.ActorPeer.getLeaderActor(globalState, wsData.actorId); if (!actor) { _chunkXNQTNCJGcjs.logger.call(void 0, ).warn("received websocket message for nonexistent actor leader", { actorId: wsData.actorId }); return; } const data = message.binary ? message.data instanceof Uint8Array ? message.data : new Uint8Array( atob(message.data).split("").map((c) => c.charCodeAt(0)) ) : message.data; if (wsData.wsHandler && wsData.wsHandler.onMessage) { wsData.wsHandler.onMessage({ data }, wsData.wsContext); } } async function handleLeaderWebSocketClose(globalState, close) { var _a; const wsData = (_a = globalState.leaderWebSockets) == null ? void 0 : _a.get(close.wi); if (!wsData) { _chunkXNQTNCJGcjs.logger.call(void 0, ).warn("received websocket close for nonexistent websocket", { websocketId: close.wi }); return; } globalState.leaderWebSockets.delete(close.wi); if (wsData.wsHandler && wsData.wsHandler.onClose) { wsData.wsHandler.onClose( { wasClean: true, code: _nullishCoalesce(close.code, () => ( 1005)), reason: _nullishCoalesce(close.reason, () => ( "")) }, wsData.wsContext ); } } // src/coordinate/node/proxy-websocket.ts async function proxyWebSocket(node, c, path, actorId, encoding, connParams, authData) { var _a, _b; const upgradeWebSocket = (_b = (_a = node.runConfig).getUpgradeWebSocket) == null ? void 0 : _b.call(_a); _invariant2.default.call(void 0, upgradeWebSocket, "missing getUpgradeWebSocket"); let clientWs; const relayConn = new RelayConn( node.registryConfig, node.runConfig, node.driverConfig, node.actorDriver, node.inlineClient, node.coordinateDriver, node.globalState, { disconnect: async (reason) => { clientWs == null ? void 0 : clientWs.close(1e3, reason); } }, actorId ); await relayConn.start(); const websocketId = crypto.randomUUID(); return upgradeWebSocket(() => ({ onOpen: (event, ws) => { clientWs = ws; _chunkXNQTNCJGcjs.logger.call(void 0, ).debug("proxy websocket onOpen called", { websocketId, actorId, path }); node.globalState.followerWebSockets = node.globalState.followerWebSockets || /* @__PURE__ */ new Map(); node.globalState.followerWebSockets.set(websocketId, { ws, relayConn }); const openMessage = { b: { lwo: { ai: actorId, wi: websocketId, url: path, e: encoding, cp: connParams, ad: authData } } }; _chunkXNQTNCJGcjs.logger.call(void 0, ).debug("sending websocket open message to leader", { websocketId, actorId }); const _promise = relayConn.publishMessageToleader(openMessage, true); }, onMessage: (event, ws) => { const wsData = node.globalState.followerWebSockets.get(websocketId); if (!wsData) return; if (event.data instanceof ArrayBuffer) { const data = new Uint8Array(event.data); try { const message = { b: { lwm: { wi: websocketId, data, binary: true } } }; const _promise = relayConn.publishMessageToleader(message, false); } catch (error) { if (error instanceof LeaderChangedError) { ws.close(1001, "Actor leader changed"); node.globalState.followerWebSockets.delete(websocketId); } } } else if (event.data instanceof Blob) { event.data.arrayBuffer().then((arrayBuffer) => { const data = new Uint8Array(arrayBuffer); try { const message = { b: { lwm: { wi: websocketId, data, binary: true } } }; const _promise = relayConn.publishMessageToleader(message, false); } catch (error) { if (error instanceof LeaderChangedError) { ws.close(1001, "Actor leader changed"); node.globalState.followerWebSockets.delete(websocketId); } } }).catch((error) => { _chunkXNQTNCJGcjs.logger.call(void 0, ).error("failed to convert blob to arraybuffer", { error }); }); } else { try { const message = { b: { lwm: { wi: websocketId, data: event.data, binary: false } } }; const _promise = relayConn.publishMessageToleader(message, false); } catch (error) { if (error instanceof LeaderChangedError) { ws.close(1001, "Actor leader changed"); node.globalState.followerWebSockets.delete(websocketId); } } } }, onClose: (event, ws) => { var _a2; const wsData = (_a2 = node.globalState.followerWebSockets) == null ? void 0 : _a2.get(websocketId); if (!wsData) return; const _promise = relayConn.disconnect(false, "Client closed WebSocket", { b: { lwc: { wi: websocketId, code: event.code, reason: event.reason } } }); node.globalState.followerWebSockets.delete(websocketId); } }))(c, _core.noopNext.call(void 0, )); } // src/coordinate/node/relay-websocket-adapter.ts var RelayWebSocketAdapter = (_class = class { #node; #websocketId; #relayConn; #readyState = WebSocket.CONNECTING; #eventListeners = /* @__PURE__ */ new Map(); #onopen = null; #onclose = null; #onerror = null; #onmessage = null; #bufferedAmount = 0; #binaryType = "blob"; #extensions = ""; #protocol = ""; #url = ""; #openPromise; #openResolve; // Event buffering is needed since events can be fired // before JavaScript has a chance to add event listeners (e.g. within the same tick) #bufferedEvents = []; constructor(node, websocketId, relayConn) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);_class.prototype.__init4.call(this); this.#node = node; this.#websocketId = websocketId; this.#relayConn = relayConn; this.#openPromise = new Promise((resolve) => { this.#openResolve = resolve; }); this.#node.globalState.relayWebSockets = this.#node.globalState.relayWebSockets || /* @__PURE__ */ new Map(); this.#node.globalState.relayWebSockets.set(websocketId, this); _chunkXNQTNCJGcjs.logger.call(void 0, ).debug("relay websocket adapter registered", { websocketId, nodeId: this.#node.globalState.nodeId, relayWebSocketsSize: this.#node.globalState.relayWebSockets.size }); } get openPromise() { return this.#openPromise; } get readyState() { return this.#readyState; } get bufferedAmount() { return this.#bufferedAmount; } get binaryType() { return this.#binaryType; } set binaryType(value) { this.#binaryType = value; } get extensions() { return this.#extensions; } get protocol() { return this.#protocol; } get url() { return this.#url; } get actorId() { return this.#relayConn.actorId; } get onopen() { return this.#onopen; } set onopen(value) { this.#onopen = value; if (value) { this.#flushBufferedEvents("open"); } } get onclose() { return this.#onclose; } set onclose(value) { this.#onclose = value; if (value) { this.#flushBufferedEvents("close"); } } get onerror() { return this.#onerror; } set onerror(value) { this.#onerror = value; if (value) { this.#flushBufferedEvents("error"); } } get onmessage() { return this.#onmessage; } set onmessage(value) { this.#onmessage = value; if (value) { this.#flushBufferedEvents("message"); } } send(data) { if (this.#readyState !== WebSocket.OPEN) { throw new DOMException("WebSocket is not open"); } let isBinary = false; let messageData; if (typeof data === "string") { messageData = data; } else if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) { isBinary = true; messageData = _core.toUint8Array.call(void 0, data); } else if (data instanceof Blob) { throw new Error("Blob sending not implemented in relay adapter"); } else { throw new Error("Invalid data type"); } const message = { b: { lwm: { wi: this.#websocketId, data: messageData, binary: isBinary } } }; this.#relayConn.publishMessageToleader(message, false).catch((error) => { if (error instanceof LeaderChangedError) { this._handleClose(1001, "Actor leader changed"); } else { const event = new Event("error"); this.#fireEvent("error", event); } }); } close(code, reason) { if (this.#readyState === WebSocket.CLOSING || this.#readyState === WebSocket.CLOSED) { return; } this.#readyState = WebSocket.CLOSING; this.#relayConn.disconnect(false, "Client closed WebSocket", { b: { lwc: { wi: this.#websocketId, code, reason } } }).finally(() => { var _a; this.#readyState = WebSocket.CLOSED; (_a = this.#node.globalState.relayWebSockets) == null ? void 0 : _a.delete( this.#websocketId ); const event = { type: "close", target: this, code: code || 1e3, reason: reason || "", wasClean: true }; this.#fireEvent("close", event); }); } addEventListener(type, listener, options) { if (typeof listener === "function") { let listeners = this.#eventListeners.get(type); if (!listeners) { listeners = /* @__PURE__ */ new Set(); this.#eventListeners.set(type, listeners); } listeners.add(listener); _chunkXNQTNCJGcjs.logger.call(void 0, ).debug(`flushing buffered events for ${type}`, { websocketId: this.#websocketId, bufferedEventsCount: this.#bufferedEvents.filter((e) => e.type === type).length }); this.#flushBufferedEvents(type); } } removeEventListener(type, listener, options) { if (typeof listener === "function") { const listeners = this.#eventListeners.get(type); if (listeners) { listeners.delete(listener); } } } dispatchEvent(event) { return true; } #fireEvent(type, event) { const listeners = this.#eventListeners.get(type); let hasListeners = false; if (listeners && listeners.size > 0) { hasListeners = true; for (const listener of listeners) { try { listener.call(this, event); } catch (error) { _chunkXNQTNCJGcjs.logger.call(void 0, ).error("error in websocket event listener", { error, type }); } } } switch (type) { case "open": if (this.#onopen) { hasListeners = true; try { this.#onopen.call(this, event); } catch (error) { _chunkXNQTNCJGcjs.logger.call(void 0, ).error("error in onopen handler", { error }); } } break; case "close": if (this.#onclose) { hasListeners = true; try { this.#onclose.call(this, event); } catch (error) { _chunkXNQTNCJGcjs.logger.call(void 0, ).error("error in onclose handler", { error }); } } break; case "error": if (this.#onerror) { hasListeners = true; try { this.#onerror.call(this, event); } catch (error) { _chunkXNQTNCJGcjs.logger.call(void 0, ).error("error in onerror handler", { error }); } } break; case "message": if (this.#onmessage) { hasListeners = true; try { this.#onmessage.call(this, event); } catch (error) { _chunkXNQTNCJGcjs.logger.call(void 0, ).error("error in onmessage handler", { error }); } } break; } if (!hasListeners) { _chunkXNQTNCJGcjs.logger.call(void 0, ).debug(`no ${type} listeners registered, buffering event`); this.#bufferedEvents.push({ type, event }); } } #flushBufferedEvents(type) { const eventsToFlush = this.#bufferedEvents.filter( (buffered) => buffered.type === type ); this.#bufferedEvents = this.#bufferedEvents.filter( (buffered) => buffered.type !== type ); for (const { event } of eventsToFlush) { const listeners = this.#eventListeners.get(type); if (listeners) { for (const listener of listeners) { try { listener.call(this, event); } catch (error) { _chunkXNQTNCJGcjs.logger.call(void 0, ).error("error in websocket event listener", { error, type }); } } } } } // Internal method to handle incoming messages from leader _handleMessage(data, isBinary) { if (this.#readyState !== WebSocket.OPEN) { return; } let messageData; if (isBinary) { if (data instanceof Uint8Array) { messageData = data; } else { throw new Error("Binary data must be Uint8Array"); } } else { messageData = data; } const event = new MessageEvent("message", { data: messageData, origin: "", lastEventId: "" }); this.#fireEvent("message", event); } // Internal method to handle open confirmation from leader _handleOpen() { _chunkXNQTNCJGcjs.logger.call(void 0, ).debug("_handleOpen called", { websocketId: this.#websocketId, currentReadyState: this.#readyState, isConnecting: this.#readyState === WebSocket.CONNECTING }); if (this.#readyState !== WebSocket.CONNECTING) { return; } this.#readyState = WebSocket.OPEN; this.#openResolve(); const event = new Event("open"); this.#fireEvent("open", event); } // Internal method to handle close from leader _handleClose(code, reason) { var _a; if (this.#readyState === WebSocket.CLOSED) { return; } this.#readyState = WebSocket.CLOSED; (_a = this.#node.globalState.relayWebSockets) == null ? void 0 : _a.delete(this.#websocketId); const event = { type: "close", target: this, code: code || 1e3, reason: reason || "", wasClean: true }; this.#fireEvent("close", event); } // Required WebSocket constants static __initStatic() {this.CONNECTING = 0} static __initStatic2() {this.OPEN = 1} static __initStatic3() {this.CLOSING = 2} static __initStatic4() {this.CLOSED = 3} // Instance constants __init() {this.CONNECTING = 0} __init2() {this.OPEN = 1} __init3() {this.CLOSING = 2} __init4() {this.CLOSED = 3} }, _class.__initStatic(), _class.__initStatic2(), _class.__initStatic3(), _class.__initStatic4(), _class); // src/coordinate/node/mod.ts var Node = class { #registryConfig; #runConfig; #driverConfig; #coordinateDriver; #globalState; #inlineClient; #actorDriver; #actorRouter; get inlineClient() { return this.#inlineClient; } get actorDriver() { return this.#actorDriver; } constructor(registryConfig, runConfig, driverConfig, managerDriver, coordinateDriver, globalState, inlineClient, actorDriver, actorRouter) { this.#registryConfig = registryConfig; this.#runConfig = runConfig; this.#driverConfig = driverConfig; this.#coordinateDriver = coordinateDriver; this.#globalState = globalState; this.#inlineClient = inlineClient; this.#actorDriver = actorDriver; this.#actorRouter = actorRouter; } get globalState() { return this.#globalState; } get coordinateDriver() { return this.#coordinateDriver; } get registryConfig() { return this.#registryConfig; } get runConfig() { return this.#runConfig; } get driverConfig() { return this.#driverConfig; } async start() { _chunkXNQTNCJGcjs.logger.call(void 0, ).debug("starting", { nodeId: this.#globalState.nodeId }); await this.#coordinateDriver.createNodeSubscriber( this.#globalState.nodeId, this.#onMessage.bind(this) ); _chunkXNQTNCJGcjs.logger.call(void 0, ).debug("node started", { nodeId: this.#globalState.nodeId }); } async #onMessage(data) { const shouldAck = !!(data.n && data.m); _chunkXNQTNCJGcjs.logger.call(void 0, ).debug("node received message", { data, shouldAck }); if (shouldAck) { _invariant2.default.call(void 0, data.n && data.m, "unreachable"); if ("a" in data.b) { throw new Error("Ack messages cannot request ack in response"); } const messageRaw = { b: { a: { m: data.m } } }; this.#coordinateDriver.publishToNode(data.n, messageRaw); } if ("a" in data.b) { await this.#onAck(data.b.a); } else if ("lf" in data.b) { await handleLeaderFetch( this.#globalState, this.#coordinateDriver, this.#actorRouter, data.n, data.b.lf ); } else if ("ffr" in data.b) { handleFollowerFetchResponse(this.#globalState, data.b.ffr); } else if ("lwo" in data.b) { _chunkXNQTNCJGcjs.logger.call(void 0, ).debug("received lwo (leader websocket open) message", { websocketId: data.b.lwo.wi, actorId: data.b.lwo.ai, fromNodeId: data.n }); await handleLeaderWebSocketOpen( this.#globalState, this.#coordinateDriver, this.#runConfig, this.#actorDriver, data.n, data.b.lwo ); } else if ("lwm" in data.b) { await handleLeaderWebSocketMessage(this.#globalState, data.b.lwm); } else if ("lwc" in data.b) { await handleLeaderWebSocketClose(this.#globalState, data.b.lwc); } else if ("fwo" in data.b) { _chunkXNQTNCJGcjs.logger.call(void 0, ).debug("received fwo (follower websocket open) message", { websocketId: data.b.fwo.wi }); await handleFollowerWebSocketOpen(this.#globalState, data.b.fwo); } else if ("fwm" in data.b) { await handleFollowerWebSocketMessage(this.#globalState, data.b.fwm); } else if ("fwc" in data.b) { await handleFollowerWebSocketClose(this.#globalState, data.b.fwc); } else { _chunkRIAC4EUGcjs.assertUnreachable.call(void 0, data.b); } } async #onAck({ m: messageId }) { const resolveAck = this.#globalState.messageAckResolvers.get(messageId); if (resolveAck) { resolveAck(); this.#globalState.messageAckResolvers.delete(messageId); } else { _chunkXNQTNCJGcjs.logger.call(void 0, ).warn("missing ack resolver", { messageId }); } } async sendRequest(actorId, actorRequest, abortController) { const requestId = crypto.randomUUID(); const url = new URL(actorRequest.url); const headers = {}; actorRequest.headers.forEach((value, key) => { headers[key] = value; }); let body; if (actorRequest.body) { const buffer = await actorRequest.arrayBuffer(); body = new Uint8Array(buffer); } const responsePromise = new Promise((resolve) => { this.#globalState.fetchResponseResolvers.set(requestId, resolve); }); const relayConn = new RelayConn( this.#registryConfig, this.#runConfig, this.#driverConfig, this.#actorDriver, this.#inlineClient, this.#coordinateDriver, this.#globalState, { disconnect: async (_reason) => { } }, actorId ); await relayConn.start(); try { const message = { b: { lf: { ri: requestId, ai: actorId, method: actorRequest.method, url: url.pathname + url.search, headers, body, // TODO: Auth data ad: void 0 } } }; await relayConn.publishMessageToleader(message, true); } catch (error) { this.#globalState.fetchResponseResolvers.delete(requestId); if (error instanceof Error) { return new Response(error.message, { status: 503 }); } return new Response( "Service unavailable (cannot send message to actor leader)", { status: 503 } ); } const response = await responsePromise.finally(() => { this.#globalState.fetchResponseResolvers.delete(requestId); }); if (response.error) { return new Response(response.error, { status: response.status, headers: response.headers }); } const responseBody = response.body; return new Response(responseBody, { status: response.status, headers: response.headers }); } // TODO: Clean up disconnecting logic for websocket. There might be missed edge conditions depending on if client or server terminates the websocket async openWebSocket(path, actorId, encoding, connParams) { const websocketId = crypto.randomUUID(); _chunkXNQTNCJGcjs.logger.call(void 0, ).debug("opening websocket for inline client", { websocketId, actorId, path, encoding, nodeId: this.#globalState.nodeId }); const relayConn = new RelayConn( this.#registryConfig, this.#runConfig, this.#driverConfig, this.#actorDriver, this.#inlineClient, this.#coordinateDriver, this.#globalState, { disconnect: async (_reason) => { } }, actorId ); await relayConn.start(); const adapter = new RelayWebSocketAdapter(this, websocketId, relayConn); this.#globalState.relayWebSockets.set(websocketId, adapter); const openMessage = { b: { lwo: { ai: actorId, wi: websocketId, url: path, e: encoding, cp: connParams, ad: void 0 } } }; await relayConn.publishMessageToleader(openMessage, true); _chunkXNQTNCJGcjs.logger.call(void 0, ).debug("websocket adapter created, waiting for open", { websocketId }); _chunkXNQTNCJGcjs.logger.call(void 0, ).debug("waiting for websocket adapter open promise", { websocketId, actorId, path, encoding, adapterReadyState: adapter.readyState }); await adapter.openPromise; _chunkXNQTNCJGcjs.logger.call(void 0, ).debug("websocket adapter open promise resolved", { websocketId, actorId, adapterReadyState: adapter.readyState }); _chunkXNQTNCJGcjs.logger.call(void 0, ).debug("websocket adapter ready", { websocketId }); return adapter; } // TODO: Implement abort controller async proxyRequest(c, actorRequest, actorId) { return await this.sendRequest(actorId, actorRequest); } async proxyWebSocket(c, path, actorId, encoding, connParams, authData) { return proxyWebSocket( this, c, path, actorId, encoding, connParams, authData ); } }; // src/mod.ts function createRedisDriver(options) { var _a, _b, _c, _d, _e; const driverConfig = RedisDriverConfig.parse({ ...options, actorPeer: { ...options == null ? void 0 : options.actorPeer, leaseDuration: _nullishCoalesce(((_a = options == null ? void 0 : options.actorPeer) == null ? void 0 : _a.leaseDuration), () => ( 3e3)), renewLeaseGrace: _nullishCoalesce(((_b = options == null ? void 0 : options.actorPeer) == null ? void 0 : _b.renewLeaseGrace), () => ( 1500)), checkLeaseInterval: _nullishCoalesce(((_c = options == null ? void 0 : options.actorPeer) == null ? void 0 : _c.checkLeaseInterval), () => ( 1e3)), checkLeaseJitter: _nullishCoalesce(((_d = options == null ? void 0 : options.actorPeer) == null ? void 0 : _d.checkLeaseJitter), () => ( 500)), messageAckTimeout: _nullishCoalesce(((_e = options == null ? void 0 : options.actorPeer) == null ? void 0 : _e.messageAckTimeout), () => ( 1e3)) } }); const globalState = { nodeId: crypto.randomUUID(), actorPeers: /* @__PURE__ */ new Map(), relayConns: /* @__PURE__ */ new Map(), messageAckResolvers: /* @__PURE__ */ new Map(), actionResponseResolvers: /* @__PURE__ */ new Map(), fetchResponseResolvers: /* @__PURE__ */ new Map(), rawWebSockets: /* @__PURE__ */ new Map(), followerWebSockets: /* @__PURE__ */ new Map(), relayWebSockets: /* @__PURE__ */ new Map() }; const coordinate = new RedisCoordinateDriver( driverConfig, driverConfig.redis ); return { name: "redis", manager: (registryConfig, runConfig) => { const manager = new (0, _chunkKFGA2UFQcjs.RedisManagerDriver)( registryConfig, driverConfig, driverConfig.redis ); const inlineClient = _core.createClientWithDriver.call(void 0, _core.createInlineClientDriver.call(void 0, manager) ); const actorDriver = new (0, _chunkXNQTNCJGcjs.RedisActorDriver)( globalState, driverConfig.redis, driverConfig ); const actorRouter = _core.createActorRouter.call(void 0, runConfig, actorDriver); const node = new Node( registryConfig, runConfig, driverConfig, manager, coordinate, globalState, inlineClient, actorDriver, actorRouter ); manager.node = node; node.start(); return manager; }, actor: (registryConfig, runConfig, managerDriver, inlineC