This release is 1 version behind 24.6.2 — the latest version of @rivet-gg/actor-client. Jump to latest
@rivet-gg/actor-client@24.6.1-rc.2
🔧 Rivet Actors have built-in RPC, state, and events — the easiest way to build modern applications.
This package works with Cloudflare Workers, Node.js, Deno, Bun, Browsers




JSR Score
70%
Published
a month ago (24.6.1-rc.2)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456import type { ActorTags } from "../common/utils.ts"; import type { ActorsRequest, ActorsResponse, RivetConfigResponse, } from "../manager-protocol/mod.ts"; import type { CreateRequest } from "../manager-protocol/query.ts"; import type { ProtocolFormat } from "../protocol/ws/mod.ts"; import * as errors from "./errors.ts"; import { ActorHandleRaw } from "./handle.ts"; import { logger } from "./log.ts"; /** * Options for configuring the client. * @typedef {Object} ClientOptions * @property {ProtocolFormat} [protocolFormat] - The format used for protocol communication. */ export interface ClientOptions { protocolFormat?: ProtocolFormat; } /** * Options for querying actors. * @typedef {Object} QueryOptions * @property {unknown} [parameters] - Parameters to pass to the connection. */ export interface QueryOptions { /** Parameters to pass to the connection. */ parameters?: unknown; } /** * Options for getting an actor by ID. * @typedef {QueryOptions} GetWithIdOptions */ export interface GetWithIdOptions extends QueryOptions {} /** * Options for getting an actor. * @typedef {QueryOptions} GetOptions * @property {boolean} [noCreate] - Prevents creating a new actor if one does not exist. * @property {Partial<CreateRequest>} [create] - Config used to create the actor. */ export interface GetOptions extends QueryOptions { /** Prevents creating a new actor if one does not exist. */ noCreate?: boolean; /** Config used to create the actor. */ create?: Partial<CreateRequest>; } /** * Options for creating an actor. * @typedef {QueryOptions} CreateOptions * @property {CreateRequest} create - Config used to create the actor. */ export interface CreateOptions extends QueryOptions { /** Config used to create the actor. */ create: CreateRequest; } /** * Connection to an actor. Allows calling actor's remote procedure calls with inferred types. See {@link ActorHandleRaw} for underlying methods. * * @example * ``` * const room = await client.get<ChatRoom>(...etc...); * // This calls the rpc named `sendMessage` on the `ChatRoom` actor. * await room.sendMessage('Hello, world!'); * ``` * * Private methods (e.g. those starting with `_`) are automatically excluded. * * @template A The actor class that this handle is connected to. * @see {@link ActorHandleRaw} */ export type ActorHandle<A = unknown> = ActorHandleRaw & { [K in keyof A as K extends string ? K extends `_${string}` ? never : K : K]: A[K] extends (...args: infer Args) => infer Return ? ActorRPCFunction<Args, Return> : never; }; /** * RPC function returned by `ActorHandle`. This will call `ActorHandle.rpc` when triggered. * * @typedef {Function} ActorRPCFunction * @template Args * @template Response * @param {...Args} args - Arguments for the RPC function. * @returns {Promise<Response>} */ export type ActorRPCFunction< Args extends Array<unknown> = unknown[], Response = unknown, > = ( ...args: Args extends [unknown, ...infer Rest] ? Rest : Args ) => Promise<Response>; /** * Represents a region to connect to. * @typedef {Object} Region * @property {string} id - The region ID. * @property {string} name - The region name. * @see {@link https://rivet.gg/docs/edge|Edge Networking} * @see {@link https://rivet.gg/docs/regions|Available Regions} */ export interface Region { /** * The region slug. */ id: string; /** * The human-friendly region name. */ name: string; } /** * Client for managing & connecting to actors. * @see {@link https://rivet.gg/docs/manage|Create & Manage Actors} */ export class Client { #managerEndpointPromise: Promise<string>; #regionPromise: Promise<Region | undefined>; #protocolFormat: ProtocolFormat; /** * Creates an instance of Client. * * @param {string | Promise<string>} managerEndpointPromise - The manager endpoint or a promise resolving to it. See {@link https://rivet.gg/docs/setup|Initial Setup} for instructions on getting the manager endpoint. * @param {ClientOptions} [opts] - Options for configuring the client. * @see {@link https://rivet.gg/docs/setup|Initial Setup} */ public constructor( managerEndpointPromise: string | Promise<string>, opts?: ClientOptions, ) { if (managerEndpointPromise instanceof Promise) { // Save promise this.#managerEndpointPromise = managerEndpointPromise; } else { // Convert to promise this.#managerEndpointPromise = new Promise((resolve) => resolve(managerEndpointPromise), ); } this.#regionPromise = this.#fetchRegion(); this.#protocolFormat = opts?.protocolFormat ?? "cbor"; } /** * Gets an actor by its ID. * @template A The actor class that this handle is connected to. * @param {string} actorId - The ID of the actor. * @param {GetWithIdOptions} [opts] - Options for getting the actor. * @returns {Promise<ActorHandle<A>>} - A promise resolving to the actor handle. */ async getWithId<A = unknown>( actorId: string, opts?: GetWithIdOptions, ): Promise<ActorHandle> { logger().debug("get actor with id ", { actorId, parameters: opts?.parameters, }); const resJson = await this.#sendManagerRequest< ActorsRequest, ActorsResponse >("POST", "/actors", { query: { getForId: { actorId, }, }, }); const handle = this.#createHandle(resJson.endpoint, opts?.parameters); return this.#createProxy(handle) as ActorHandle<A>; } /** * Gets an actor by its tags, creating it if necessary. * * @example * ``` * const room = await client.get<ChatRoom>({ * name: 'chat_room', * // Get or create the actor for the channel `random` * channel: 'random' * }); * * // This actor will have the tags: { name: 'chat_room', channel: 'random' } * await room.sendMessage('Hello, world!'); * ``` * * @template A The actor class that this handle is connected to. * @param {ActorTags} tags - The tags to identify the actor. * @param {GetOptions} [opts] - Options for getting the actor. * @returns {Promise<ActorHandle<A>>} - A promise resolving to the actor handle. * @see {@link https://rivet.gg/docs/manage#client.get} */ async get<A = unknown>( tags: ActorTags, opts?: GetOptions, ): Promise<ActorHandle<A>> { if (!("name" in tags)) throw new Error("Tags must contain name"); // Build create config let create: CreateRequest | undefined = undefined; if (!opts?.noCreate) { create = { // Default to the same tags as the request tags: opts?.create?.tags ?? tags, // Default to the chosen region region: opts?.create?.region ?? (await this.#regionPromise)?.id, }; } logger().debug("get actor", { tags, parameters: opts?.parameters, create }); const resJson = await this.#sendManagerRequest< ActorsRequest, ActorsResponse >("POST", "/actors", { query: { getOrCreateForTags: { tags, create, }, }, }); const handle = this.#createHandle(resJson.endpoint, opts?.parameters); return this.#createProxy(handle) as ActorHandle<A>; } /** * Creates a new actor with the provided tags. * * @example * ``` * // Create a new document actor * const doc = await client.create<MyDocument>({ * create: { * tags: { * name: 'my_document', * docId: '123' * } * } * }); * * await doc.doSomething(); * ``` * * @template A The actor class that this handle is connected to. * @param {CreateOptions} opts - Options for creating the actor. * @returns {Promise<ActorHandle<A>>} - A promise resolving to the actor handle. * @see {@link https://rivet.gg/docs/manage#client.create} */ async create<A = unknown>(opts: CreateOptions): Promise<ActorHandle<A>> { // Build create config const create = opts.create; // Default to the chosen region if (!create.region) create.region = (await this.#regionPromise)?.id; logger().debug("create actor", { parameters: opts?.parameters, create }); const resJson = await this.#sendManagerRequest< ActorsRequest, ActorsResponse >("POST", "/actors", { query: { create, }, }); const handle = this.#createHandle(resJson.endpoint, opts?.parameters); return this.#createProxy(handle) as ActorHandle<A>; } #createHandle(endpoint: string, parameters: unknown): ActorHandleRaw { const handle = new ActorHandleRaw( endpoint, parameters, this.#protocolFormat, ); handle.connect(); return handle; } #createProxy(handle: ActorHandleRaw): ActorHandle { // Stores returned RPC functions for faster calls const methodCache = new Map<string, ActorRPCFunction>(); return new Proxy(handle, { get(target: ActorHandleRaw, prop: string | symbol, receiver: unknown) { // Handle built-in Symbol properties if (typeof prop === "symbol") { return Reflect.get(target, prop, receiver); } // Handle built-in Promise methods and existing properties if ( prop === "then" || prop === "catch" || prop === "finally" || prop === "constructor" || prop in target ) { const value = Reflect.get(target, prop, receiver); // Preserve method binding if (typeof value === "function") { return value.bind(target); } return value; } // Create RPC function that preserves 'this' context if (typeof prop === "string") { let method = methodCache.get(prop); if (!method) { method = (...args: unknown[]) => target.rpc(prop, ...args); methodCache.set(prop, method); } return method; } }, // Support for 'in' operator has(target: ActorHandleRaw, prop: string | symbol) { // All string properties are potentially RPC functions if (typeof prop === "string") { return true; } // For symbols, defer to the target's own has behavior return Reflect.has(target, prop); }, // Support instanceof checks getPrototypeOf(target: ActorHandleRaw) { return Reflect.getPrototypeOf(target); }, // Prevent property enumeration of non-existent RPC methods ownKeys(target: ActorHandleRaw) { return Reflect.ownKeys(target); }, // Support proper property descriptors getOwnPropertyDescriptor(target: ActorHandleRaw, prop: string | symbol) { const targetDescriptor = Reflect.getOwnPropertyDescriptor(target, prop); if (targetDescriptor) { return targetDescriptor; } if (typeof prop === "string") { // Make RPC methods appear non-enumerable return { configurable: true, enumerable: false, writable: false, value: (...args: unknown[]) => target.rpc(prop, ...args), }; } return undefined; }, }) as ActorHandle; } /** * Sends an HTTP request to the manager actor. * @private * @template Request * @template Response * @param {string} method - The HTTP method. * @param {string} path - The path for the request. * @param {Request} [body] - The request body. * @returns {Promise<Response>} - A promise resolving to the response. * @see {@link https://rivet.gg/docs/manage#client} */ async #sendManagerRequest<Request, Response>( method: string, path: string, body?: Request, ): Promise<Response> { try { const managerEndpoint = await this.#managerEndpointPromise; const res = await fetch(`${managerEndpoint}${path}`, { method, headers: { "Content-Type": "application/json", }, body: body ? JSON.stringify(body) : undefined, }); if (!res.ok) { throw new errors.ManagerError(`${res.statusText}: ${await res.text()}`); } return res.json(); } catch (error) { throw new errors.ManagerError(String(error), { cause: error }); } } /** * Fetches the region information. * @private * @returns {Promise<Region | undefined>} - A promise resolving to the region or undefined. * @see {@link https://rivet.gg/docs/edge#Fetching-regions-via-API} */ async #fetchRegion(): Promise<Region | undefined> { try { // Fetch the connection info from the manager const { endpoint, project, environment } = await this.#sendManagerRequest< undefined, RivetConfigResponse >("GET", "/rivet/config"); // Fetch the region // // This is fetched from the client instead of the manager so Rivet // can automatically determine the recommended region using an // anycast request made from the client const url = new URL("/regions/resolve", endpoint); if (project) url.searchParams.set("project", project); if (environment) url.searchParams.set("environment", environment); const res = await fetch(url.toString()); if (!res.ok) { // Add safe fallback in case we can't fetch the region logger().error("failed to fetch region, defaulting to manager region", { status: res.statusText, body: await res.text(), }); return undefined; } const { region }: { region: Region } = await res.json(); return region; } catch (error) { // Add safe fallback in case we can't fetch the region logger().error("failed to fetch region, defaulting to manager region", { error, }); return undefined; } } }