UNPKG

@rivetkit/redis

Version:

_Lightweight Libraries for Backends_

527 lines (504 loc) 15.8 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 }; } var _class; var _chunkVQDBJLFCcjs = require('./chunk-VQDBJLFC.cjs'); // ../../core/dist/chunk-54HA33U4.js var _cborx = require('cbor-x'); var cbor = _interopRequireWildcard(_cborx); var cbor2 = _interopRequireWildcard(_cborx); var cbor3 = _interopRequireWildcard(_cborx); var _zod = require('zod'); var _streaming = require('hono/streaming'); var RUNTIME_LOGGER_NAME = "actor-runtime"; function logger2() { return _chunkVQDBJLFCcjs.getLogger.call(void 0, RUNTIME_LOGGER_NAME); } function assertUnreachable(x) { logger2().error("unreachable", { value: `${x}`, stack: new Error().stack }); throw new (0, _chunkVQDBJLFCcjs.Unreachable)(x); } var EncodingSchema = _zod.z.enum(["json", "cbor"]); var CachedSerializer = class { #data; #cache = /* @__PURE__ */ new Map(); constructor(data) { this.#data = data; } get rawData() { return this.#data; } serialize(encoding) { const cached = this.#cache.get(encoding); if (cached) { return cached; } else { const serialized = serialize(this.#data, encoding); this.#cache.set(encoding, serialized); return serialized; } } }; function serialize(value, encoding) { if (encoding === "json") { return JSON.stringify(value); } else if (encoding === "cbor") { const cleanValue = JSON.parse(JSON.stringify(value)); return cbor.encode(cleanValue); } else { assertUnreachable(encoding); } } var CONNECTION_CHECK_LIVENESS_SYMBOL = Symbol("checkLiveness"); var Conn = (_class = class { __init() {this.subscriptions = /* @__PURE__ */ new Set()} #stateEnabled; // TODO: Remove this cyclical reference #actor; #status = "connected"; /** * The proxied state that notifies of changes automatically. * * Any data that should be stored indefinitely should be held within this object. */ /** * Driver used to manage realtime connection communication. * * @protected */ #driver; get params() { return this.__persist.p; } get auth() { return this.__persist.a; } get driver() { return this.__persist.d; } get _stateEnabled() { return this.#stateEnabled; } /** * Gets the current state of the connection. * * Throws an error if the state is not enabled. */ get state() { this.#validateStateEnabled(); if (!this.__persist.s) throw new Error("state should exists"); return this.__persist.s; } /** * Sets the state of the connection. * * Throws an error if the state is not enabled. */ set state(value) { this.#validateStateEnabled(); this.__persist.s = value; } /** * Unique identifier for the connection. */ get id() { return this.__persist.i; } /** * Token used to authenticate this request. */ get _token() { return this.__persist.t; } /** * Status of the connection. */ get status() { return this.#status; } /** * Timestamp of the last time the connection was seen, i.e. the last time the connection was active and checked for liveness. */ get lastSeen() { return this.__persist.l; } /** * Initializes a new instance of the Connection class. * * This should only be constructed by {@link Actor}. * * @protected */ constructor(actor, persist, driver, stateEnabled) {;_class.prototype.__init.call(this); this.#actor = actor; this.__persist = persist; this.#driver = driver; this.#stateEnabled = stateEnabled; } #validateStateEnabled() { if (!this.#stateEnabled) { throw new (0, _chunkVQDBJLFCcjs.ConnStateNotEnabled)(); } } /** * Sends a WebSocket message to the client. * * @param message - The message to send. * * @protected */ _sendMessage(message) { var _a, _b; (_b = (_a = this.#driver).sendMessage) == null ? void 0 : _b.call(_a, this.#actor, this, this.__persist.ds, message); } /** * Sends an event with arguments to the client. * * @param eventName - The name of the event. * @param args - The arguments for the event. * @see {@link https://rivet.gg/docs/events|Events Documentation} */ send(eventName, ...args) { this.#actor.inspector.emitter.emit("eventFired", { type: "event", eventName, args, connId: this.id }); this._sendMessage( new CachedSerializer({ b: { ev: { n: eventName, a: args } } }) ); } /** * Disconnects the client with an optional reason. * * @param reason - The reason for disconnection. */ async disconnect(reason) { this.#status = "reconnecting"; await this.#driver.disconnect(this.#actor, this, this.__persist.ds, reason); } /** * This method checks the connection's liveness by querying the driver for its ready state. * If the connection is not closed, it updates the last liveness timestamp and returns `true`. * Otherwise, it returns `false`. * @internal */ [CONNECTION_CHECK_LIVENESS_SYMBOL]() { var _a, _b; const readyState = (_b = (_a = this.#driver).getConnectionReadyState) == null ? void 0 : _b.call( _a, this.#actor, this ); const isConnectionClosed = readyState === 3 || readyState === 2 || readyState === void 0; const newLastSeen = Date.now(); const newStatus = isConnectionClosed ? "reconnecting" : "connected"; logger2().debug("liveness probe for connection", { connId: this.id, actorId: this.#actor.id, readyState, status: this.#status, newStatus, lastSeen: this.__persist.l, currentTs: newLastSeen }); if (!isConnectionClosed) { this.__persist.l = newLastSeen; } this.#status = newStatus; return { status: this.#status, lastSeen: this.__persist.l }; } }, _class); var ActionRequestSchema = _zod.z.object({ // Args a: _zod.z.array(_zod.z.unknown()) }); var ActionResponseSchema = _zod.z.object({ // Output o: _zod.z.unknown() }); var ActionRequestSchema2 = _zod.z.object({ // ID i: _zod.z.number().int(), // Name n: _zod.z.string(), // Args a: _zod.z.array(_zod.z.unknown()) }); var SubscriptionRequestSchema = _zod.z.object({ // Event name e: _zod.z.string(), // Subscribe s: _zod.z.boolean() }); var ToServerSchema = _zod.z.object({ // Body b: _zod.z.union([ _zod.z.object({ ar: ActionRequestSchema2 }), _zod.z.object({ sr: SubscriptionRequestSchema }) ]) }); var TransportSchema = _zod.z.enum(["websocket", "sse"]); var HEADER_ACTOR_QUERY = "X-RivetKit-Query"; // ../../core/dist/chunk-4J2FQLTP.js var defaultTokenFn = () => { const envToken = _chunkVQDBJLFCcjs.getEnvUniversal.call(void 0, "RIVETKIT_INSPECTOR_TOKEN"); if (envToken) { return envToken; } return ""; }; var defaultEnabled = () => { return _chunkVQDBJLFCcjs.getEnvUniversal.call(void 0, "NODE_ENV") !== "production" || !_chunkVQDBJLFCcjs.getEnvUniversal.call(void 0, "RIVETKIT_INSPECTOR_DISABLE"); }; var defaultInspectorOrigins = [ "http://localhost:43708", "https://studio.rivet.gg" ]; var defaultCors = { origin: (origin) => { if (defaultInspectorOrigins.includes(origin) || origin.startsWith("https://") && origin.endsWith("rivet-gg.vercel.app")) { return origin; } else { return null; } }, allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], allowHeaders: [ "Content-Type", "Authorization", HEADER_ACTOR_QUERY, "last-event-id" ], maxAge: 3600, credentials: true }; var InspectorConfigSchema = _zod.z.object({ enabled: _zod.z.boolean().optional().default(defaultEnabled), /** CORS configuration for the router. Uses Hono's CORS middleware options. */ cors: _zod.z.custom().optional().default(() => defaultCors), /** * Token used to access the Inspector. */ token: _zod.z.function().returns(_zod.z.string()).optional().default(() => defaultTokenFn), /** * Default RivetKit server endpoint for Rivet Inspector to connect to. This should be the same endpoint as what you use for your Rivet client to connect to RivetKit. * * This is a convenience property just for printing out the inspector URL. */ defaultEndpoint: _zod.z.string().optional() }).optional().default(() => ({ enabled: defaultEnabled(), token: defaultTokenFn, cors: defaultCors })); var DriverConfigSchema = _zod.z.object({ /** Machine-readable name to identify this driver by. */ name: _zod.z.string(), manager: _zod.z.custom(), actor: _zod.z.custom() }); var RunConfigSchema = _zod.z.object({ driver: DriverConfigSchema.optional(), /** Endpoint to connect to the Rivet engine. Can be configured via RIVET_ENGINE env var. */ engine: _zod.z.string().optional(), // This is a function to allow for lazy configuration of upgradeWebSocket on the // fly. This is required since the dependencies that profie upgradeWebSocket // (specifically Node.js) can sometimes only be specified after the router is // created or must be imported async using `await import(...)` getUpgradeWebSocket: _zod.z.custom().optional(), role: _zod.z.enum(["all", "server", "runner"]).optional().default("all"), /** CORS configuration for the router. Uses Hono's CORS middleware options. */ cors: _zod.z.custom().optional(), maxIncomingMessageSize: _zod.z.number().optional().default(65536), inspector: InspectorConfigSchema, /** * Base path for the router. This is used to prefix all routes. * For example, if the base path is `/api`, then the route `/actors` will be * available at `/api/actors`. */ basePath: _zod.z.string().optional().default("/") }).default({}); function serializeEmptyPersistData(input) { const persistData = { i: input, hi: false, s: void 0, c: [], e: [] }; return cbor2.encode(persistData); } // src/manager.ts var _invariant = require('invariant'); var _invariant2 = _interopRequireDefault(_invariant); // src/utils.ts var _crypto = require('crypto'); var crypto2 = _interopRequireWildcard(_crypto); function generateActorId(name, key) { const jsonString = JSON.stringify([name, key]); const hash = crypto2.createHash("sha256").update(jsonString).digest("hex").substring(0, 16); return hash; } // src/manager.ts var RedisManagerDriver = class { #registryConfig; #driverConfig; #redis; #node; // inspector: ManagerInspector = new ManagerInspector(this, { // getAllActors: () => this.#state.getAllActors(), // getAllTypesOfActors: () => Object.keys(this.registry.config.actors), // }); constructor(registryConfig, driverConfig, redis) { this.#registryConfig = registryConfig; this.#driverConfig = driverConfig; this.#redis = redis; } get node() { _invariant2.default.call(void 0, this.#node, "node should exist"); return this.#node; } set node(node) { _invariant2.default.call(void 0, !this.#node, "node cannot be set twice"); this.#node = node; } async getForId({ actorId }) { const metadataRaw = await this.#redis.getBuffer( _chunkVQDBJLFCcjs.KEYS.ACTOR.metadata(this.#driverConfig.keyPrefix, actorId) ); if (!metadataRaw) { return void 0; } const metadata = cbor3.decode(metadataRaw); const { name, key } = metadata; return { actorId, name, key }; } async getWithKey({ name, key }) { const lookupKey = _chunkVQDBJLFCcjs.KEYS.actorByKey(this.#driverConfig.keyPrefix, name, key); const actorId = await this.#redis.get(lookupKey); if (!actorId) { return void 0; } return this.getForId({ name, actorId }); } async getOrCreateWithKey(input) { var _a, _b, _c; const { name, key } = input; const actorId = generateActorId(input.name, input.key); const pipeline = this.#redis.multi(); pipeline.setnx( _chunkVQDBJLFCcjs.KEYS.actorByKey(this.#driverConfig.keyPrefix, name, key), actorId ); pipeline.setnx( _chunkVQDBJLFCcjs.KEYS.ACTOR.metadata(this.#driverConfig.keyPrefix, actorId), cbor3.encode({ name, key }) ); pipeline.setnx( _chunkVQDBJLFCcjs.KEYS.ACTOR.persistedData(this.#driverConfig.keyPrefix, actorId), Buffer.from(serializeEmptyPersistData(input.input)) ); const results = await pipeline.exec(); if (!results) { throw new Error("redis pipeline execution failed"); } const keyCreated = (_a = results[0]) == null ? void 0 : _a[1]; const metadataCreated = (_b = results[1]) == null ? void 0 : _b[1]; const persistedDataCreated = (_c = results[2]) == null ? void 0 : _c[1]; _invariant2.default.call(void 0, metadataCreated === keyCreated, "metadataCreated inconsistent with keyCreated" ); _invariant2.default.call(void 0, persistedDataCreated === keyCreated, "persistedDataCreated inconsistent with keyCreated" ); if (keyCreated === 1) { _chunkVQDBJLFCcjs.logger.call(void 0, ).debug("actor created", { actorId }); } else { _chunkVQDBJLFCcjs.logger.call(void 0, ).debug("actor already exists", { actorId }); } return { actorId, name: input.name, key: input.key }; } async createActor({ name, key, input }) { var _a, _b, _c; const actorId = generateActorId(name, key); const pipeline = this.#redis.multi(); pipeline.setnx( _chunkVQDBJLFCcjs.KEYS.actorByKey(this.#driverConfig.keyPrefix, name, key), actorId ); pipeline.setnx( _chunkVQDBJLFCcjs.KEYS.ACTOR.metadata(this.#driverConfig.keyPrefix, actorId), cbor3.encode({ name, key }) ); pipeline.setnx( _chunkVQDBJLFCcjs.KEYS.ACTOR.persistedData(this.#driverConfig.keyPrefix, actorId), Buffer.from(serializeEmptyPersistData(input)) ); const results = await pipeline.exec(); if (!results) { throw new Error("redis pipeline execution failed"); } const keyResult = (_a = results[0]) == null ? void 0 : _a[1]; const metadataResult = (_b = results[1]) == null ? void 0 : _b[1]; const persistedDataResult = (_c = results[2]) == null ? void 0 : _c[1]; _invariant2.default.call(void 0, metadataResult === keyResult, "metadataResult inconsistent with keyResult" ); _invariant2.default.call(void 0, persistedDataResult === keyResult, "metadataResult inconsistent with keyResult" ); if (keyResult === 0) { throw new (0, _chunkVQDBJLFCcjs.ActorAlreadyExists)(name, key); } return { actorId, name, key }; } async sendRequest(actorId, actorRequest) { return await this.#node.sendRequest(actorId, actorRequest); } async openWebSocket(path, actorId, encoding, connParams) { _chunkVQDBJLFCcjs.logger.call(void 0, ).debug("RedisManagerDriver.openWebSocket called", { path, actorId, encoding }); return await this.#node.openWebSocket(path, actorId, encoding, connParams); } async proxyRequest(c, actorRequest, actorId) { return await this.#node.proxyRequest(c, actorRequest, actorId); } async proxyWebSocket(c, path, actorId, encoding, connParams, authData) { return await this.#node.proxyWebSocket( c, path, actorId, encoding, connParams, authData ); } }; exports.RedisManagerDriver = RedisManagerDriver; //# sourceMappingURL=chunk-E7IBW7FC.cjs.map