UNPKG

@rivetkit/redis

Version:

_Lightweight Libraries for Backends_

250 lines (221 loc) 6.06 kB
import { type ActorRouter, createActorRouter, createClientWithDriver, createInlineClientDriver, type Encoding, type RegistryConfig, type RunConfig, } from "@rivetkit/core"; import { type ActorDriver, type ActorOutput, type CreateInput, type GetForIdInput, type GetOrCreateWithKeyInput, type GetWithKeyInput, type ManagerDriver, serializeEmptyPersistData, } from "@rivetkit/core/driver-helpers"; import { ActorAlreadyExists } from "@rivetkit/core/errors"; import * as cbor from "cbor-x"; import type { Context as HonoContext } from "hono"; import invariant from "invariant"; import type Redis from "ioredis"; import type { RedisDriverConfig } from "./config"; import type { Node } from "./coordinate/node/mod"; import { KEYS } from "./keys"; import { logger } from "./log"; import { generateActorId } from "./utils"; export class RedisManagerDriver implements ManagerDriver { #registryConfig: RegistryConfig; #driverConfig: RedisDriverConfig; #redis: Redis; #node!: Node; // inspector: ManagerInspector = new ManagerInspector(this, { // getAllActors: () => this.#state.getAllActors(), // getAllTypesOfActors: () => Object.keys(this.registry.config.actors), // }); constructor( registryConfig: RegistryConfig, driverConfig: RedisDriverConfig, redis: Redis, ) { this.#registryConfig = registryConfig; this.#driverConfig = driverConfig; this.#redis = redis; } get node(): Node { invariant(this.#node, "node should exist"); return this.#node; } set node(node: Node) { invariant(!this.#node, "node cannot be set twice"); this.#node = node; } async getForId({ actorId }: GetForIdInput): Promise<ActorOutput | undefined> { // Get metadata from Redis const metadataRaw = await this.#redis.getBuffer( KEYS.ACTOR.metadata(this.#driverConfig.keyPrefix, actorId), ); // If the actor doesn't exist, return undefined if (!metadataRaw) { return undefined; } const metadata = cbor.decode(metadataRaw); const { name, key } = metadata; return { actorId, name, key, }; } async getWithKey({ name, key, }: GetWithKeyInput): Promise<ActorOutput | undefined> { // Since keys are 1:1 with actor IDs, we can directly look up by key const lookupKey = KEYS.actorByKey(this.#driverConfig.keyPrefix, name, key); const actorId = await this.#redis.get(lookupKey); if (!actorId) { return undefined; } return this.getForId({ name, actorId }); } async getOrCreateWithKey( input: GetOrCreateWithKeyInput, ): Promise<ActorOutput> { const { name, key } = input; const actorId = generateActorId(input.name, input.key); // Write actor const pipeline = this.#redis.multi(); pipeline.setnx( KEYS.actorByKey(this.#driverConfig.keyPrefix, name, key), actorId, ); pipeline.setnx( KEYS.ACTOR.metadata(this.#driverConfig.keyPrefix, actorId), cbor.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 = results[0]?.[1] as number; const metadataCreated = results[1]?.[1] as number; const persistedDataCreated = results[2]?.[1] as number; invariant( metadataCreated === keyCreated, "metadataCreated inconsistent with keyCreated", ); invariant( persistedDataCreated === keyCreated, "persistedDataCreated inconsistent with keyCreated", ); // If we created the actor, we have the metadata 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 }: CreateInput): Promise<ActorOutput> { const actorId = generateActorId(name, key); // Write actor const pipeline = this.#redis.multi(); pipeline.setnx( KEYS.actorByKey(this.#driverConfig.keyPrefix, name, key), actorId, ); pipeline.setnx( KEYS.ACTOR.metadata(this.#driverConfig.keyPrefix, actorId), cbor.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"); } // Check all SETNX results const keyResult = results[0]?.[1]; const metadataResult = results[1]?.[1]; const persistedDataResult = results[2]?.[1]; invariant( metadataResult === keyResult, "metadataResult inconsistent with keyResult", ); invariant( persistedDataResult === keyResult, "metadataResult inconsistent with keyResult", ); // If the actor key already existed, it's an error if (keyResult === 0) { throw new ActorAlreadyExists(name, key); } // Notify inspector of actor creation // this.inspector.onActorsChange([ // { // id: actorId, // name, // key, // }, // ]); return { actorId, name, key, }; } async sendRequest(actorId: string, actorRequest: Request): Promise<Response> { return await this.#node.sendRequest(actorId, actorRequest); } async openWebSocket( path: string, actorId: string, encoding: Encoding, connParams: unknown, ): Promise<WebSocket> { logger().debug("RedisManagerDriver.openWebSocket called", { path, actorId, encoding, }); return await this.#node.openWebSocket(path, actorId, encoding, connParams); } async proxyRequest( c: HonoContext, actorRequest: Request, actorId: string, ): Promise<Response> { return await this.#node.proxyRequest(c, actorRequest, actorId); } async proxyWebSocket( c: HonoContext, path: string, actorId: string, encoding: Encoding, connParams: unknown, authData: unknown, ): Promise<Response> { return await this.#node.proxyWebSocket( c, path, actorId, encoding, connParams, authData, ); } }