@rivetkit/redis
Version:
_Lightweight Libraries for Backends_
527 lines (521 loc) • 14.7 kB
JavaScript
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