rivetkit
Version: 
Lightweight libraries for building stateful actors on edge platforms
1,758 lines (1,749 loc) • 121 kB
JavaScript
import {
  ColumnsSchema,
  ForeignKeysSchema,
  PatchSchema,
  TablesSchema
} from "./chunk-KGDZYQYE.js";
import {
  importWebSocket,
  logger
} from "./chunk-346X2XU4.js";
import {
  HTTP_ACTION_REQUEST_VERSIONED,
  HTTP_ACTION_RESPONSE_VERSIONED,
  HTTP_RESPONSE_ERROR_VERSIONED,
  PERSISTED_ACTOR_VERSIONED,
  TO_CLIENT_VERSIONED,
  TO_SERVER_VERSIONED,
  inputDataToBuffer,
  processMessage
} from "./chunk-QRFXXTLG.js";
import {
  CachedSerializer,
  DeadlineError,
  HEADER_CONN_ID,
  HEADER_CONN_PARAMS,
  HEADER_CONN_TOKEN,
  HEADER_ENCODING,
  HEADER_RIVET_ACTOR,
  HEADER_RIVET_TARGET,
  HEADER_RIVET_TOKEN,
  PATH_CONNECT_WEBSOCKET,
  PATH_RAW_WEBSOCKET_PREFIX,
  WS_PROTOCOL_ACTOR,
  WS_PROTOCOL_CONN_ID,
  WS_PROTOCOL_CONN_PARAMS,
  WS_PROTOCOL_CONN_TOKEN,
  WS_PROTOCOL_ENCODING,
  WS_PROTOCOL_STANDARD,
  WS_PROTOCOL_TARGET,
  WS_PROTOCOL_TOKEN,
  assertUnreachable as assertUnreachable2,
  contentTypeForEncoding,
  deadline,
  deserializeWithEncoding,
  encodeDataToString,
  encodingIsBinary,
  generateRandomString,
  generateSecureToken,
  jsonStringifyCompat,
  serializeWithEncoding,
  uint8ArrayToBase64
} from "./chunk-MLQIYKAZ.js";
import {
  getBaseLogger,
  getIncludeTarget,
  getLogger
} from "./chunk-7E5K3375.js";
import {
  SinglePromiseQueue,
  assertUnreachable,
  bufferToArrayBuffer,
  combineUrlPath,
  deconstructError,
  getEnvUniversal,
  httpUserAgent,
  isCborSerializable,
  noopNext,
  promiseWithResolvers,
  stringifyError
} from "./chunk-HI55LHM3.js";
import {
  ActionNotFound,
  ActionTimedOut,
  ActorNotFound,
  ConnStateNotEnabled,
  DatabaseNotEnabled,
  FetchHandlerNotDefined,
  InternalError,
  InvalidFetchResponse,
  InvalidRequest,
  InvalidStateType,
  StateNotEnabled,
  VarsNotEnabled
} from "./chunk-YPZFLUO6.js";
// src/actor/conn.ts
import * as cbor from "cbor-x";
// src/actor/conn-drivers.ts
var WEBSOCKET_DRIVER = {
  sendMessage: (actor, _conn, state, message) => {
    const serialized = message.serialize(state.encoding);
    actor.rLog.debug({
      msg: "sending websocket message",
      encoding: state.encoding,
      dataType: typeof serialized,
      isUint8Array: serialized instanceof Uint8Array,
      isArrayBuffer: serialized instanceof ArrayBuffer,
      dataLength: serialized.byteLength || serialized.length
    });
    if (serialized instanceof Uint8Array) {
      const buffer = serialized.buffer.slice(
        serialized.byteOffset,
        serialized.byteOffset + serialized.byteLength
      );
      if (buffer instanceof SharedArrayBuffer) {
        const arrayBuffer = new ArrayBuffer(buffer.byteLength);
        new Uint8Array(arrayBuffer).set(new Uint8Array(buffer));
        actor.rLog.debug({
          msg: "converted SharedArrayBuffer to ArrayBuffer",
          byteLength: arrayBuffer.byteLength
        });
        state.websocket.send(arrayBuffer);
      } else {
        actor.rLog.debug({
          msg: "sending ArrayBuffer",
          byteLength: buffer.byteLength
        });
        state.websocket.send(buffer);
      }
    } else {
      actor.rLog.debug({
        msg: "sending string data",
        length: serialized.length
      });
      state.websocket.send(serialized);
    }
  },
  disconnect: async (_actor, _conn, state, reason) => {
    state.websocket.close(1e3, reason);
    await state.closePromise.promise;
  },
  getConnectionReadyState: (_actor, _conn, state) => {
    return state.websocket.readyState;
  }
};
var SSE_DRIVER = {
  sendMessage: (_actor, _conn, state, message) => {
    state.stream.writeSSE({
      data: encodeDataToString(message.serialize(state.encoding))
    });
  },
  disconnect: async (_actor, _conn, state, _reason) => {
    state.stream.close();
  },
  getConnectionReadyState: (_actor, _conn, state) => {
    if (state.stream.aborted || state.stream.closed) {
      return 3 /* CLOSED */;
    }
    return 1 /* OPEN */;
  }
};
var HTTP_DRIVER = {
  getConnectionReadyState(_actor, _conn) {
    return 1 /* OPEN */;
  },
  disconnect: async () => {
  }
};
var CONN_DRIVERS = {
  [0 /* WEBSOCKET */]: WEBSOCKET_DRIVER,
  [1 /* SSE */]: SSE_DRIVER,
  [2 /* HTTP */]: HTTP_DRIVER
};
function getConnDriverKindFromState(state) {
  if (0 /* WEBSOCKET */ in state) return 0 /* WEBSOCKET */;
  else if (1 /* SSE */ in state) return 1 /* SSE */;
  else if (2 /* HTTP */ in state) return 2 /* HTTP */;
  else assertUnreachable(state);
}
// src/actor/conn.ts
function generateConnId() {
  return crypto.randomUUID();
}
function generateConnToken() {
  return generateSecureToken(32);
}
function generateConnSocketId() {
  return crypto.randomUUID();
}
var Conn = class {
  subscriptions = /* @__PURE__ */ new Set();
  // TODO: Remove this cyclical reference
  #actor;
  /**
   * The proxied state that notifies of changes automatically.
   *
   * Any data that should be stored indefinitely should be held within this object.
   */
  __persist;
  get __driverState() {
    var _a;
    return (_a = this.__socket) == null ? void 0 : _a.driverState;
  }
  /**
   * Socket connected to this connection.
   *
   * If undefined, then nothing is connected to this.
   */
  __socket;
  get __status() {
    if (this.__socket) {
      return "connected";
    } else {
      return "reconnecting";
    }
  }
  get params() {
    return this.__persist.params;
  }
  get __stateEnabled() {
    return this.#actor.connStateEnabled;
  }
  /**
   * Gets the current state of the connection.
   *
   * Throws an error if the state is not enabled.
   */
  get state() {
    this.#validateStateEnabled();
    if (!this.__persist.state) throw new Error("state should exists");
    return this.__persist.state;
  }
  /**
   * Sets the state of the connection.
   *
   * Throws an error if the state is not enabled.
   */
  set state(value) {
    this.#validateStateEnabled();
    this.__persist.state = value;
  }
  /**
   * Unique identifier for the connection.
   */
  get id() {
    return this.__persist.connId;
  }
  /**
   * Token used to authenticate this request.
   */
  get _token() {
    return this.__persist.token;
  }
  /**
   * 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.lastSeen;
  }
  /**
   * Initializes a new instance of the Connection class.
   *
   * This should only be constructed by {@link Actor}.
   *
   * @protected
   */
  constructor(actor, persist) {
    this.#actor = actor;
    this.__persist = persist;
  }
  #validateStateEnabled() {
    if (!this.__stateEnabled) {
      throw new ConnStateNotEnabled();
    }
  }
  /**
   * Sends a WebSocket message to the client.
   *
   * @param message - The message to send.
   *
   * @protected
   */
  _sendMessage(message) {
    if (this.__driverState) {
      const driverKind = getConnDriverKindFromState(this.__driverState);
      const driver = CONN_DRIVERS[driverKind];
      if (driver.sendMessage) {
        driver.sendMessage(
          this.#actor,
          this,
          this.__driverState[driverKind],
          message
        );
      } else {
        this.#actor.rLog.debug({
          msg: "conn driver does not support sending messages",
          conn: this.id
        });
      }
    } else {
      this.#actor.rLog.warn({
        msg: "missing connection driver state for send message",
        conn: this.id
      });
    }
  }
  /**
   * 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.dev/docs/events|Events Documentation}
   */
  send(eventName, ...args) {
    this.#actor.inspector.emitter.emit("eventFired", {
      type: "event",
      eventName,
      args,
      connId: this.id
    });
    this._sendMessage(
      new CachedSerializer(
        {
          body: {
            tag: "Event",
            val: {
              name: eventName,
              args: bufferToArrayBuffer(cbor.encode(args))
            }
          }
        },
        TO_CLIENT_VERSIONED
      )
    );
  }
  /**
   * Disconnects the client with an optional reason.
   *
   * @param reason - The reason for disconnection.
   */
  async disconnect(reason) {
    if (this.__socket && this.__driverState) {
      const driverKind = getConnDriverKindFromState(this.__driverState);
      const driver = CONN_DRIVERS[driverKind];
      if (driver.disconnect) {
        driver.disconnect(
          this.#actor,
          this,
          this.__driverState[driverKind],
          reason
        );
      } else {
        this.#actor.rLog.debug({
          msg: "no disconnect handler for conn driver",
          conn: this.id
        });
      }
      this.#actor.__connDisconnected(this, true, this.__socket.socketId);
    } else {
      this.#actor.rLog.warn({
        msg: "missing connection driver state for disconnect",
        conn: this.id
      });
    }
    this.__socket = void 0;
  }
};
// src/actor/instance.ts
import * as cbor2 from "cbor-x";
import invariant from "invariant";
import onChange from "on-change";
// src/inspector/actor.ts
import { sValidator } from "@hono/standard-validator";
import jsonPatch from "@rivetkit/fast-json-patch";
import { Hono } from "hono";
import { streamSSE } from "hono/streaming";
import { createNanoEvents } from "nanoevents";
import z from "zod/v4";
function createActorInspectorRouter() {
  return new Hono().get("/ping", (c) => {
    return c.json({ message: "pong" }, 200);
  }).get("/state", async (c) => {
    if (await c.var.inspector.accessors.isStateEnabled()) {
      return c.json(
        {
          enabled: true,
          state: await c.var.inspector.accessors.getState()
        },
        200
      );
    }
    return c.json({ enabled: false, state: null }, 200);
  }).patch(
    "/state",
    sValidator(
      "json",
      z.object({ patch: PatchSchema }).or(z.object({ replace: z.any() }))
    ),
    async (c) => {
      if (!await c.var.inspector.accessors.isStateEnabled()) {
        return c.json({ enabled: false }, 200);
      }
      const body = c.req.valid("json");
      if ("replace" in body) {
        await c.var.inspector.accessors.setState(body.replace);
        return c.json(
          {
            enabled: true,
            state: await c.var.inspector.accessors.getState()
          },
          200
        );
      }
      const state = await c.var.inspector.accessors.getState();
      const { newDocument: newState } = jsonPatch.applyPatch(
        state,
        body.patch
      );
      await c.var.inspector.accessors.setState(newState);
      return c.json(
        { enabled: true, state: await c.var.inspector.accessors.getState() },
        200
      );
    }
  ).get("/state/stream", async (c) => {
    if (!await c.var.inspector.accessors.isStateEnabled()) {
      return c.json({ enabled: false }, 200);
    }
    let id = 0;
    let unsub;
    return streamSSE(
      c,
      async (stream) => {
        unsub = c.var.inspector.emitter.on("stateUpdated", async (state) => {
          stream.writeSSE({
            data: JSON.stringify(state) || "",
            event: "state-update",
            id: String(id++)
          });
        });
        const { promise } = promiseWithResolvers();
        return promise;
      },
      async () => {
        unsub == null ? void 0 : unsub();
      }
    );
  }).get("/connections", async (c) => {
    const connections = await c.var.inspector.accessors.getConnections();
    return c.json({ connections }, 200);
  }).get("/connections/stream", async (c) => {
    let id = 0;
    let unsub;
    return streamSSE(
      c,
      async (stream) => {
        unsub = c.var.inspector.emitter.on("connectionUpdated", async () => {
          stream.writeSSE({
            data: JSON.stringify(
              await c.var.inspector.accessors.getConnections()
            ),
            event: "connection-update",
            id: String(id++)
          });
        });
        const { promise } = promiseWithResolvers();
        return promise;
      },
      async () => {
        unsub == null ? void 0 : unsub();
      }
    );
  }).get("/events", async (c) => {
    const events = c.var.inspector.lastRealtimeEvents;
    return c.json({ events }, 200);
  }).post("/events/clear", async (c) => {
    c.var.inspector.lastRealtimeEvents.length = 0;
    return c.json({ message: "Events cleared" }, 200);
  }).get("/events/stream", async (c) => {
    let id = 0;
    let unsub;
    return streamSSE(
      c,
      async (stream) => {
        unsub = c.var.inspector.emitter.on("eventFired", () => {
          stream.writeSSE({
            data: JSON.stringify(c.var.inspector.lastRealtimeEvents),
            event: "realtime-event",
            id: String(id++)
          });
        });
        const { promise } = promiseWithResolvers();
        return promise;
      },
      async () => {
        unsub == null ? void 0 : unsub();
      }
    );
  }).get("/rpcs", async (c) => {
    const rpcs = await c.var.inspector.accessors.getRpcs();
    return c.json({ rpcs }, 200);
  }).get("/db", async (c) => {
    if (!await c.var.inspector.accessors.isDbEnabled()) {
      return c.json({ enabled: false, db: null }, 200);
    }
    const db = await c.var.inspector.accessors.getDb();
    const rows = await db.execute(`PRAGMA table_list`);
    const tables = TablesSchema.parse(rows).filter(
      (table) => table.schema !== "temp" && !table.name.startsWith("sqlite_")
    );
    const tablesInfo = await Promise.all(
      tables.map((table) => db.execute(`PRAGMA table_info(${table.name})`))
    );
    const columns = tablesInfo.map((def) => ColumnsSchema.parse(def));
    const foreignKeysList = await Promise.all(
      tables.map(
        (table) => db.execute(`PRAGMA foreign_key_list(${table.name})`)
      )
    );
    const foreignKeys = foreignKeysList.map(
      (def) => ForeignKeysSchema.parse(def)
    );
    const countInfo = await Promise.all(
      tables.map(
        (table) => db.execute(`SELECT COUNT(*) as count FROM ${table.name}`)
      )
    );
    const counts = countInfo.map((def) => {
      return def[0].count || 0;
    });
    return c.json(
      {
        enabled: true,
        db: tablesInfo.map((_, index) => {
          return {
            table: tables[index],
            columns: columns[index],
            foreignKeys: foreignKeys[index],
            records: counts[index]
          };
        })
      },
      200
    );
  }).post(
    "/db",
    sValidator(
      "json",
      z.object({ query: z.string(), params: z.array(z.any()).optional() })
    ),
    async (c) => {
      if (!await c.var.inspector.accessors.isDbEnabled()) {
        return c.json({ enabled: false }, 200);
      }
      const db = await c.var.inspector.accessors.getDb();
      try {
        const result = await db.execute(
          c.req.valid("json").query,
          ...c.req.valid("json").params || []
        );
        return c.json({ result }, 200);
      } catch (error) {
        c;
        return c.json({ error: error.message }, 500);
      }
    }
  );
}
var ActorInspector = class {
  accessors;
  emitter = createNanoEvents();
  #lastRealtimeEvents = [];
  get lastRealtimeEvents() {
    return this.#lastRealtimeEvents;
  }
  constructor(accessors) {
    this.accessors = accessors();
    this.emitter.on("eventFired", (event) => {
      this.#lastRealtimeEvents.push({
        id: crypto.randomUUID(),
        timestamp: Date.now(),
        ...event
      });
      if (this.#lastRealtimeEvents.length > 100) {
        this.#lastRealtimeEvents = this.#lastRealtimeEvents.slice(-100);
      }
    });
  }
};
// src/actor/context.ts
var ActorContext = class {
  #actor;
  constructor(actor) {
    this.#actor = actor;
  }
  /**
   * Get the actor state
   */
  get state() {
    return this.#actor.state;
  }
  /**
   * Get the actor variables
   */
  get vars() {
    return this.#actor.vars;
  }
  /**
   * Broadcasts an event to all connected clients.
   * @param name - The name of the event.
   * @param args - The arguments to send with the event.
   */
  broadcast(name, ...args) {
    this.#actor._broadcast(name, ...args);
    return;
  }
  /**
   * Gets the logger instance.
   */
  get log() {
    return this.#actor.log;
  }
  /**
   * Gets actor ID.
   */
  get actorId() {
    return this.#actor.id;
  }
  /**
   * Gets the actor name.
   */
  get name() {
    return this.#actor.name;
  }
  /**
   * Gets the actor key.
   */
  get key() {
    return this.#actor.key;
  }
  /**
   * Gets the region.
   */
  get region() {
    return this.#actor.region;
  }
  /**
   * Gets the scheduler.
   */
  get schedule() {
    return this.#actor.schedule;
  }
  /**
   * Gets the map of connections.
   */
  get conns() {
    return this.#actor.conns;
  }
  /**
   * Returns the client for the given registry.
   */
  client() {
    return this.#actor.inlineClient;
  }
  /**
   * Gets the database.
   * @experimental
   * @throws {DatabaseNotEnabled} If the database is not enabled.
   */
  get db() {
    return this.#actor.db;
  }
  /**
   * Forces the state to get saved.
   *
   * @param opts - Options for saving the state.
   */
  async saveState(opts) {
    return this.#actor.saveState(opts);
  }
  /**
   * Prevents the actor from sleeping until promise is complete.
   */
  waitUntil(promise) {
    this.#actor._waitUntil(promise);
  }
  /**
   * AbortSignal that fires when the actor is stopping.
   */
  get abortSignal() {
    return this.#actor.abortSignal;
  }
  /**
   * Forces the actor to sleep.
   *
   * Not supported on all drivers.
   *
   * @experimental
   */
  sleep() {
    this.#actor._sleep();
  }
};
// src/actor/keys.ts
var EMPTY_KEY = "/";
var KEY_SEPARATOR = "/";
function serializeActorKey(key) {
  if (key.length === 0) {
    return EMPTY_KEY;
  }
  const escapedParts = key.map((part) => {
    if (part === "") {
      return "\\0";
    }
    let escaped = part.replace(/\\/g, "\\\\");
    escaped = escaped.replace(/\//g, `\\${KEY_SEPARATOR}`);
    return escaped;
  });
  return escapedParts.join(KEY_SEPARATOR);
}
function deserializeActorKey(keyString) {
  if (keyString === void 0 || keyString === null || keyString === EMPTY_KEY) {
    return [];
  }
  const parts = [];
  let currentPart = "";
  let escaping = false;
  let isEmptyStringMarker = false;
  for (let i = 0; i < keyString.length; i++) {
    const char = keyString[i];
    if (escaping) {
      if (char === "0") {
        isEmptyStringMarker = true;
      } else {
        currentPart += char;
      }
      escaping = false;
    } else if (char === "\\") {
      escaping = true;
    } else if (char === KEY_SEPARATOR) {
      if (isEmptyStringMarker) {
        parts.push("");
        isEmptyStringMarker = false;
      } else {
        parts.push(currentPart);
      }
      currentPart = "";
    } else {
      currentPart += char;
    }
  }
  if (escaping) {
    parts.push(currentPart + "\\");
  } else if (isEmptyStringMarker) {
    parts.push("");
  } else if (currentPart !== "" || parts.length > 0) {
    parts.push(currentPart);
  }
  return parts;
}
// src/actor/schedule.ts
var Schedule = class {
  #actor;
  constructor(actor) {
    this.#actor = actor;
  }
  async after(duration, fn, ...args) {
    await this.#actor.scheduleEvent(Date.now() + duration, fn, args);
  }
  async at(timestamp, fn, ...args) {
    await this.#actor.scheduleEvent(timestamp, fn, args);
  }
};
// src/actor/instance.ts
var ActorInstance = class {
  // Shared actor context for this instance
  actorContext;
  /** Actor log, intended for the user to call */
  #log;
  /** Runtime log, intended for internal actor logs */
  #rLog;
  #sleepCalled = false;
  #stopCalled = false;
  get isStopping() {
    return this.#stopCalled || this.#sleepCalled;
  }
  #persistChanged = false;
  #isInOnStateChange = false;
  /**
   * The proxied state that notifies of changes automatically.
   *
   * Any data that should be stored indefinitely should be held within this object.
   */
  #persist;
  /** Raw state without the proxy wrapper */
  #persistRaw;
  #persistWriteQueue = new SinglePromiseQueue();
  #alarmWriteQueue = new SinglePromiseQueue();
  #lastSaveTime = 0;
  #pendingSaveTimeout;
  #vars;
  #backgroundPromises = [];
  #abortController = new AbortController();
  #config;
  #actorDriver;
  #inlineClient;
  #actorId;
  #name;
  #key;
  #region;
  #ready = false;
  #connections = /* @__PURE__ */ new Map();
  #subscriptionIndex = /* @__PURE__ */ new Map();
  #checkConnLivenessInterval;
  #sleepTimeout;
  // Track active raw requests so sleep logic can account for them
  #activeRawFetchCount = 0;
  #activeRawWebSockets = /* @__PURE__ */ new Set();
  #schedule;
  #db;
  #inspector = new ActorInspector(() => {
    return {
      isDbEnabled: async () => {
        return this.#db !== void 0;
      },
      getDb: async () => {
        return this.db;
      },
      isStateEnabled: async () => {
        return this.stateEnabled;
      },
      getState: async () => {
        this.#validateStateEnabled();
        return this.#persistRaw.state;
      },
      getRpcs: async () => {
        return Object.keys(this.#config.actions);
      },
      getConnections: async () => {
        return Array.from(this.#connections.entries()).map(([id, conn]) => ({
          id,
          stateEnabled: conn.__stateEnabled,
          params: conn.params,
          state: conn.__stateEnabled ? conn.state : void 0
        }));
      },
      setState: async (state) => {
        this.#validateStateEnabled();
        this.#persist.state = { ...state };
        await this.saveState({ immediate: true });
      }
    };
  });
  get id() {
    return this.#actorId;
  }
  get inlineClient() {
    return this.#inlineClient;
  }
  get inspector() {
    return this.#inspector;
  }
  get #sleepingSupported() {
    return this.#actorDriver.sleep !== void 0;
  }
  /**
   * This constructor should never be used directly.
   *
   * Constructed in {@link ActorInstance.start}.
   *
   * @private
   */
  constructor(config) {
    this.#config = config;
    this.actorContext = new ActorContext(this);
  }
  async start(actorDriver, inlineClient, actorId, name, key, region) {
    var _a, _b;
    const logParams = {
      actor: name,
      key: serializeActorKey(key),
      actorId
    };
    this.#log = getBaseLogger().child(
      Object.assign(getIncludeTarget() ? { target: "actor" } : {}, logParams)
    );
    this.#rLog = getBaseLogger().child(
      Object.assign(
        getIncludeTarget() ? { target: "actor-runtime" } : {},
        logParams
      )
    );
    this.#actorDriver = actorDriver;
    this.#inlineClient = inlineClient;
    this.#actorId = actorId;
    this.#name = name;
    this.#key = key;
    this.#region = region;
    this.#schedule = new Schedule(this);
    await this.#initialize();
    if (this.#varsEnabled) {
      let vars;
      if ("createVars" in this.#config) {
        const dataOrPromise = this.#config.createVars(
          this.actorContext,
          this.#actorDriver.getContext(this.#actorId)
        );
        if (dataOrPromise instanceof Promise) {
          vars = await deadline(
            dataOrPromise,
            this.#config.options.createVarsTimeout
          );
        } else {
          vars = dataOrPromise;
        }
      } else if ("vars" in this.#config) {
        vars = structuredClone(this.#config.vars);
      } else {
        throw new Error("Could not variables from 'createVars' or 'vars'");
      }
      this.#vars = vars;
    }
    this.#rLog.info({ msg: "actor starting" });
    if (this.#config.onStart) {
      const result = this.#config.onStart(this.actorContext);
      if (result instanceof Promise) {
        await result;
      }
    }
    if ("db" in this.#config && this.#config.db) {
      const client = await this.#config.db.createClient({
        getDatabase: () => actorDriver.getDatabase(this.#actorId)
      });
      this.#rLog.info({ msg: "database migration starting" });
      await ((_b = (_a = this.#config.db).onMigrate) == null ? void 0 : _b.call(_a, client));
      this.#rLog.info({ msg: "database migration complete" });
      this.#db = client;
    }
    if (this.#persist.scheduledEvents.length > 0) {
      await this.#queueSetAlarm(this.#persist.scheduledEvents[0].timestamp);
    }
    this.#rLog.info({ msg: "actor ready" });
    this.#ready = true;
    this.#resetSleepTimer();
    this.#checkConnLivenessInterval = setInterval(
      this.#checkConnectionsLiveness.bind(this),
      this.#config.options.connectionLivenessInterval
    );
    this.#checkConnectionsLiveness();
    await this._onAlarm();
  }
  async #scheduleEventInner(newEvent) {
    this.actorContext.log.info({ msg: "scheduling event", ...newEvent });
    const insertIndex = this.#persist.scheduledEvents.findIndex(
      (x) => x.timestamp > newEvent.timestamp
    );
    if (insertIndex === -1) {
      this.#persist.scheduledEvents.push(newEvent);
    } else {
      this.#persist.scheduledEvents.splice(insertIndex, 0, newEvent);
    }
    if (insertIndex === 0 || this.#persist.scheduledEvents.length === 1) {
      this.actorContext.log.info({
        msg: "setting alarm",
        timestamp: newEvent.timestamp,
        eventCount: this.#persist.scheduledEvents.length
      });
      await this.#queueSetAlarm(newEvent.timestamp);
    }
  }
  /**
   * Triggers any pending alarms.
   *
   * This method is idempotent. It's called automatically when the actor wakes
   * in order to trigger any pending alarms.
   */
  async _onAlarm() {
    const now = Date.now();
    this.actorContext.log.debug({
      msg: "alarm triggered",
      now,
      events: this.#persist.scheduledEvents.length
    });
    this.#resetSleepTimer();
    const runIndex = this.#persist.scheduledEvents.findIndex(
      (x) => x.timestamp <= now
    );
    if (runIndex === -1) {
      this.#rLog.debug({ msg: "no events are due yet" });
      if (this.#persist.scheduledEvents.length > 0) {
        const nextTs = this.#persist.scheduledEvents[0].timestamp;
        this.actorContext.log.debug({
          msg: "alarm fired early, rescheduling for next event",
          now,
          nextTs,
          delta: nextTs - now
        });
        await this.#queueSetAlarm(nextTs);
      }
      this.actorContext.log.debug({ msg: "no events to run", now });
      return;
    }
    const scheduleEvents = this.#persist.scheduledEvents.splice(
      0,
      runIndex + 1
    );
    this.actorContext.log.debug({
      msg: "running events",
      count: scheduleEvents.length
    });
    if (this.#persist.scheduledEvents.length > 0) {
      const nextTs = this.#persist.scheduledEvents[0].timestamp;
      this.actorContext.log.info({
        msg: "setting next alarm",
        nextTs,
        remainingEvents: this.#persist.scheduledEvents.length
      });
      await this.#queueSetAlarm(nextTs);
    }
    for (const event of scheduleEvents) {
      try {
        this.actorContext.log.info({
          msg: "running action for event",
          event: event.eventId,
          timestamp: event.timestamp,
          action: event.kind.generic.actionName
        });
        const fn = this.#config.actions[event.kind.generic.actionName];
        if (!fn)
          throw new Error(
            `Missing action for alarm ${event.kind.generic.actionName}`
          );
        if (typeof fn !== "function")
          throw new Error(
            `Alarm function lookup for ${event.kind.generic.actionName} returned ${typeof fn}`
          );
        try {
          const args = event.kind.generic.args ? cbor2.decode(new Uint8Array(event.kind.generic.args)) : [];
          await fn.call(void 0, this.actorContext, ...args);
        } catch (error) {
          this.actorContext.log.error({
            msg: "error while running event",
            error: stringifyError(error),
            event: event.eventId,
            timestamp: event.timestamp,
            action: event.kind.generic.actionName
          });
        }
      } catch (error) {
        this.actorContext.log.error({
          msg: "internal error while running event",
          error: stringifyError(error),
          ...event
        });
      }
    }
  }
  async scheduleEvent(timestamp, action, args) {
    return this.#scheduleEventInner({
      eventId: crypto.randomUUID(),
      timestamp,
      kind: {
        generic: {
          actionName: action,
          args: bufferToArrayBuffer(cbor2.encode(args))
        }
      }
    });
  }
  get stateEnabled() {
    return "createState" in this.#config || "state" in this.#config;
  }
  #validateStateEnabled() {
    if (!this.stateEnabled) {
      throw new StateNotEnabled();
    }
  }
  get connStateEnabled() {
    return "createConnState" in this.#config || "connState" in this.#config;
  }
  get #varsEnabled() {
    return "createVars" in this.#config || "vars" in this.#config;
  }
  #validateVarsEnabled() {
    if (!this.#varsEnabled) {
      throw new VarsNotEnabled();
    }
  }
  /** Promise used to wait for a save to complete. This is required since you cannot await `#saveStateThrottled`. */
  #onPersistSavedPromise;
  /** Throttled save state method. Used to write to KV at a reasonable cadence. */
  #savePersistThrottled() {
    const now = Date.now();
    const timeSinceLastSave = now - this.#lastSaveTime;
    const saveInterval = this.#config.options.stateSaveInterval;
    if (timeSinceLastSave < saveInterval) {
      if (this.#pendingSaveTimeout === void 0) {
        this.#pendingSaveTimeout = setTimeout(() => {
          this.#pendingSaveTimeout = void 0;
          this.#savePersistInner();
        }, saveInterval - timeSinceLastSave);
      }
    } else {
      this.#savePersistInner();
    }
  }
  /** Saves the state to KV. You probably want to use #saveStateThrottled instead except for a few edge cases. */
  async #savePersistInner() {
    var _a, _b;
    try {
      this.#lastSaveTime = Date.now();
      if (this.#persistChanged) {
        const finished = this.#persistWriteQueue.enqueue(async () => {
          this.#rLog.debug({ msg: "saving persist" });
          this.#persistChanged = false;
          const bareData = this.#convertToBarePersisted(this.#persistRaw);
          await this.#actorDriver.writePersistedData(
            this.#actorId,
            PERSISTED_ACTOR_VERSIONED.serializeWithEmbeddedVersion(bareData)
          );
          this.#rLog.debug({ msg: "persist saved" });
        });
        await finished;
      }
      (_a = this.#onPersistSavedPromise) == null ? void 0 : _a.resolve();
    } catch (error) {
      (_b = this.#onPersistSavedPromise) == null ? void 0 : _b.reject(error);
      throw error;
    }
  }
  async #queueSetAlarm(timestamp) {
    await this.#alarmWriteQueue.enqueue(async () => {
      await this.#actorDriver.setAlarm(this, timestamp);
    });
  }
  /**
   * Creates proxy for `#persist` that handles automatically flagging when state needs to be updated.
   */
  #setPersist(target) {
    this.#persistRaw = target;
    if (target === null || typeof target !== "object") {
      let invalidPath = "";
      if (!isCborSerializable(
        target,
        (path) => {
          invalidPath = path;
        },
        ""
      )) {
        throw new InvalidStateType({ path: invalidPath });
      }
      return target;
    }
    if (this.#persist) {
      onChange.unsubscribe(this.#persist);
    }
    this.#persist = onChange(
      target,
      // biome-ignore lint/suspicious/noExplicitAny: Don't know types in proxy
      (path, value, _previousValue, _applyData) => {
        if (path !== "state" && !path.startsWith("state.")) {
          return;
        }
        let invalidPath = "";
        if (!isCborSerializable(
          value,
          (invalidPathPart) => {
            invalidPath = invalidPathPart;
          },
          ""
        )) {
          throw new InvalidStateType({
            path: path + (invalidPath ? `.${invalidPath}` : "")
          });
        }
        this.#persistChanged = true;
        this.inspector.emitter.emit("stateUpdated", this.#persist.state);
        if (this.#config.onStateChange && this.#ready && !this.#isInOnStateChange) {
          try {
            this.#isInOnStateChange = true;
            this.#config.onStateChange(
              this.actorContext,
              this.#persistRaw.state
            );
          } catch (error) {
            this.#rLog.error({
              msg: "error in `_onStateChange`",
              error: stringifyError(error)
            });
          } finally {
            this.#isInOnStateChange = false;
          }
        }
      },
      { ignoreDetached: true }
    );
  }
  async #initialize() {
    const persistDataBuffer = await this.#actorDriver.readPersistedData(
      this.#actorId
    );
    invariant(
      persistDataBuffer !== void 0,
      "persist data has not been set, it should be set when initialized"
    );
    const bareData = PERSISTED_ACTOR_VERSIONED.deserializeWithEmbeddedVersion(
      persistDataBuffer
    );
    const persistData = this.#convertFromBarePersisted(bareData);
    if (persistData.hasInitiated) {
      this.#rLog.info({
        msg: "actor restoring",
        connections: persistData.connections.length
      });
      this.#setPersist(persistData);
      for (const connPersist of this.#persist.connections) {
        const conn = new Conn(this, connPersist);
        this.#connections.set(conn.id, conn);
        for (const sub of connPersist.subscriptions) {
          this.#addSubscription(sub.eventName, conn, true);
        }
      }
    } else {
      this.#rLog.info({ msg: "actor creating" });
      let stateData;
      if (this.stateEnabled) {
        this.#rLog.info({ msg: "actor state initializing" });
        if ("createState" in this.#config) {
          this.#config.createState;
          stateData = await this.#config.createState(
            this.actorContext,
            persistData.input
          );
        } else if ("state" in this.#config) {
          stateData = structuredClone(this.#config.state);
        } else {
          throw new Error("Both 'createState' or 'state' were not defined");
        }
      } else {
        this.#rLog.debug({ msg: "state not enabled" });
      }
      persistData.state = stateData;
      persistData.hasInitiated = true;
      this.#rLog.debug({ msg: "writing state" });
      const bareData2 = this.#convertToBarePersisted(persistData);
      await this.#actorDriver.writePersistedData(
        this.#actorId,
        PERSISTED_ACTOR_VERSIONED.serializeWithEmbeddedVersion(bareData2)
      );
      this.#setPersist(persistData);
      if (this.#config.onCreate) {
        await this.#config.onCreate(this.actorContext, persistData.input);
      }
    }
  }
  __getConnForId(id) {
    return this.#connections.get(id);
  }
  /**
   * Call when conn is disconnected. Used by transports.
   *
   * If a clean diconnect, will be removed immediately.
   *
   * If not a clean disconnect, will keep the connection alive for a given interval to wait for reconnect.
   */
  __connDisconnected(conn, wasClean, socketId) {
    if (socketId && conn.__socket && socketId !== conn.__socket.socketId) {
      this.#rLog.debug({
        msg: "ignoring stale disconnect event",
        connId: conn.id,
        eventSocketId: socketId,
        currentSocketId: conn.__socket.socketId
      });
      return;
    }
    if (wasClean) {
      this.#removeConn(conn);
    } else {
      if (!conn.__driverState) {
        this.rLog.warn("called conn disconnected without driver state");
      }
      conn.__persist.lastSeen = Date.now();
      conn.__socket = void 0;
      this.#resetSleepTimer();
    }
  }
  /**
   * Removes a connection and cleans up its resources.
   */
  #removeConn(conn) {
    const connIdx = this.#persist.connections.findIndex(
      (c) => c.connId === conn.id
    );
    if (connIdx !== -1) {
      this.#persist.connections.splice(connIdx, 1);
      this.saveState({ immediate: true, allowStoppingState: true });
    } else {
      this.#rLog.warn({
        msg: "could not find persisted connection to remove",
        connId: conn.id
      });
    }
    this.#connections.delete(conn.id);
    this.#rLog.debug({ msg: "removed conn", connId: conn.id });
    for (const eventName of [...conn.subscriptions.values()]) {
      this.#removeSubscription(eventName, conn, true);
    }
    this.inspector.emitter.emit("connectionUpdated");
    if (this.#config.onDisconnect) {
      try {
        const result = this.#config.onDisconnect(this.actorContext, conn);
        if (result instanceof Promise) {
          result.catch((error) => {
            this.#rLog.error({
              msg: "error in `onDisconnect`",
              error: stringifyError(error)
            });
          });
        }
      } catch (error) {
        this.#rLog.error({
          msg: "error in `onDisconnect`",
          error: stringifyError(error)
        });
      }
    }
    this.#resetSleepTimer();
  }
  /**
   * Called to create a new connection or reconnect an existing one.
   */
  async createConn(socket, params, request, connectionId, connectionToken) {
    this.#assertReady();
    if (connectionId && connectionToken) {
      this.rLog.debug({
        msg: "checking for existing connection",
        connectionId
      });
      const existingConn = this.#connections.get(connectionId);
      if (existingConn && existingConn._token === connectionToken) {
        this.rLog.debug({
          msg: "reconnecting existing connection",
          connectionId
        });
        if (existingConn.__driverState) {
          const driverKind = getConnDriverKindFromState(
            existingConn.__driverState
          );
          const driver = CONN_DRIVERS[driverKind];
          if (driver.disconnect) {
            driver.disconnect(
              this,
              existingConn,
              existingConn.__driverState[driverKind],
              "Reconnecting with new driver state"
            );
          }
        }
        existingConn.__socket = socket;
        existingConn.__persist.lastSeen = Date.now();
        this.#resetSleepTimer();
        this.inspector.emitter.emit("connectionUpdated");
        existingConn._sendMessage(
          new CachedSerializer(
            {
              body: {
                tag: "Init",
                val: {
                  actorId: this.id,
                  connectionId: existingConn.id,
                  connectionToken: existingConn._token
                }
              }
            },
            TO_CLIENT_VERSIONED
          )
        );
        return existingConn;
      }
      this.rLog.debug({
        msg: "connection not found or token mismatch, creating new connection",
        connectionId
      });
    }
    const newConnId = generateConnId();
    const newConnToken = generateConnToken();
    if (this.#connections.has(newConnId)) {
      throw new Error(`Connection already exists: ${newConnId}`);
    }
    let connState;
    const onBeforeConnectOpts = {
      request
    };
    if (this.#config.onBeforeConnect) {
      await this.#config.onBeforeConnect(
        this.actorContext,
        onBeforeConnectOpts,
        params
      );
    }
    if (this.connStateEnabled) {
      if ("createConnState" in this.#config) {
        const dataOrPromise = this.#config.createConnState(
          this.actorContext,
          onBeforeConnectOpts,
          params
        );
        if (dataOrPromise instanceof Promise) {
          connState = await deadline(
            dataOrPromise,
            this.#config.options.createConnStateTimeout
          );
        } else {
          connState = dataOrPromise;
        }
      } else if ("connState" in this.#config) {
        connState = structuredClone(this.#config.connState);
      } else {
        throw new Error(
          "Could not create connection state from 'createConnState' or 'connState'"
        );
      }
    }
    const persist = {
      connId: newConnId,
      token: newConnToken,
      params,
      state: connState,
      lastSeen: Date.now(),
      subscriptions: []
    };
    const conn = new Conn(this, persist);
    conn.__socket = socket;
    this.#connections.set(conn.id, conn);
    this.#resetSleepTimer();
    this.#persist.connections.push(persist);
    this.saveState({ immediate: true });
    if (this.#config.onConnect) {
      try {
        const result = this.#config.onConnect(this.actorContext, conn);
        if (result instanceof Promise) {
          deadline(result, this.#config.options.onConnectTimeout).catch(
            (error) => {
              this.#rLog.error({
                msg: "error in `onConnect`, closing socket",
                error
              });
              conn == null ? void 0 : conn.disconnect("`onConnect` failed");
            }
          );
        }
      } catch (error) {
        this.#rLog.error({
          msg: "error in `onConnect`",
          error: stringifyError(error)
        });
        conn == null ? void 0 : conn.disconnect("`onConnect` failed");
      }
    }
    this.inspector.emitter.emit("connectionUpdated");
    conn._sendMessage(
      new CachedSerializer(
        {
          body: {
            tag: "Init",
            val: {
              actorId: this.id,
              connectionId: conn.id,
              connectionToken: conn._token
            }
          }
        },
        TO_CLIENT_VERSIONED
      )
    );
    return conn;
  }
  // MARK: Messages
  async processMessage(message, conn) {
    await processMessage(message, this, conn, {
      onExecuteAction: async (ctx, name, args) => {
        this.inspector.emitter.emit("eventFired", {
          type: "action",
          name,
          args,
          connId: conn.id
        });
        return await this.executeAction(ctx, name, args);
      },
      onSubscribe: async (eventName, conn2) => {
        this.inspector.emitter.emit("eventFired", {
          type: "subscribe",
          eventName,
          connId: conn2.id
        });
        this.#addSubscription(eventName, conn2, false);
      },
      onUnsubscribe: async (eventName, conn2) => {
        this.inspector.emitter.emit("eventFired", {
          type: "unsubscribe",
          eventName,
          connId: conn2.id
        });
        this.#removeSubscription(eventName, conn2, false);
      }
    });
  }
  // MARK: Events
  #addSubscription(eventName, connection, fromPersist) {
    if (connection.subscriptions.has(eventName)) {
      this.#rLog.debug({
        msg: "connection already has subscription",
        eventName
      });
      return;
    }
    if (!fromPersist) {
      connection.__persist.subscriptions.push({ eventName });
      this.saveState({ immediate: true });
    }
    connection.subscriptions.add(eventName);
    let subscribers = this.#subscriptionIndex.get(eventName);
    if (!subscribers) {
      subscribers = /* @__PURE__ */ new Set();
      this.#subscriptionIndex.set(eventName, subscribers);
    }
    subscribers.add(connection);
  }
  #removeSubscription(eventName, connection, fromRemoveConn) {
    if (!connection.subscriptions.has(eventName)) {
      this.#rLog.warn({
        msg: "connection does not have subscription",
        eventName
      });
      return;
    }
    if (!fromRemoveConn) {
      connection.subscriptions.delete(eventName);
      const subIdx = connection.__persist.subscriptions.findIndex(
        (s) => s.eventName === eventName
      );
      if (subIdx !== -1) {
        connection.__persist.subscriptions.splice(subIdx, 1);
      } else {
        this.#rLog.warn({
          msg: "subscription does not exist with name",
          eventName
        });
      }
      this.saveState({ immediate: true });
    }
    const subscribers = this.#subscriptionIndex.get(eventName);
    if (subscribers) {
      subscribers.delete(connection);
      if (subscribers.size === 0) {
        this.#subscriptionIndex.delete(eventName);
      }
    }
  }
  #assertReady(allowStoppingState = false) {
    if (!this.#ready) throw new InternalError("Actor not ready");
    if (!allowStoppingState && this.#sleepCalled)
      throw new InternalError("Actor is going to sleep");
    if (!allowStoppingState && this.#stopCalled)
      throw new InternalError("Actor is stopping");
  }
  /**
   * Check the liveness of all connections.
   * Sets up a recurring check based on the configured interval.
   */
  #checkConnectionsLiveness() {
    this.#rLog.debug({ msg: "checking connections liveness" });
    for (const conn of this.#connections.values()) {
      if (conn.__status === "connected") {
        this.#rLog.debug({ msg: "connection is alive", connId: conn.id });
      } else {
        const lastSeen = conn.__persist.lastSeen;
        const sinceLastSeen = Date.now() - lastSeen;
        if (sinceLastSeen < this.#config.options.connectionLivenessTimeout) {
          this.#rLog.debug({
            msg: "connection might be alive, will check later",
            connId: conn.id,
            lastSeen,
            sinceLastSeen
          });
          continue;
        }
        this.#rLog.info({
          msg: "connection is dead, removing",
          connId: conn.id,
          lastSeen
        });
        this.#removeConn(conn);
      }
    }
  }
  /**
   * Check if the actor is ready to handle requests.
   */
  isReady() {
    return this.#ready;
  }
  /**
   * Execute an action call from a client.
   *
   * This method handles:
   * 1. Validating the action name
   * 2. Executing the action function
   * 3. Processing the result through onBeforeActionResponse (if configured)
   * 4. Handling timeouts and errors
   * 5. Saving state changes
   *
   * @param ctx The action context
   * @param actionName The name of the action being called
   * @param args The arguments passed to the action
   * @returns The result of the action call
   * @throws {ActionNotFound} If the action doesn't exist
   * @throws {ActionTimedOut} If the action times out
   * @internal
   */
  async executeAction(ctx, actionName, args) {
    invariant(this.#ready, "executing action before ready");
    if (!(actionName in this.#config.actions)) {
      this.#rLog.warn({ msg: "action does not exist", actionName });
      throw new ActionNotFound(actionName);
    }
    const actionFunction = this.#config.actions[actionName];
    if (typeof actionFunction !== "function") {
      this.#rLog.warn({
        msg: "action is not a function",
        actionName,
        type: typeof actionFunction
      });
      throw new ActionNotFound(actionName);
    }
    try {
      this.#rLog.debug({
        msg: "executing action",
        actionName,
        args
      });
      const outputOrPromise = actionFunction.call(void 0, ctx, ...args);
      let output;
      if (outputOrPromise instanceof Promise) {
        this.#rLog.debug({
          msg: "awaiting async action",
          actionName
        });
        output = await deadline(
          outputOrPromise,
          this.#config.options.actionTimeout
        );
        this.#rLog.debug({
          msg: "async action completed",
          actionName
        });
      } else {
        output = outputOrPromise;
      }
      if (this.#config.onBeforeActionResponse) {
        try {
          const processedOutput = this.#config.onBeforeActionResponse(
            this.actorContext,
            actionName,
            args,
            output
          );
          if (processedOutput instanceof Promise) {
            this.#rLog.debug({
              msg: "awaiting onBeforeActionResponse",
              actionName
            });
            output = await processedOutput;
            this.#rLog.debug({
              msg: "onBeforeActionResponse completed",
              actionName
            });
          } else {
            output = processedOutput;
          }
        } catch (error) {
          this.#rLog.error({
            msg: "error in `onBeforeActionResponse`",
            error: stringifyError(error)
          });
        }
      }
      this.#rLog.debug({
        msg: "action completed",
        actionName,
        outputType: typeof output,
        isPromise: output instanceof Promise
      });
      return output;
    } catch (error) {
      if (error instanceof DeadlineError) {
        throw new ActionTimedOut();
      }
      this.#rLog.error({
        msg: "action error",
        actionName,
        error: stringifyError(error)
      });
      throw error;
    } finally {
      this.#savePersistThrottled();
    }
  }
  /**
   * Returns a list of action methods available on this actor.
   */
  get actions() {
    return Object.keys(this.#config.actions);
  }
  /**
   * Handles raw HTTP requests to the actor.
   */
  async handleFetch(request, opts) {
    this.#assertReady();
    if (!this.#config.onFetch) {
      throw new FetchHandlerNotDefined();
    }
    this.#activeRawFetchCount++;
    this.#resetSleepTimer();
    try {
      const response = await this.#config.onFetch(
        this.actorContext,
        request,
        opts
      );
      if (!response) {
        throw new InvalidFetchResponse();
      }
      return response;
    } catch (error) {
      this.#rLog.error({ msg: "onFetch error", error: stringifyError(error) });
      throw error;
    } finally {
      this.#activeRawFetchCount = Math.max(0, this.#activeRawFetchCount - 1);
      this.#resetSleepTimer();
      this.#savePersistThrottled();
    }
  }
  /**
   * Handles raw WebSocket connections to the actor.
   */
  async handleWebSocket(websocket, opts) {
    this.#assertReady();
    if (!this.#config.onWebSocket) {
      throw new InternalError("onWebSocket handler not defined");
    }
    try {
      const stateBeforeHandler = this.#persistChanged;
      this.#activeRawWebSockets.add(we