From 5e205cf1011c534521576f56060d9ef18e920a87 Mon Sep 17 00:00:00 2001
From: Kacper Wojciechowski <39823706+jog1t@users.noreply.github.com>
Date: Thu, 19 Jun 2025 00:31:15 +0200
Subject: [PATCH] feat: sqlite
---
examples/counter/scripts/connect.ts | 8 +-
examples/drizzle/.env.sample | 1 +
examples/drizzle/README.md | 33 +
examples/drizzle/drizzle.config.ts | 6 +
.../drizzle/0000_wonderful_iron_patriot.sql | 8 +
.../drizzle/drizzle/meta/0000_snapshot.json | 64 ++
examples/drizzle/drizzle/meta/_journal.json | 20 +
examples/drizzle/drizzle/migrations.js | 10 +
examples/drizzle/hooks.js | 10 +
examples/drizzle/package.json | 27 +
examples/drizzle/register.js | 15 +
examples/drizzle/src/db/schema.ts | 9 +
examples/drizzle/src/registry.ts | 27 +
examples/drizzle/src/server.ts | 7 +
examples/drizzle/tsconfig.json | 44 ++
.../driver-test-suite/action-timeout.ts | 68 ++
packages/core/src/client/worker-common.ts | 13 +-
packages/core/src/drivers/memory/worker.ts | 4 +
.../core/src/drivers/rivet/worker-driver.ts | 5 +
packages/core/src/registry/config.ts | 3 +-
packages/core/src/test/driver/worker.ts | 4 +
packages/core/src/worker/action.ts | 21 +-
packages/core/src/worker/config.ts | 122 +++-
packages/core/src/worker/connection.ts | 12 +-
packages/core/src/worker/context.ts | 28 +-
packages/core/src/worker/definition.ts | 22 +-
packages/core/src/worker/driver.ts | 7 +
packages/core/src/worker/errors.ts | 9 +
packages/core/src/worker/instance.ts | 86 ++-
packages/core/src/worker/mod.ts | 25 +-
.../core/src/worker/protocol/message/mod.ts | 34 +-
packages/core/tests/worker-types.test.ts | 12 +-
packages/db/package.json | 63 ++
packages/db/src/config.ts | 12 +
packages/db/src/drizzle/mod.ts | 76 ++
packages/db/src/mod.ts | 46 ++
packages/db/tsconfig.json | 9 +
packages/db/tsup.config.ts | 7 +
packages/db/turbo.json | 4 +
packages/drivers/file-system/src/worker.ts | 76 +-
packages/drivers/redis/src/worker.ts | 5 +
.../cloudflare-workers/src/worker-driver.ts | 6 +-
pnpm-lock.yaml | 650 +++++++++++++++++-
43 files changed, 1545 insertions(+), 173 deletions(-)
create mode 100644 examples/drizzle/.env.sample
create mode 100644 examples/drizzle/README.md
create mode 100644 examples/drizzle/drizzle.config.ts
create mode 100644 examples/drizzle/drizzle/0000_wonderful_iron_patriot.sql
create mode 100644 examples/drizzle/drizzle/meta/0000_snapshot.json
create mode 100644 examples/drizzle/drizzle/meta/_journal.json
create mode 100644 examples/drizzle/drizzle/migrations.js
create mode 100644 examples/drizzle/hooks.js
create mode 100644 examples/drizzle/package.json
create mode 100644 examples/drizzle/register.js
create mode 100644 examples/drizzle/src/db/schema.ts
create mode 100644 examples/drizzle/src/registry.ts
create mode 100644 examples/drizzle/src/server.ts
create mode 100644 examples/drizzle/tsconfig.json
create mode 100644 packages/core/fixtures/driver-test-suite/action-timeout.ts
create mode 100644 packages/db/package.json
create mode 100644 packages/db/src/config.ts
create mode 100644 packages/db/src/drizzle/mod.ts
create mode 100644 packages/db/src/mod.ts
create mode 100644 packages/db/tsconfig.json
create mode 100644 packages/db/tsup.config.ts
create mode 100644 packages/db/turbo.json
diff --git a/examples/counter/scripts/connect.ts b/examples/counter/scripts/connect.ts
index 04091852d..b8abc6cd7 100644
--- a/examples/counter/scripts/connect.ts
+++ b/examples/counter/scripts/connect.ts
@@ -1,11 +1,13 @@
///
import { createClient } from "@rivetkit/worker/client";
-import type { Registry } from "../workers/registry";
+import type { Registry } from "../src/workers/registry";
async function main() {
- const client = createClient(process.env.ENDPOINT ?? "http://localhost:6420");
+ const client = createClient(
+ process.env.ENDPOINT ?? "http://127.0.0.1:8080",
+ );
- const counter = client.counter.connect()
+ const counter = (await client.counter.getOrCreate()).connect();
counter.on("newCount", (count: number) => console.log("Event:", count));
diff --git a/examples/drizzle/.env.sample b/examples/drizzle/.env.sample
new file mode 100644
index 000000000..842bef05d
--- /dev/null
+++ b/examples/drizzle/.env.sample
@@ -0,0 +1 @@
+DB_FILE_NAME=file:local.db
\ No newline at end of file
diff --git a/examples/drizzle/README.md b/examples/drizzle/README.md
new file mode 100644
index 000000000..bb41842a6
--- /dev/null
+++ b/examples/drizzle/README.md
@@ -0,0 +1,33 @@
+# Hono Integration for RivetKit
+
+Example project demonstrating Hono web framework integration with [RivetKit](https://rivetkit.org).
+
+[Learn More →](https://github.com/rivet-gg/rivetkit)
+
+[Discord](https://rivet.gg/discord) — [Documentation](https://rivetkit.org) — [Issues](https://github.com/rivet-gg/rivetkit/issues)
+
+## Getting Started
+
+### Prerequisites
+
+- Node.js
+
+### Installation
+
+```sh
+git clone https://github.com/rivet-gg/rivetkit
+cd rivetkit/examples/hono
+npm install
+```
+
+### Development
+
+```sh
+npm run dev
+```
+
+Open your browser to http://localhost:3000 to see the Hono server with RivetKit integration.
+
+## License
+
+Apache 2.0
\ No newline at end of file
diff --git a/examples/drizzle/drizzle.config.ts b/examples/drizzle/drizzle.config.ts
new file mode 100644
index 000000000..cbb8b521e
--- /dev/null
+++ b/examples/drizzle/drizzle.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from "@rivetkit/db/drizzle";
+
+export default defineConfig({
+ out: "./drizzle",
+ schema: "./src/db/schema.ts",
+});
diff --git a/examples/drizzle/drizzle/0000_wonderful_iron_patriot.sql b/examples/drizzle/drizzle/0000_wonderful_iron_patriot.sql
new file mode 100644
index 000000000..2382ea5a2
--- /dev/null
+++ b/examples/drizzle/drizzle/0000_wonderful_iron_patriot.sql
@@ -0,0 +1,8 @@
+CREATE TABLE `users_table` (
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ `name` text NOT NULL,
+ `age` integer NOT NULL,
+ `email` text NOT NULL
+);
+--> statement-breakpoint
+CREATE UNIQUE INDEX `users_table_email_unique` ON `users_table` (`email`);
\ No newline at end of file
diff --git a/examples/drizzle/drizzle/meta/0000_snapshot.json b/examples/drizzle/drizzle/meta/0000_snapshot.json
new file mode 100644
index 000000000..c6434f1d6
--- /dev/null
+++ b/examples/drizzle/drizzle/meta/0000_snapshot.json
@@ -0,0 +1,64 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "22f3d49c-97d5-46ca-b0f1-99950c3efec7",
+ "prevId": "00000000-0000-0000-0000-000000000000",
+ "tables": {
+ "users_table": {
+ "name": "users_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "age": {
+ "name": "age",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "users_table_email_unique": {
+ "name": "users_table_email_unique",
+ "columns": [
+ "email"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/examples/drizzle/drizzle/meta/_journal.json b/examples/drizzle/drizzle/meta/_journal.json
new file mode 100644
index 000000000..5e0783723
--- /dev/null
+++ b/examples/drizzle/drizzle/meta/_journal.json
@@ -0,0 +1,20 @@
+{
+ "version": "7",
+ "dialect": "sqlite",
+ "entries": [
+ {
+ "idx": 0,
+ "version": "6",
+ "when": 1750711614205,
+ "tag": "0000_wonderful_iron_patriot",
+ "breakpoints": true
+ },
+ {
+ "idx": 1,
+ "version": "6",
+ "when": 1750716663518,
+ "tag": "0001_rich_susan_delgado",
+ "breakpoints": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/drizzle/drizzle/migrations.js b/examples/drizzle/drizzle/migrations.js
new file mode 100644
index 000000000..33f0f927c
--- /dev/null
+++ b/examples/drizzle/drizzle/migrations.js
@@ -0,0 +1,10 @@
+import journal from './meta/_journal.json';
+import m0000 from './0000_wonderful_iron_patriot.sql';
+
+ export default {
+ journal,
+ migrations: {
+ m0000,
+ }
+ }
+
\ No newline at end of file
diff --git a/examples/drizzle/hooks.js b/examples/drizzle/hooks.js
new file mode 100644
index 000000000..cc7bce84f
--- /dev/null
+++ b/examples/drizzle/hooks.js
@@ -0,0 +1,10 @@
+export async function load(url, context, nextLoad) {
+ if(url.endsWith('.sql')) {
+ return {
+ shortCircuit: true,
+ format: 'module',
+ source: `export default 'SQL file loaded from ${url}';`
+ }
+ }
+ return nextLoad(url, context)
+}
\ No newline at end of file
diff --git a/examples/drizzle/package.json b/examples/drizzle/package.json
new file mode 100644
index 000000000..f2b525c14
--- /dev/null
+++ b/examples/drizzle/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "example-sqlite",
+ "version": "0.9.0-rc.1",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "tsx --watch src/server.ts",
+ "check-types": "tsc --noEmit"
+ },
+ "devDependencies": {
+ "@types/node": "^22.13.9",
+ "rivetkit": "workspace:*",
+ "tsx": "^3.12.7",
+ "typescript": "^5.5.2"
+ },
+ "dependencies": {
+ "@rivetkit/db": "workspace:0.9.0-rc.1",
+ "drizzle-kit": "^0.31.2",
+ "drizzle-orm": "^0.44.2"
+ },
+ "example": {
+ "platforms": [
+ "*"
+ ]
+ },
+ "stableVersion": "0.8.0"
+}
diff --git a/examples/drizzle/register.js b/examples/drizzle/register.js
new file mode 100644
index 000000000..31ef20696
--- /dev/null
+++ b/examples/drizzle/register.js
@@ -0,0 +1,15 @@
+import {register} from "node:module";
+import { pathToFileURL } from 'node:url';
+
+
+register("./hooks.js", pathToFileURL(__filename))
+
+
+// registerHooks({
+// resolve(specifier, context, nextResolve) {
+// console.log({specifier, context});
+// },
+// load(url, context, nextLoad) {
+// console.log({url, context});
+// },
+// });
\ No newline at end of file
diff --git a/examples/drizzle/src/db/schema.ts b/examples/drizzle/src/db/schema.ts
new file mode 100644
index 000000000..f24058ac8
--- /dev/null
+++ b/examples/drizzle/src/db/schema.ts
@@ -0,0 +1,9 @@
+// import { int, sqliteTable, text } from "@rivetkit/db/drizzle";
+
+// export const usersTable = sqliteTable("users_table", {
+// id: int().primaryKey({ autoIncrement: true }),
+// name: text().notNull(),
+// age: int().notNull(),
+// email: text().notNull().unique(),
+// email2: text().notNull().unique(),
+// });
diff --git a/examples/drizzle/src/registry.ts b/examples/drizzle/src/registry.ts
new file mode 100644
index 000000000..b7896048f
--- /dev/null
+++ b/examples/drizzle/src/registry.ts
@@ -0,0 +1,27 @@
+// import { worker, setup } from "rivetkit";
+// import { db } from "@rivetkit/db/drizzle";
+// import * as schema from "./db/schema";
+// import migrations from "../drizzle/migrations";
+
+// export const counter = worker({
+// db: db({ schema, migrations }),
+// state: {
+// count: 0,
+// },
+// onAuth: () => {
+// // Configure auth here
+// },
+// actions: {
+// increment: (c, x: number) => {
+// // createState or state fix fix fix
+// c.db.c.state.count += x;
+// return c.state.count;
+// },
+// },
+// });
+
+// export const registry = setup({
+// workers: { counter },
+// });
+
+// export type Registry = typeof registry;
diff --git a/examples/drizzle/src/server.ts b/examples/drizzle/src/server.ts
new file mode 100644
index 000000000..5be165d64
--- /dev/null
+++ b/examples/drizzle/src/server.ts
@@ -0,0 +1,7 @@
+// import { registry } from "./registry";
+// import { createMemoryDriver } from "@rivetkit/memory";
+// import { serve } from "@rivetkit/nodejs";
+
+// serve(registry, {
+// driver: createMemoryDriver(),
+// });
diff --git a/examples/drizzle/tsconfig.json b/examples/drizzle/tsconfig.json
new file mode 100644
index 000000000..4f308bb30
--- /dev/null
+++ b/examples/drizzle/tsconfig.json
@@ -0,0 +1,44 @@
+{
+ "compilerOptions": {
+ /* Visit https://aka.ms/tsconfig.json to read more about this file */
+
+ /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
+ "target": "esnext",
+ /* Specify a set of bundled library declaration files that describe the target runtime environment. */
+ "lib": ["esnext"],
+ /* Specify what JSX code is generated. */
+ "jsx": "react-jsx",
+ "allowArbitraryExtensions": true,
+
+ /* Specify what module code is generated. */
+ "module": "esnext",
+ /* Specify how TypeScript looks up a file from a given module specifier. */
+ "moduleResolution": "bundler",
+ /* Specify type package names to be included without being referenced in a source file. */
+ "types": ["node"],
+ /* Enable importing .json files */
+ "resolveJsonModule": true,
+
+ /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
+ "allowJs": true,
+ /* Enable error reporting in type-checked JavaScript files. */
+ "checkJs": false,
+
+ /* Disable emitting files from a compilation. */
+ "noEmit": true,
+
+ /* Ensure that each file can be safely transpiled without relying on other imports. */
+ "isolatedModules": true,
+ /* Allow 'import x from y' when a module doesn't have a default export. */
+ "allowSyntheticDefaultImports": true,
+ /* Ensure that casing is correct in imports. */
+ "forceConsistentCasingInFileNames": true,
+
+ /* Enable all strict type-checking options. */
+ "strict": true,
+
+ /* Skip type checking all .d.ts files. */
+ "skipLibCheck": true
+ },
+ "include": ["src/**/*"]
+}
diff --git a/packages/core/fixtures/driver-test-suite/action-timeout.ts b/packages/core/fixtures/driver-test-suite/action-timeout.ts
new file mode 100644
index 000000000..598d47e8e
--- /dev/null
+++ b/packages/core/fixtures/driver-test-suite/action-timeout.ts
@@ -0,0 +1,68 @@
+import { worker } from "rivetkit";
+
+// Short timeout worker
+export const shortTimeoutWorker = worker({
+ onAuth: () => {},
+ state: { value: 0 },
+ options: {
+ action: {
+ timeout: 50, // 50ms timeout
+ },
+ },
+ actions: {
+ quickAction: async (c) => {
+ return "quick response";
+ },
+ slowAction: async (c) => {
+ // This action should timeout
+ await new Promise((resolve) => setTimeout(resolve, 100));
+ return "slow response";
+ },
+ },
+});
+
+// Long timeout worker
+export const longTimeoutWorker = worker({
+ onAuth: () => {},
+ state: { value: 0 },
+ options: {
+ action: {
+ timeout: 200, // 200ms timeout
+ },
+ },
+ actions: {
+ delayedAction: async (c) => {
+ // This action should complete within timeout
+ await new Promise((resolve) => setTimeout(resolve, 100));
+ return "delayed response";
+ },
+ },
+});
+
+// Default timeout worker
+export const defaultTimeoutWorker = worker({
+ onAuth: () => {},
+ state: { value: 0 },
+ actions: {
+ normalAction: async (c) => {
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ return "normal response";
+ },
+ },
+});
+
+// Sync worker (timeout shouldn't apply)
+export const syncTimeoutWorker = worker({
+ onAuth: () => {},
+ state: { value: 0 },
+ options: {
+ action: {
+ timeout: 50, // 50ms timeout
+ },
+ },
+ actions: {
+ syncAction: (c) => {
+ return "sync response";
+ },
+ },
+});
diff --git a/packages/core/src/client/worker-common.ts b/packages/core/src/client/worker-common.ts
index cabd449f5..4d17862fb 100644
--- a/packages/core/src/client/worker-common.ts
+++ b/packages/core/src/client/worker-common.ts
@@ -2,16 +2,6 @@ import type {
AnyWorkerDefinition,
WorkerDefinition,
} from "@/worker/definition";
-import type * as protoHttpResolve from "@/worker/protocol/http/resolve";
-import type { Encoding } from "@/worker/protocol/serde";
-import type { WorkerQuery } from "@/manager/protocol/query";
-import { logger } from "./log";
-import * as errors from "./errors";
-import { sendHttpRequest } from "./utils";
-import {
- HEADER_WORKER_QUERY,
- HEADER_ENCODING,
-} from "@/worker/router-endpoints";
/**
* Action function returned by Worker connections and handles.
@@ -33,7 +23,8 @@ export type WorkerActionFunction<
* Maps action methods from worker definition to typed function signatures.
*/
export type WorkerDefinitionActions =
- AD extends WorkerDefinition
+ // biome-ignore lint/suspicious/noExplicitAny: safe to use any here
+ AD extends WorkerDefinition
? {
[K in keyof R]: R[K] extends (...args: infer Args) => infer Return
? WorkerActionFunction
diff --git a/packages/core/src/drivers/memory/worker.ts b/packages/core/src/drivers/memory/worker.ts
index d22cb4d6a..c4e404edc 100644
--- a/packages/core/src/drivers/memory/worker.ts
+++ b/packages/core/src/drivers/memory/worker.ts
@@ -32,4 +32,8 @@ export class MemoryWorkerDriver implements WorkerDriver {
worker.onAlarm();
}, delay);
}
+
+ getDatabase(workerId: string): Promise {
+ return Promise.resolve(undefined);
+ }
}
diff --git a/packages/core/src/drivers/rivet/worker-driver.ts b/packages/core/src/drivers/rivet/worker-driver.ts
index 9533f8a01..f7d8a6a2d 100644
--- a/packages/core/src/drivers/rivet/worker-driver.ts
+++ b/packages/core/src/drivers/rivet/worker-driver.ts
@@ -52,4 +52,9 @@ export class RivetWorkerDriver implements WorkerDriver {
worker.onAlarm();
}, timeout);
}
+
+ getDatabase(_workerId: string): Promise {
+ // TODO: Implement database access
+ return Promise.resolve(undefined);
+ }
}
diff --git a/packages/core/src/registry/config.ts b/packages/core/src/registry/config.ts
index 6bfed9437..833b3c1e2 100644
--- a/packages/core/src/registry/config.ts
+++ b/packages/core/src/registry/config.ts
@@ -5,7 +5,7 @@ import { z } from "zod";
export const WorkersSchema = z.record(
z.string(),
- z.custom>(),
+ z.custom>(),
);
export type RegistryWorkers = z.infer;
@@ -21,6 +21,7 @@ export const RegistryConfigSchema = z.object({
* Test configuration.
*
* DO NOT MANUALLY ENABLE. THIS IS USED INTERNALLY.
+ * @internal
**/
test: TestConfigSchema.optional().default({ enabled: false }),
});
diff --git a/packages/core/src/test/driver/worker.ts b/packages/core/src/test/driver/worker.ts
index c6820a2be..1140896b4 100644
--- a/packages/core/src/test/driver/worker.ts
+++ b/packages/core/src/test/driver/worker.ts
@@ -37,4 +37,8 @@ export class TestWorkerDriver implements WorkerDriver {
worker.onAlarm();
}, delay);
}
+
+ getDatabase(workerId: string): Promise {
+ return Promise.resolve(undefined);
+ }
}
diff --git a/packages/core/src/worker/action.ts b/packages/core/src/worker/action.ts
index bc03dd4da..ed9f49119 100644
--- a/packages/core/src/worker/action.ts
+++ b/packages/core/src/worker/action.ts
@@ -1,20 +1,18 @@
-import type { AnyWorkerInstance } from "./instance";
import type { Conn } from "./connection";
import type { Logger } from "@/common/log";
import type { WorkerKey } from "@/common/utils";
import type { Schedule } from "./schedule";
import type { ConnId } from "./connection";
import type { SaveStateOptions } from "./instance";
-import { Actions } from "./config";
-import { WorkerContext } from "./context";
+import type { WorkerContext } from "./context";
/**
* Context for a remote procedure call.
*
* @typeParam A Worker this action belongs to
*/
-export class ActionContext {
- #workerContext: WorkerContext;
+export class ActionContext {
+ #workerContext: WorkerContext;
/**
* Should not be called directly.
@@ -23,8 +21,8 @@ export class ActionContext {
* @param conn - The connection associated with the action
*/
constructor(
- workerContext: WorkerContext,
- public readonly conn: Conn,
+ workerContext: WorkerContext,
+ public readonly conn: Conn,
) {
this.#workerContext = workerContext;
}
@@ -95,10 +93,17 @@ export class ActionContext {
/**
* Gets the map of connections.
*/
- get conns(): Map> {
+ get conns(): Map> {
return this.#workerContext.conns;
}
+ /**
+ * @experimental
+ */
+ get db(): DB {
+ return this.#workerContext.db;
+ }
+
/**
* Forces the state to get saved.
*/
diff --git a/packages/core/src/worker/config.ts b/packages/core/src/worker/config.ts
index a660f131c..8a6aeb917 100644
--- a/packages/core/src/worker/config.ts
+++ b/packages/core/src/worker/config.ts
@@ -24,6 +24,7 @@ export const WorkerConfigSchema = z
connState: z.any().optional(),
createConnState: z.function().optional(),
vars: z.any().optional(),
+ db: z.any().optional(),
createVars: z.function().optional(),
options: z
.object({
@@ -98,11 +99,19 @@ export interface OnConnectOptions {
// This must have only one or the other or else S will not be able to be inferred
//
// Data returned from this handler will be available on `c.state`.
-type CreateState =
+type CreateState =
| { state: S }
| {
createState: (
- c: WorkerContext,
+ c: WorkerContext<
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined
+ >,
opts: CreateStateOptions,
) => S | Promise;
}
@@ -113,11 +122,19 @@ type CreateState =
// This must have only one or the other or else S will not be able to be inferred
//
// Data returned from this handler will be available on `c.conn.state`.
-type CreateConnState =
+type CreateConnState =
| { connState: CS }
| {
createConnState: (
- c: WorkerContext,
+ c: WorkerContext<
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined
+ >,
opts: OnConnectOptions,
) => CS | Promise;
}
@@ -129,7 +146,7 @@ type CreateConnState =
/**
* @experimental
*/
-type CreateVars =
+type CreateVars =
| {
/**
* @experimental
@@ -141,15 +158,23 @@ type CreateVars =
* @experimental
*/
createVars: (
- c: WorkerContext,
+ c: WorkerContext<
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined
+ >,
driverCtx: unknown,
) => V | Promise;
}
| Record;
-export interface Actions {
+export interface Actions {
[Action: string]: (
- c: ActionContext,
+ c: ActionContext,
...args: any[]
) => any;
}
@@ -180,7 +205,8 @@ interface BaseWorkerConfig<
V,
I,
AD,
- R extends Actions,
+ DB,
+ R extends Actions,
> {
/**
* Called on the HTTP server before clients can interact with the worker.
@@ -216,7 +242,7 @@ interface BaseWorkerConfig<
* This is called before any other lifecycle hooks.
*/
onCreate?: (
- c: WorkerContext,
+ c: WorkerContext,
opts: OnCreateOptions,
) => void | Promise;
@@ -228,7 +254,7 @@ interface BaseWorkerConfig<
*
* @returns Void or a Promise that resolves when startup is complete
*/
- onStart?: (c: WorkerContext) => void | Promise;
+ onStart?: (c: WorkerContext) => void | Promise;
/**
* Called when the worker's state changes.
@@ -238,7 +264,10 @@ interface BaseWorkerConfig<
*
* @param newState The updated state
*/
- onStateChange?: (c: WorkerContext, newState: S) => void;
+ onStateChange?: (
+ c: WorkerContext,
+ newState: S,
+ ) => void;
/**
* Called before a client connects to the worker.
@@ -261,7 +290,7 @@ interface BaseWorkerConfig<
* @throws Throw an error to reject the connection
*/
onBeforeConnect?: (
- c: WorkerContext,
+ c: WorkerContext,
opts: OnConnectOptions,
) => void | Promise;
@@ -275,8 +304,8 @@ interface BaseWorkerConfig<
* @returns Void or a Promise that resolves when connection handling is complete
*/
onConnect?: (
- c: WorkerContext,
- conn: Conn,
+ c: WorkerContext,
+ conn: Conn,
) => void | Promise;
/**
@@ -289,8 +318,8 @@ interface BaseWorkerConfig<
* @returns Void or a Promise that resolves when disconnect handling is complete
*/
onDisconnect?: (
- c: WorkerContext,
- conn: Conn,
+ c: WorkerContext,
+ conn: Conn,
) => void | Promise;
/**
@@ -306,7 +335,7 @@ interface BaseWorkerConfig<
* @returns The modified output to send to the client
*/
onBeforeActionResponse?: (
- c: WorkerContext,
+ c: WorkerContext,
name: string,
args: unknown[],
output: Out,
@@ -315,10 +344,32 @@ interface BaseWorkerConfig<
actions: R;
}
+export type DatabaseFactory = (ctx: {
+ createDatabase: () => Promise;
+}) => Promise<{
+ /**
+ * @experimental
+ */
+ db?: DB;
+ /**
+ * @experimental
+ */
+ onMigrate?: () => void | Promise;
+}>;
+
+type WorkerDatabaseConfig =
+ | {
+ /**
+ * @experimental
+ */
+ db: DatabaseFactory;
+ }
+ | Record;
+
// 1. Infer schema
// 2. Omit keys that we'll manually define (because of generics)
// 3. Define our own types that have generic constraints
-export type WorkerConfig = Omit<
+export type WorkerConfig = Omit<
z.infer,
| "actions"
| "onAuth"
@@ -335,11 +386,13 @@ export type WorkerConfig = Omit<
| "createConnState"
| "vars"
| "createVars"
+ | "db"
> &
- BaseWorkerConfig> &
- CreateState &
- CreateConnState &
- CreateVars;
+ BaseWorkerConfig> &
+ CreateState &
+ CreateConnState &
+ CreateVars &
+ WorkerDatabaseConfig;
// See description on `WorkerConfig`
export type WorkerConfigInput<
@@ -349,7 +402,8 @@ export type WorkerConfigInput<
V,
I,
AD,
- R extends Actions,
+ DB,
+ R extends Actions,
> = Omit<
z.input,
| "actions"
@@ -367,11 +421,13 @@ export type WorkerConfigInput<
| "createConnState"
| "vars"
| "createVars"
+ | "db"
> &
- BaseWorkerConfig &
- CreateState &
- CreateConnState &
- CreateVars;
+ BaseWorkerConfig &
+ CreateState &
+ CreateConnState &
+ CreateVars &
+ WorkerDatabaseConfig;
// For testing type definitions:
export function test<
@@ -381,17 +437,19 @@ export function test<
V,
I,
AD,
- R extends Actions,
+ DB,
+ R extends Actions,
>(
- input: WorkerConfigInput,
-): WorkerConfig {
+ input: WorkerConfigInput,
+): WorkerConfig {
const config = WorkerConfigSchema.parse(input) as WorkerConfig<
S,
CP,
CS,
V,
I,
- AD
+ AD,
+ DB
>;
return config;
}
diff --git a/packages/core/src/worker/connection.ts b/packages/core/src/worker/connection.ts
index 8f0645f35..897f35c1a 100644
--- a/packages/core/src/worker/connection.ts
+++ b/packages/core/src/worker/connection.ts
@@ -3,9 +3,9 @@ import * as errors from "./errors";
import { generateSecureToken } from "./utils";
import { CachedSerializer } from "./protocol/serde";
import type { ConnDriver } from "./driver";
-import * as messageToClient from "@/worker/protocol/message/to-client";
+import type * as messageToClient from "@/worker/protocol/message/to-client";
import type { PersistedConn } from "./persisted";
-import * as wsToClient from "@/worker/protocol/message/to-client";
+import type * as wsToClient from "@/worker/protocol/message/to-client";
export function generateConnId(): string {
return crypto.randomUUID();
@@ -17,7 +17,7 @@ export function generateConnToken(): string {
export type ConnId = string;
-export type AnyConn = Conn;
+export type AnyConn = Conn;
/**
* Represents a client connection to a worker.
@@ -26,13 +26,13 @@ export type AnyConn = Conn;
*
* @see {@link https://rivet.gg/docs/connections|Connection Documentation}
*/
-export class Conn {
+export class Conn {
subscriptions: Set = new Set();
#stateEnabled: boolean;
// TODO: Remove this cyclical reference
- #worker: WorkerInstance;
+ #worker: WorkerInstance;
/**
* The proxied state that notifies of changes automatically.
@@ -103,7 +103,7 @@ export class Conn {
* @protected
*/
public constructor(
- worker: WorkerInstance,
+ worker: WorkerInstance,
persist: PersistedConn,
driver: ConnDriver,
stateEnabled: boolean,
diff --git a/packages/core/src/worker/context.ts b/packages/core/src/worker/context.ts
index f7fd07064..6363e2a4b 100644
--- a/packages/core/src/worker/context.ts
+++ b/packages/core/src/worker/context.ts
@@ -1,17 +1,16 @@
-import { Logger } from "@/common/log";
-import { Actions } from "./config";
-import { WorkerInstance, SaveStateOptions } from "./instance";
-import { Conn, ConnId } from "./connection";
-import { WorkerKey } from "@/common/utils";
-import { Schedule } from "./schedule";
+import type { Logger } from "@/common/log";
+import type { WorkerInstance, SaveStateOptions } from "./instance";
+import type { Conn, ConnId } from "./connection";
+import type { WorkerKey } from "@/common/utils";
+import type { Schedule } from "./schedule";
/**
* WorkerContext class that provides access to worker methods and state
*/
-export class WorkerContext {
- #worker: WorkerInstance;
+export class WorkerContext {
+ #worker: WorkerInstance;
- constructor(worker: WorkerInstance) {
+ constructor(worker: WorkerInstance) {
this.#worker = worker;
}
@@ -84,10 +83,19 @@ export class WorkerContext {
/**
* Gets the map of connections.
*/
- get conns(): Map> {
+ get conns(): Map> {
return this.#worker.conns;
}
+ /**
+ * Gets the database.
+ * @experimental
+ * @throws {DatabaseNotEnabled} If the database is not enabled.
+ */
+ get db(): DB {
+ return this.#worker.db;
+ }
+
/**
* Forces the state to get saved.
*
diff --git a/packages/core/src/worker/definition.ts b/packages/core/src/worker/definition.ts
index 617040a38..31bcaf0c0 100644
--- a/packages/core/src/worker/definition.ts
+++ b/packages/core/src/worker/definition.ts
@@ -1,6 +1,6 @@
-import { type WorkerConfig, type Actions } from "./config";
+import type { WorkerConfig, Actions } from "./config";
import { WorkerInstance } from "./instance";
-import { WorkerContext } from "./context";
+import type { WorkerContext } from "./context";
import type { ActionContext } from "./action";
export type AnyWorkerDefinition = WorkerDefinition<
@@ -10,6 +10,7 @@ export type AnyWorkerDefinition = WorkerDefinition<
any,
any,
any,
+ any,
any
>;
@@ -24,9 +25,10 @@ export type WorkerContextOf =
infer V,
infer I,
infer AD,
+ infer DB,
any
>
- ? WorkerContext
+ ? WorkerContext
: never;
/**
@@ -40,9 +42,10 @@ export type ActionContextOf =
infer V,
infer I,
infer AD,
+ infer DB,
any
>
- ? ActionContext
+ ? ActionContext
: never;
export class WorkerDefinition<
@@ -52,19 +55,20 @@ export class WorkerDefinition<
V,
I,
AD,
- R extends Actions,
+ DB,
+ R extends Actions,
> {
- #config: WorkerConfig;
+ #config: WorkerConfig;
- constructor(config: WorkerConfig) {
+ constructor(config: WorkerConfig) {
this.#config = config;
}
- get config(): WorkerConfig {
+ get config(): WorkerConfig {
return this.#config;
}
- instantiate(): WorkerInstance {
+ instantiate(): WorkerInstance {
return new WorkerInstance(this.#config);
}
}
diff --git a/packages/core/src/worker/driver.ts b/packages/core/src/worker/driver.ts
index 7eb2a184a..ae51063fc 100644
--- a/packages/core/src/worker/driver.ts
+++ b/packages/core/src/worker/driver.ts
@@ -17,6 +17,13 @@ export interface WorkerDriver {
// Schedule
setAlarm(worker: AnyWorkerInstance, timestamp: number): Promise;
+ // Database
+ /**
+ * @experimental
+ * This is an experimental API that may change in the future.
+ */
+ getDatabase(workerId: string): Promise;
+
// TODO:
//destroy(): Promise;
//readState(): void;
diff --git a/packages/core/src/worker/errors.ts b/packages/core/src/worker/errors.ts
index fbbe87077..f97b600cd 100644
--- a/packages/core/src/worker/errors.ts
+++ b/packages/core/src/worker/errors.ts
@@ -295,3 +295,12 @@ export class Forbidden extends WorkerError {
this.statusCode = 403;
}
}
+
+export class DatabaseNotEnabled extends WorkerError {
+ constructor() {
+ super(
+ "database_not_enabled",
+ "Database not enabled. Must implement `database` to use database.",
+ );
+ }
+}
diff --git a/packages/core/src/worker/instance.ts b/packages/core/src/worker/instance.ts
index 200356c29..ab078b371 100644
--- a/packages/core/src/worker/instance.ts
+++ b/packages/core/src/worker/instance.ts
@@ -15,7 +15,7 @@ import { instanceLogger, logger } from "./log";
import type { ActionContext } from "./action";
import { DeadlineError, Lock, deadline } from "./utils";
import { Schedule } from "./schedule";
-import * as wsToClient from "@/worker/protocol/message/to-client";
+import type * as wsToClient from "@/worker/protocol/message/to-client";
import type * as wsToServer from "@/worker/protocol/message/to-server";
import { CachedSerializer } from "./protocol/serde";
import { WorkerContext } from "./context";
@@ -37,8 +37,22 @@ export interface SaveStateOptions {
}
/** Worker type alias with all `any` types. Used for `extends` in classes referencing this worker. */
-// biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
-export type AnyWorkerInstance = WorkerInstance;
+export type AnyWorkerInstance = WorkerInstance<
+ // biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
+ any,
+ // biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
+ any,
+ // biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
+ any,
+ // biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
+ any,
+ // biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
+ any,
+ // biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
+ any,
+ // biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
+ any
+>;
export type ExtractWorkerState =
A extends WorkerInstance<
@@ -52,6 +66,8 @@ export type ExtractWorkerState =
// biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
any,
// biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
+ any,
+ // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
any
>
? State
@@ -69,6 +85,8 @@ export type ExtractWorkerConnParams =
// biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
any,
// biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
+ any,
+ // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
any
>
? ConnParams
@@ -86,14 +104,16 @@ export type ExtractWorkerConnState =
// biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
any,
// biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
+ any,
+ // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
any
>
? ConnState
: never;
-export class WorkerInstance {
+export class WorkerInstance {
// Shared worker context for this instance
- workerContext: WorkerContext;
+ workerContext: WorkerContext;
isStopping = false;
#persistChanged = false;
@@ -116,7 +136,7 @@ export class WorkerInstance {
#vars?: V;
#backgroundPromises: Promise[] = [];
- #config: WorkerConfig;
+ #config: WorkerConfig;
#connectionDrivers!: ConnDrivers;
#workerDriver!: WorkerDriver;
#workerId!: string;
@@ -125,12 +145,13 @@ export class WorkerInstance {
#region!: string;
#ready = false;
- #connections = new Map>();
- #subscriptionIndex = new Map>>();
+ #connections = new Map>();
+ #subscriptionIndex = new Map>>();
#schedule!: Schedule;
// inspector!: WorkerInspector;
+ #db!: DB;
get id() {
return this.#workerId;
@@ -143,7 +164,7 @@ export class WorkerInstance {
*
* @private
*/
- constructor(config: WorkerConfig) {
+ constructor(config: WorkerConfig) {
this.#config = config;
this.workerContext = new WorkerContext(this);
}
@@ -181,6 +202,7 @@ export class WorkerInstance {
undefined,
undefined,
undefined,
+ undefined,
undefined
>,
this.#workerDriver.getContext(this.#workerId),
@@ -210,6 +232,17 @@ export class WorkerInstance {
}
}
+ // Setup Database
+ if ("db" in this.#config) {
+ const db = await this.#config.db({
+ createDatabase: () => workerDriver.getDatabase(this.#workerId),
+ });
+
+ logger().info("database migration starting");
+ await db.onMigrate?.();
+ logger().info("database migration complete");
+ }
+
// Set alarm for next scheduled event if any exist after finishing initiation sequence
if (this.#persist.e.length > 0) {
await this.#workerDriver.setAlarm(this, this.#persist.e[0].t);
@@ -491,7 +524,7 @@ export class WorkerInstance {
for (const connPersist of this.#persist.c) {
// Create connections
const driver = this.__getConnDriver(connPersist.d);
- const conn = new Conn(
+ const conn = new Conn(
this,
connPersist,
driver,
@@ -525,6 +558,7 @@ export class WorkerInstance {
undefined,
undefined,
undefined,
+ undefined,
undefined
>,
{ input },
@@ -557,14 +591,14 @@ export class WorkerInstance {
}
}
- __getConnForId(id: string): Conn | undefined {
+ __getConnForId(id: string): Conn | undefined {
return this.#connections.get(id);
}
/**
* Removes a connection and cleans up its resources.
*/
- __removeConn(conn: Conn | undefined) {
+ __removeConn(conn: Conn | undefined) {
if (!conn) {
logger().warn("`conn` does not exist");
return;
@@ -638,6 +672,7 @@ export class WorkerInstance {
undefined,
undefined,
undefined,
+ undefined,
undefined
>,
onBeforeConnectOpts,
@@ -680,7 +715,7 @@ export class WorkerInstance {
driverId: string,
driverState: unknown,
authData: unknown,
- ): Promise> {
+ ): Promise> {
if (this.#connections.has(connectionId)) {
throw new Error(`Connection already exists: ${connectionId}`);
}
@@ -697,7 +732,7 @@ export class WorkerInstance {
a: authData,
su: [],
};
- const conn = new Conn(
+ const conn = new Conn(
this,
persist,
driver,
@@ -753,7 +788,7 @@ export class WorkerInstance {
// MARK: Messages
async processMessage(
message: wsToServer.ToServer,
- conn: Conn,
+ conn: Conn,
) {
await processMessage(message, this, conn, {
onExecuteAction: async (ctx, name, args) => {
@@ -771,7 +806,7 @@ export class WorkerInstance {
// MARK: Events
#addSubscription(
eventName: string,
- connection: Conn,
+ connection: Conn,
fromPersist: boolean,
) {
if (connection.subscriptions.has(eventName)) {
@@ -801,7 +836,7 @@ export class WorkerInstance {
#removeSubscription(
eventName: string,
- connection: Conn,
+ connection: Conn,
fromRemoveConn: boolean,
) {
if (!connection.subscriptions.has(eventName)) {
@@ -860,7 +895,7 @@ export class WorkerInstance {
* @internal
*/
async executeAction(
- ctx: ActionContext,
+ ctx: ActionContext,
actionName: string,
args: unknown[],
): Promise {
@@ -873,7 +908,6 @@ export class WorkerInstance {
}
// Check if the method exists on this object
- // biome-ignore lint/suspicious/noExplicitAny: action name is dynamic from client
const actionFunction = this.#config.actions[actionName];
if (typeof actionFunction !== "function") {
logger().warn("action not found", { actionName: actionName });
@@ -1004,7 +1038,7 @@ export class WorkerInstance {
/**
* Gets the map of connections.
*/
- get conns(): Map> {
+ get conns(): Map> {
return this.#connections;
}
@@ -1018,6 +1052,18 @@ export class WorkerInstance {
return this.#persist.s;
}
+ /**
+ * Gets the database.
+ * @experimental
+ * @throws {DatabaseNotEnabled} If the database is not enabled.
+ */
+ get db(): DB {
+ if (!this.#db) {
+ throw new errors.DatabaseNotEnabled();
+ }
+ return this.#db;
+ }
+
/**
* Sets the current state.
*
diff --git a/packages/core/src/worker/mod.ts b/packages/core/src/worker/mod.ts
index 08c5af305..3dede08b2 100644
--- a/packages/core/src/worker/mod.ts
+++ b/packages/core/src/worker/mod.ts
@@ -20,9 +20,26 @@ export type {
ActionContextOf,
} from "./definition";
-export function worker>(
- input: WorkerConfigInput,
-): WorkerDefinition {
- const config = WorkerConfigSchema.parse(input) as WorkerConfig;
+export function worker<
+ S,
+ CP,
+ CS,
+ V,
+ I,
+ AD,
+ DB,
+ R extends Actions,
+>(
+ input: WorkerConfigInput,
+): WorkerDefinition {
+ const config = WorkerConfigSchema.parse(input) as WorkerConfig<
+ S,
+ CP,
+ CS,
+ V,
+ I,
+ AD,
+ DB
+ >;
return new WorkerDefinition(config);
}
diff --git a/packages/core/src/worker/protocol/message/mod.ts b/packages/core/src/worker/protocol/message/mod.ts
index 9dd20e36c..0d9619e19 100644
--- a/packages/core/src/worker/protocol/message/mod.ts
+++ b/packages/core/src/worker/protocol/message/mod.ts
@@ -1,6 +1,6 @@
-import * as wsToClient from "@/worker/protocol/message/to-client";
+import type * as wsToClient from "@/worker/protocol/message/to-client";
import * as wsToServer from "@/worker/protocol/message/to-server";
-import type { WorkerInstance, AnyWorkerInstance } from "../../instance";
+import type { WorkerInstance } from "../../instance";
import type { Conn } from "../../connection";
import * as errors from "../../errors";
import { logger } from "../../log";
@@ -9,13 +9,11 @@ import { assertUnreachable } from "../../utils";
import { z } from "zod";
import {
deserialize,
- Encoding,
- InputData,
+ type Encoding,
+ type InputData,
CachedSerializer,
} from "@/worker/protocol/serde";
import { deconstructError } from "@/common/utils";
-import { Actions } from "@/worker/config";
-import invariant from "invariant";
export const TransportSchema = z.enum(["websocket", "sse"]);
@@ -69,24 +67,27 @@ export async function parseMessage(
return message;
}
-export interface ProcessMessageHandler {
+export interface ProcessMessageHandler {
onExecuteAction?: (
- ctx: ActionContext,
+ ctx: ActionContext,
name: string,
args: unknown[],
) => Promise;
- onSubscribe?: (eventName: string, conn: Conn) => Promise;
+ onSubscribe?: (
+ eventName: string,
+ conn: Conn,
+ ) => Promise;
onUnsubscribe?: (
eventName: string,
- conn: Conn,
+ conn: Conn,
) => Promise;
}
-export async function processMessage(
+export async function processMessage(
message: wsToServer.ToServer,
- worker: WorkerInstance,
- conn: Conn,
- handler: ProcessMessageHandler,
+ worker: WorkerInstance,
+ conn: Conn,
+ handler: ProcessMessageHandler,
) {
let actionId: number | undefined;
let actionName: string | undefined;
@@ -110,7 +111,10 @@ export async function processMessage(
argsCount: args.length,
});
- const ctx = new ActionContext(worker.workerContext, conn);
+ const ctx = new ActionContext(
+ worker.workerContext,
+ conn,
+ );
// Process the action request and wait for the result
// This will wait for async actions to complete
diff --git a/packages/core/tests/worker-types.test.ts b/packages/core/tests/worker-types.test.ts
index 3fc1a5223..268f9cf87 100644
--- a/packages/core/tests/worker-types.test.ts
+++ b/packages/core/tests/worker-types.test.ts
@@ -30,6 +30,11 @@ describe("WorkerDefinition", () => {
baz: string;
}
+ interface TestDatabase {
+ onMigrate: () => void;
+ client: object;
+ }
+
// For testing type utilities, we don't need a real worker instance
// We just need a properly typed WorkerDefinition to check against
type TestActions = Record;
@@ -40,6 +45,7 @@ describe("WorkerDefinition", () => {
TestVars,
TestInput,
TestAuthData,
+ TestDatabase,
TestActions
>;
@@ -51,7 +57,8 @@ describe("WorkerDefinition", () => {
TestConnState,
TestVars,
TestInput,
- TestAuthData
+ TestAuthData,
+ TestDatabase
>
>();
@@ -67,7 +74,8 @@ describe("WorkerDefinition", () => {
TestConnState,
TestVars,
TestInput,
- TestAuthData
+ TestAuthData,
+ TestDatabase
>
>();
});
diff --git a/packages/db/package.json b/packages/db/package.json
new file mode 100644
index 000000000..0077d44e0
--- /dev/null
+++ b/packages/db/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "@rivetkit/db",
+ "version": "0.9.0-rc.1",
+ "license": "Apache-2.0",
+ "sideEffects": false,
+ "type": "module",
+ "files": [
+ "dist",
+ "package.json"
+ ],
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/mod.d.ts",
+ "default": "./dist/mod.js"
+ },
+ "require": {
+ "types": "./dist/mod.d.cts",
+ "default": "./dist/mod.cjs"
+ }
+ },
+ "./drizzle": {
+ "import": {
+ "types": "./dist/drizzle/mod.d.ts",
+ "default": "./dist/drizzle/mod.js"
+ },
+ "require": {
+ "types": "./dist/drizzle/mod.d.cts",
+ "default": "./dist/drizzle/mod.cjs"
+ }
+ }
+ },
+ "scripts": {
+ "build": "tsup src/mod.ts src/drizzle/mod.ts",
+ "check-types": "tsc --noEmit"
+ },
+ "peerDependencies": {
+ "drizzle-kit": "^0.31.2",
+ "drizzle-orm": "^0.44.2",
+ "rivetkit": "*"
+ },
+ "peerDependenciesMeta": {
+ "drizzle-orm": {
+ "optional": true
+ },
+ "drizzle-kit": {
+ "optional": true
+ }
+ },
+ "devDependencies": {
+ "@types/better-sqlite3": "^7.6.13",
+ "@types/node": "^24.0.4",
+ "drizzle-orm": "^0.44.2",
+ "rivetkit": "workspace:*",
+ "tsup": "^8.3.6",
+ "typescript": "^5.5.2",
+ "vitest": "^3.1.1"
+ },
+ "stableVersion": "0.8.0",
+ "dependencies": {
+ "better-sqlite3": "^11.10.0"
+ }
+}
diff --git a/packages/db/src/config.ts b/packages/db/src/config.ts
new file mode 100644
index 000000000..988e82142
--- /dev/null
+++ b/packages/db/src/config.ts
@@ -0,0 +1,12 @@
+export interface DatabaseConfig {
+ client: DB;
+ onMigrate: () => void;
+}
+
+export interface DatabaseFactoryContext {
+ createDatabase: () => Promise;
+}
+
+export type DatabaseFactory = (
+ ctx: DatabaseFactoryContext,
+) => Promise>;
diff --git a/packages/db/src/drizzle/mod.ts b/packages/db/src/drizzle/mod.ts
new file mode 100644
index 000000000..d7fe198d3
--- /dev/null
+++ b/packages/db/src/drizzle/mod.ts
@@ -0,0 +1,76 @@
+import {
+ type BetterSQLite3Database,
+ drizzle as sqliteDrizzle,
+} from "drizzle-orm/better-sqlite3";
+
+import { migrate as sqliteMigrate } from "drizzle-orm/durable-sqlite/migrator";
+import { drizzle as durableDrizzle } from "drizzle-orm/durable-sqlite";
+import { migrate as durableMigrate } from "drizzle-orm/durable-sqlite/migrator";
+import * as Database from "better-sqlite3";
+import type { DatabaseFactory } from "@/config";
+
+export * from "drizzle-orm/sqlite-core";
+
+import { defineConfig as originalDefineConfig, type Config } from "drizzle-kit";
+
+export function defineConfig(
+ config: Partial,
+): Config {
+ // This is a workaround to avoid the "drizzle-kit" import issue in the examples.
+ // It allows us to use the same defineConfig function in both the main package and the examples.
+ return originalDefineConfig({
+ dialect: "sqlite",
+ driver: "durable-sqlite",
+ ...config,
+ });
+}
+
+interface DatabaseFactoryConfig<
+ TSchema extends Record = Record,
+> {
+ /**
+ * The database schema.
+ */
+ schema?: TSchema;
+ migrations?: any;
+}
+
+export function db<
+ TSchema extends Record = Record,
+>(
+ config?: DatabaseFactoryConfig,
+): DatabaseFactory> {
+ return async (ctx) => {
+ const conn = await ctx.createDatabase();
+
+ if (!conn) {
+ throw new Error(
+ "Cannot create database connection, or database feature is not enabled.",
+ );
+ }
+
+ if (typeof conn === "object" && conn && "exec" in conn) {
+ // If the connection is already an object with exec method, return it
+ // i.e. in serverless environments (Cloudflare Workers)
+ const client = durableDrizzle(conn, config);
+ return {
+ client,
+ onMigrate: async () => {
+ await durableMigrate(client, config?.migrations);
+ },
+ };
+ }
+
+ const client = sqliteDrizzle({
+ client: new Database(conn as string),
+ ...config,
+ });
+
+ return {
+ client,
+ onMigrate: async () => {
+ await sqliteMigrate(client, config?.migrations);
+ },
+ };
+ };
+}
diff --git a/packages/db/src/mod.ts b/packages/db/src/mod.ts
new file mode 100644
index 000000000..47e0172d5
--- /dev/null
+++ b/packages/db/src/mod.ts
@@ -0,0 +1,46 @@
+import * as SQLite from "better-sqlite3";
+import type { DatabaseFactory } from "./config";
+
+/**
+ * On serverless environments, we use a shim, as not all methods are available.
+ * This is a minimal shim that only includes the `exec` method, which is used for
+ * running raw SQL commands.
+ */
+type SQLiteShim = Pick;
+
+interface DatabaseFactoryConfig {
+ onMigrate?: (db: SQLiteShim) => void;
+}
+
+export function db({
+ onMigrate,
+}: DatabaseFactoryConfig = {}): DatabaseFactory {
+ return async (ctx) => {
+ const conn = await ctx.createDatabase();
+
+ if (!conn) {
+ throw new Error(
+ "Cannot create database connection, or database feature is not enabled.",
+ );
+ }
+
+ if (typeof conn === "object" && conn && "exec" in conn) {
+ // if the connection is already an object with exec method, return it
+ // i.e. in serverless environments (cloudflare)
+ return {
+ client: conn as SQLiteShim,
+ onMigrate: () => {
+ onMigrate?.(client);
+ },
+ };
+ }
+
+ const client = new SQLite(conn as string);
+ return {
+ client,
+ onMigrate: () => {
+ onMigrate?.(client);
+ },
+ };
+ };
+}
diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json
new file mode 100644
index 000000000..b98b96a49
--- /dev/null
+++ b/packages/db/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["src/**/*"]
+}
diff --git a/packages/db/tsup.config.ts b/packages/db/tsup.config.ts
new file mode 100644
index 000000000..e1becbf0a
--- /dev/null
+++ b/packages/db/tsup.config.ts
@@ -0,0 +1,7 @@
+import defaultConfig from "../../tsup.base.ts";
+import { defineConfig } from "tsup";
+
+export default defineConfig({
+ ...defaultConfig,
+ external: ["better-sqlite3"],
+});
diff --git a/packages/db/turbo.json b/packages/db/turbo.json
new file mode 100644
index 000000000..95960709b
--- /dev/null
+++ b/packages/db/turbo.json
@@ -0,0 +1,4 @@
+{
+ "$schema": "https://turbo.build/schema.json",
+ "extends": ["//"]
+}
diff --git a/packages/drivers/file-system/src/worker.ts b/packages/drivers/file-system/src/worker.ts
index 96c5fc160..9b7412da6 100644
--- a/packages/drivers/file-system/src/worker.ts
+++ b/packages/drivers/file-system/src/worker.ts
@@ -7,42 +7,46 @@ export type WorkerDriverContext = Record