Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion rivetkit-typescript/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## Tree-Shaking Boundaries

- Do not import `@rivetkit/workflow-engine` outside the `rivetkit/workflow` entrypoint so it remains tree-shakeable.
- Do not import SQLite VFS or `wa-sqlite` outside the `rivetkit/db` (or `@rivetkit/sqlite-vfs`) entrypoint so SQLite support remains tree-shakeable.
- Do not import SQLite VFS or `@rivetkit/sqlite` outside the `rivetkit/db` (or `@rivetkit/sqlite-vfs`) entrypoint so SQLite support remains tree-shakeable.
- Importing `rivetkit/db` (or `@rivetkit/sqlite-vfs`) is the explicit opt-in for SQLite. Do not lazily load SQLite from `rivetkit/db`; it may be imported eagerly inside that entrypoint.
- Core drivers must remain SQLite-agnostic. Any SQLite-specific wiring belongs behind the `rivetkit/db` or `@rivetkit/sqlite-vfs` boundary.

Expand Down
11 changes: 7 additions & 4 deletions rivetkit-typescript/packages/rivetkit/src/actor/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,16 @@ export interface ActorDriver {
): Promise<BaseSQLiteDatabase<any,any,any,any> | undefined>;

/**
* SQLite VFS instance for creating KV-backed databases.
* Returns a SQLite VFS instance for creating KV-backed databases.
* If not provided, the database provider will need an override.
*
* wa-sqlite's async build is not re-entrant per module instance. Drivers
* should scope this instance to a single actor when using KV-backed SQLite.
* @rivetkit/sqlite's async build is not re-entrant per module instance. Drivers
* should return a new instance per call for actor-level isolation.
*
* This is a method (not a property) so drivers can use dynamic imports,
* keeping the core driver tree-shakeable from @rivetkit/sqlite.
*/
sqliteVfs?: SqliteVfs;
getSqliteVfs?(): SqliteVfs | Promise<SqliteVfs>;

/**
* Requests the actor to go to sleep.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1409,10 +1409,12 @@ export class ActorInstance<

let client: InferDatabaseClient<DB> | undefined;
try {
// Every actor gets its own SqliteVfs/wa-sqlite instance. The async
// wa-sqlite build is not re-entrant, and sharing one instance across
// Every actor gets its own SqliteVfs/@rivetkit/sqlite instance. The async
// @rivetkit/sqlite build is not re-entrant, and sharing one instance across
// actors can cause cross-actor contention and runtime corruption.
this.#sqliteVfs ??= this.driver.sqliteVfs;
if (!this.#sqliteVfs && this.driver.getSqliteVfs) {
this.#sqliteVfs = await this.driver.getSqliteVfs();
}

client = await this.#config.db.createClient({
actorId: this.#actorId,
Expand Down
2 changes: 1 addition & 1 deletion rivetkit-typescript/packages/rivetkit/src/db/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export interface DatabaseProviderContext {

/**
* SQLite VFS instance for creating KV-backed databases.
* This should be actor-scoped because wa-sqlite is not re-entrant per
* This should be actor-scoped because @rivetkit/sqlite is not re-entrant per
* module instance.
*/
sqliteVfs?: SqliteVfs;
Expand Down
12 changes: 6 additions & 6 deletions rivetkit-typescript/packages/rivetkit/src/db/drizzle/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ function createActorKvStore(kv: {
}

/**
* Mutex to serialize async operations on a wa-sqlite database handle.
* wa-sqlite is not safe for concurrent operations on the same handle.
* Mutex to serialize async operations on a @rivetkit/sqlite database handle.
* @rivetkit/sqlite is not safe for concurrent operations on the same handle.
*/
class DbMutex {
#locked = false;
Expand Down Expand Up @@ -89,7 +89,7 @@ class DbMutex {
}

/**
* Create a sqlite-proxy async callback from a wa-sqlite Database
* Create a sqlite-proxy async callback from a @rivetkit/sqlite Database
*/
function createProxyCallback(
waDb: Database,
Expand Down Expand Up @@ -126,7 +126,7 @@ function createProxyCallback(
}

/**
* Run inline migrations via the wa-sqlite Database.
* Run inline migrations via the @rivetkit/sqlite Database.
* Migrations use the same embedded format as drizzle-orm's durable-sqlite.
*/
async function runInlineMigrations(
Expand Down Expand Up @@ -186,7 +186,7 @@ export function db<
>(
config?: DatabaseFactoryConfig<TSchema>,
): DatabaseProvider<SqliteRemoteDatabase<TSchema> & RawAccess> {
// Store the wa-sqlite Database instance alongside the drizzle client
// Store the @rivetkit/sqlite Database instance alongside the drizzle client
let waDbInstance: Database | null = null;
const mutex = new DbMutex();

Expand Down Expand Up @@ -240,7 +240,7 @@ export function db<
}) as TRow[];
}
// Use exec for non-parameterized queries since
// wa-sqlite's query() can crash on some statements.
// @rivetkit/sqlite's query() can crash on some statements.
const results: Record<string, unknown>[] = [];
let columnNames: string[] | null = null;
await waDb.exec(
Expand Down
4 changes: 2 additions & 2 deletions rivetkit-typescript/packages/rivetkit/src/db/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ export function db({
let op: Promise<void> = Promise.resolve();

const serialize = async <T>(fn: () => Promise<T>): Promise<T> => {
// Ensure wa-sqlite calls are not concurrent. Actors can process multiple
// actions concurrently, and wa-sqlite is not re-entrant.
// Ensure @rivetkit/sqlite calls are not concurrent. Actors can process multiple
// actions concurrently, and @rivetkit/sqlite is not re-entrant.
const next = op.then(fn, fn);
op = next.then(
() => undefined,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { AnyClient } from "@/client/client";
import type { RawDatabaseClient } from "@/db/config";
import { SqliteVfs } from "@rivetkit/sqlite-vfs";
import type { SqliteVfs } from "@rivetkit/sqlite-vfs";
import type {
ActorDriver,
AnyActorInstance,
Expand Down Expand Up @@ -82,10 +82,18 @@ export class FileSystemActorDriver implements ActorDriver {
}

/** SQLite VFS instance for creating KV-backed databases */
get sqliteVfs(): SqliteVfs {
// The async wa-sqlite build is not re-entrant per module instance.
async getSqliteVfs(): Promise<SqliteVfs> {
// Dynamic import keeps @rivetkit/sqlite out of the main entrypoint bundle,
// preserving tree-shakeability for environments that don't use SQLite.
// The async @rivetkit/sqlite build is not re-entrant per module instance.
// Returning a fresh SqliteVfs here gives each actor its own module,
// allowing actor-level parallelism without cross-actor re-entry.
//
// The specifier is built via concatenation so that bundlers like
// wrangler's esbuild cannot statically analyze and attempt to
// bundle the module (it is never used on Cloudflare Workers).
const specifier = "@rivetkit/" + "sqlite-vfs";
const { SqliteVfs } = await import(specifier);
return new SqliteVfs();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export class ActorInspector {
"SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '__drizzle_%'",
) as { name: string; type: string }[];

// Serialize all queries to avoid concurrent wa-sqlite access
// Serialize all queries to avoid concurrent @rivetkit/sqlite access
// which can cause "file is not a database" errors.
const tableInfos = [];
for (const table of tables) {
Expand Down
16 changes: 8 additions & 8 deletions rivetkit-typescript/packages/sqlite-vfs/src/vfs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* used concurrently with other instances.
*/

// Note: wa-sqlite VFS.Base type definitions have incorrect types for xRead/xWrite
// Note: @rivetkit/sqlite VFS.Base type definitions have incorrect types for xRead/xWrite
// The actual runtime uses Uint8Array, not the {size, value} object shown in types
import * as VFS from "@rivetkit/sqlite/src/VFS.js";

Expand Down Expand Up @@ -57,7 +57,7 @@ function decodeFileMeta(data: Uint8Array): number {

/**
* SQLite API interface (subset needed for VFS registration)
* This is part of wa-sqlite but not exported in TypeScript types
* This is part of @rivetkit/sqlite but not exported in TypeScript types
*/
interface SQLite3Api {
vfs_register: (vfs: unknown, makeDefault?: boolean) => number;
Expand Down Expand Up @@ -88,7 +88,7 @@ interface SQLite3Api {

/**
* Simple async mutex for serializing database operations
* wa-sqlite calls are not safe to run concurrently on one module instance
* @rivetkit/sqlite calls are not safe to run concurrently on one module instance
*/
class AsyncMutex {
#locked = false;
Expand Down Expand Up @@ -188,7 +188,7 @@ export class Database {
}

/**
* Get the raw wa-sqlite API (for advanced usage)
* Get the raw @rivetkit/sqlite API (for advanced usage)
*/
get sqlite3(): SQLite3Api {
return this.#sqlite3;
Expand All @@ -205,7 +205,7 @@ export class Database {
/**
* SQLite VFS backed by KV storage.
*
* Each instance is independent and has its own wa-sqlite WASM module.
* Each instance is independent and has its own @rivetkit/sqlite WASM module.
* This allows multiple instances to operate concurrently without interference.
*/
export class SqliteVfs {
Expand All @@ -222,7 +222,7 @@ export class SqliteVfs {
}

/**
* Initialize wa-sqlite and VFS (called once per instance)
* Initialize @rivetkit/sqlite and VFS (called once per instance)
*/
async #ensureInitialized(): Promise<void> {
// Fast path: already initialized
Expand All @@ -238,7 +238,7 @@ export class SqliteVfs {
const wasmPath = require.resolve("@rivetkit/sqlite/dist/wa-sqlite-async.wasm");
const wasmBinary = readFileSync(wasmPath);

// Initialize wa-sqlite module - each instance gets its own module
// Initialize @rivetkit/sqlite module - each instance gets its own module
const module = await SQLiteESMFactory({ wasmBinary });
this.#sqlite3 = Factory(module) as unknown as SQLite3Api;

Expand Down Expand Up @@ -266,7 +266,7 @@ export class SqliteVfs {
// Serialize all open operations within this instance
await this.#openMutex.acquire();
try {
// Initialize wa-sqlite and SqliteSystem on first call
// Initialize @rivetkit/sqlite and SqliteSystem on first call
await this.#ensureInitialized();

if (!this.#sqlite3 || !this.#sqliteSystem) {
Expand Down
Loading
Loading