UNPKG

@rivetkit/redis

Version:

_Lightweight Libraries for Backends_

527 lines (521 loc) 14.7 kB
import { ActorAlreadyExists, ConnStateNotEnabled, KEYS, Unreachable, getEnvUniversal, getLogger, logger } from "./chunk-CGJPXOT3.js"; // ../../core/dist/chunk-54HA33U4.js import * as cbor from "cbor-x"; import { z } from "zod"; import { streamSSE } from "hono/streaming"; import { z as z2 } from "zod"; import { z as z4 } from "zod"; import { z as z3 } from "zod"; var RUNTIME_LOGGER_NAME = "actor-runtime"; function logger2() { return getLogger(RUNTIME_LOGGER_NAME); } function assertUnreachable(x) { logger2().error("unreachable", { value: `${x}`, stack: new Error().stack }); throw new Unreachable(x); } var EncodingSchema = 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 { 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. */ __persist; /** * 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) { this.#actor = actor; this.__persist = persist; this.#driver = driver; this.#stateEnabled = stateEnabled; } #validateStateEnabled() { if (!this.#stateEnabled) { throw new 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 }; } }; var ActionRequestSchema = z2.object({ // Args a: z2.array(z2.unknown()) }); var ActionResponseSchema = z2.object({ // Output o: z2.unknown() }); var ActionRequestSchema2 = z3.object({ // ID i: z3.number().int(), // Name n: z3.string(), // Args a: z3.array(z3.unknown()) }); var SubscriptionRequestSchema = z3.object({ // Event name e: z3.string(), // Subscribe s: z3.boolean() }); var ToServerSchema = z3.object({ // Body b: z3.union([ z3.object({ ar: ActionRequestSchema2 }), z3.object({ sr: SubscriptionRequestSchema }) ]) }); var TransportSchema = z4.enum(["websocket", "sse"]); var HEADER_ACTOR_QUERY = "X-RivetKit-Query"; // ../../core/dist/chunk-4J2FQLTP.js import { z as z22 } from "zod"; import { z as z5 } from "zod"; import * as cbor2 from "cbor-x"; var defaultTokenFn = () => { const envToken = getEnvUniversal("RIVETKIT_INSPECTOR_TOKEN"); if (envToken) { return envToken; } return ""; }; var defaultEnabled = () => { return getEnvUniversal("NODE_ENV") !== "production" || !getEnvUniversal("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 = z5.object({ enabled: z5.boolean().optional().default(defaultEnabled), /** CORS configuration for the router. Uses Hono's CORS middleware options. */ cors: z5.custom().optional().default(() => defaultCors), /** * Token used to access the Inspector. */ token: z5.function().returns(z5.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: z5.string().optional() }).optional().default(() => ({ enabled: defaultEnabled(), token: defaultTokenFn, cors: defaultCors })); var DriverConfigSchema = z22.object({ /** Machine-readable name to identify this driver by. */ name: z22.string(), manager: z22.custom(), actor: z22.custom() }); var RunConfigSchema = z22.object({ driver: DriverConfigSchema.optional(), /** Endpoint to connect to the Rivet engine. Can be configured via RIVET_ENGINE env var. */ engine: z22.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: z22.custom().optional(), role: z22.enum(["all", "server", "runner"]).optional().default("all"), /** CORS configuration for the router. Uses Hono's CORS middleware options. */ cors: z22.custom().optional(), maxIncomingMessageSize: z22.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: z22.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 import * as cbor3 from "cbor-x"; import invariant from "invariant"; // src/utils.ts import * as crypto2 from "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() { invariant(this.#node, "node should exist"); return this.#node; } set node(node) { invariant(!this.#node, "node cannot be set twice"); this.#node = node; } async getForId({ actorId }) { const metadataRaw = await this.#redis.getBuffer( 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 = 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( KEYS.actorByKey(this.#driverConfig.keyPrefix, name, key), actorId ); pipeline.setnx( KEYS.ACTOR.metadata(this.#driverConfig.keyPrefix, actorId), cbor3.encode({ name, key }) ); pipeline.setnx( 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]; invariant( metadataCreated === keyCreated, "metadataCreated inconsistent with keyCreated" ); invariant( persistedDataCreated === keyCreated, "persistedDataCreated inconsistent with keyCreated" ); if (keyCreated === 1) { logger().debug("actor created", { actorId }); } else { logger().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( KEYS.actorByKey(this.#driverConfig.keyPrefix, name, key), actorId ); pipeline.setnx( KEYS.ACTOR.metadata(this.#driverConfig.keyPrefix, actorId), cbor3.encode({ name, key }) ); pipeline.setnx( 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]; invariant( metadataResult === keyResult, "metadataResult inconsistent with keyResult" ); invariant( persistedDataResult === keyResult, "metadataResult inconsistent with keyResult" ); if (keyResult === 0) { throw new ActorAlreadyExists(name, key); } return { actorId, name, key }; } async sendRequest(actorId, actorRequest) { return await this.#node.sendRequest(actorId, actorRequest); } async openWebSocket(path, actorId, encoding, connParams) { logger().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 ); } }; export { RedisManagerDriver }; //# sourceMappingURL=chunk-Q35KFYYS.js.map