diff --git a/.gitignore b/.gitignore index bfb1e5e86..1aefe2118 100644 --- a/.gitignore +++ b/.gitignore @@ -184,3 +184,5 @@ Cargo.lock **/.wrangler **/.DS_Store .aider* + +packages/**/tsup.config.bundled_*.mjs \ No newline at end of file diff --git a/examples/drizzle/drizzle/0000_wonderful_iron_patriot.sql b/examples/drizzle/drizzle/0000_moaning_tomorrow_man.sql similarity index 100% rename from examples/drizzle/drizzle/0000_wonderful_iron_patriot.sql rename to examples/drizzle/drizzle/0000_moaning_tomorrow_man.sql diff --git a/examples/drizzle/drizzle/meta/0000_snapshot.json b/examples/drizzle/drizzle/meta/0000_snapshot.json index c6434f1d6..c1a874381 100644 --- a/examples/drizzle/drizzle/meta/0000_snapshot.json +++ b/examples/drizzle/drizzle/meta/0000_snapshot.json @@ -1,7 +1,7 @@ { "version": "6", "dialect": "sqlite", - "id": "22f3d49c-97d5-46ca-b0f1-99950c3efec7", + "id": "5175c351-75eb-4f1d-9211-7ee520f0b9c2", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "users_table": { diff --git a/examples/drizzle/drizzle/meta/_journal.json b/examples/drizzle/drizzle/meta/_journal.json index 5e0783723..31667078b 100644 --- a/examples/drizzle/drizzle/meta/_journal.json +++ b/examples/drizzle/drizzle/meta/_journal.json @@ -5,15 +5,8 @@ { "idx": 0, "version": "6", - "when": 1750711614205, - "tag": "0000_wonderful_iron_patriot", - "breakpoints": true - }, - { - "idx": 1, - "version": "6", - "when": 1750716663518, - "tag": "0001_rich_susan_delgado", + "when": 1750976447195, + "tag": "0000_moaning_tomorrow_man", "breakpoints": true } ] diff --git a/examples/drizzle/drizzle/migrations.js b/examples/drizzle/drizzle/migrations.js index 33f0f927c..a7ebc8ab1 100644 --- a/examples/drizzle/drizzle/migrations.js +++ b/examples/drizzle/drizzle/migrations.js @@ -1,10 +1,10 @@ import journal from './meta/_journal.json'; -import m0000 from './0000_wonderful_iron_patriot.sql'; +import m0000 from './0000_moaning_tomorrow_man.sql'; export default { journal, migrations: { - m0000, + m0000 } } \ No newline at end of file diff --git a/examples/drizzle/hooks.js b/examples/drizzle/hooks.js deleted file mode 100644 index cc7bce84f..000000000 --- a/examples/drizzle/hooks.js +++ /dev/null @@ -1,10 +0,0 @@ -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 index f2b525c14..f110ac9b6 100644 --- a/examples/drizzle/package.json +++ b/examples/drizzle/package.json @@ -4,17 +4,19 @@ "private": true, "type": "module", "scripts": { - "dev": "tsx --watch src/server.ts", + "dev": "tsx --loader @rivetkit/sql-loader --watch src/server.ts", "check-types": "tsc --noEmit" }, "devDependencies": { + "@rivetkit/actor": "workspace:*", + "@rivetkit/sql-loader": "workspace:*", "@types/node": "^22.13.9", - "rivetkit": "workspace:*", "tsx": "^3.12.7", "typescript": "^5.5.2" }, "dependencies": { "@rivetkit/db": "workspace:0.9.0-rc.1", + "better-sqlite3": "^12.1.1", "drizzle-kit": "^0.31.2", "drizzle-orm": "^0.44.2" }, diff --git a/examples/drizzle/register.js b/examples/drizzle/register.js deleted file mode 100644 index 31ef20696..000000000 --- a/examples/drizzle/register.js +++ /dev/null @@ -1,15 +0,0 @@ -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/scripts/connect.ts b/examples/drizzle/scripts/connect.ts new file mode 100644 index 000000000..3462c1994 --- /dev/null +++ b/examples/drizzle/scripts/connect.ts @@ -0,0 +1,30 @@ +/// +import { createClient } from "@rivetkit/actor/client"; +import type { Registry } from "../src/registry"; + +async function main() { + const client = createClient( + process.env.ENDPOINT ?? "http://127.0.0.1:8080", + ); + + const contacts = client.contacts.getOrCreate(); + + // counter.on("newCount", (count: number) => console.log("Event:", count)); + + for (let i = 0; i < 5; i++) { + const out = await contacts.insert({ + name: `User ${i}`, + age: 20 + i, + email: `example+${i}@example.com`, + }); + console.log("Inserted:", out); + } + + console.log("Reading all users:"); + const users = await contacts.read(); + console.log(users); + + // await counter.dispose(); +} + +main(); diff --git a/examples/drizzle/src/db/schema.ts b/examples/drizzle/src/db/schema.ts index f24058ac8..eb07503d6 100644 --- a/examples/drizzle/src/db/schema.ts +++ b/examples/drizzle/src/db/schema.ts @@ -1,9 +1,8 @@ -// import { int, sqliteTable, text } from "@rivetkit/db/drizzle"; +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(), -// }); +export const usersTable = sqliteTable("users_table", { + id: int().primaryKey({ autoIncrement: true }), + name: text().notNull(), + age: int().notNull(), + email: text().notNull().unique(), +}); diff --git a/examples/drizzle/src/registry.ts b/examples/drizzle/src/registry.ts index 98a14f9cc..2d598a3eb 100644 --- a/examples/drizzle/src/registry.ts +++ b/examples/drizzle/src/registry.ts @@ -1,27 +1,34 @@ -// import { actor, setup } from "rivetkit"; -// import { db } from "@rivetkit/db/drizzle"; -// import * as schema from "./db/schema"; -// import migrations from "../drizzle/migrations"; +import { actor, setup } from "@rivetkit/actor"; +import { db } from "@rivetkit/db/drizzle"; +import * as schema from "./db/schema"; +import migrations from "../drizzle/migrations"; -// export const counter = actor({ -// 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 contacts = actor({ + db: db({ schema, migrations }), + onAuth: async (c) => {}, + actions: { + insert: async (c, record: { name: string; age: number; email: string }) => { + // Example of using the DB + const result = await c.db.insert(schema.usersTable).values(record); + return result; + }, + read: async (c) => { + // Example of reading from the DB + const users = await c.db.query.usersTable.findMany(); + return users; + }, + search: async (c, query: string) => { + // Example of searching in the DB + const users = await c.db.query.usersTable.findMany({ + where: (table, { ilike }) => ilike(table.name, `%${query}%`), + }); + return users; + }, + }, +}); -// export const registry = setup({ -// use: { counter }, -// }); +export const registry = setup({ + use: { contacts }, +}); -// export type Registry = typeof registry; +export type Registry = typeof registry; diff --git a/examples/drizzle/src/server.ts b/examples/drizzle/src/server.ts index 5be165d64..fee7051e9 100644 --- a/examples/drizzle/src/server.ts +++ b/examples/drizzle/src/server.ts @@ -1,7 +1,3 @@ -// import { registry } from "./registry"; -// import { createMemoryDriver } from "@rivetkit/memory"; -// import { serve } from "@rivetkit/nodejs"; +import { registry } from "./registry"; -// serve(registry, { -// driver: createMemoryDriver(), -// }); +await registry.runServer(); diff --git a/packages/core/package.json b/packages/core/package.json index 68c882412..d6ae35834 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -135,6 +135,16 @@ "types": "./dist/test/mod.d.cts", "default": "./dist/test/mod.cjs" } + }, + "./db": { + "import": { + "types": "./dist/db/mod.d.ts", + "default": "./dist/db/mod.js" + }, + "require": { + "types": "./dist/db/mod.d.cts", + "default": "./dist/bd/mod.cjs" + } } }, "engines": { @@ -143,7 +153,7 @@ "sideEffects": false, "scripts": { "dev": "pnpm build --watch", - "build": "tsup src/mod.ts src/client/mod.ts src/common/log.ts src/common/websocket.ts src/actor/errors.ts src/topologies/coordinate/mod.ts src/topologies/partition/mod.ts src/utils.ts src/drivers/rivet/mod.ts src/driver-helpers/mod.ts src/driver-test-suite/mod.ts src/test/mod.ts", + "build": "tsup src/mod.ts src/client/mod.ts src/common/log.ts src/common/websocket.ts src/actor/errors.ts src/topologies/coordinate/mod.ts src/topologies/partition/mod.ts src/utils.ts src/drivers/rivet/mod.ts src/driver-helpers/mod.ts src/driver-test-suite/mod.ts src/test/mod.ts src/db/mod.ts", "check-types": "tsc --noEmit", "boop": "tsc --outDir dist/test -d", "test": "vitest run", diff --git a/packages/core/src/actor/action.ts b/packages/core/src/actor/action.ts index 6c6f86f64..1ccd16e1a 100644 --- a/packages/core/src/actor/action.ts +++ b/packages/core/src/actor/action.ts @@ -5,15 +5,16 @@ import type { ConnId } from "./connection"; import type { ActorContext } from "./context"; import type { SaveStateOptions } from "./instance"; import type { Schedule } from "./schedule"; -import { Registry } from "@/registry/mod"; -import { Client } from "@/client/client"; +import type { Registry } from "@/registry/mod"; +import type { Client } from "@/client/client"; +import type { AnyDatabaseClient, DatabaseProviderOf } from "@/db/mod"; /** * Context for a remote procedure call. * * @typeParam A Actor this action belongs to */ -export class ActionContext { +export class ActionContext { #actorContext: ActorContext; /** @@ -24,7 +25,7 @@ export class ActionContext { */ constructor( actorContext: ActorContext, - public readonly conn: Conn, + public readonly conn: Conn>, ) { this.#actorContext = actorContext; } @@ -95,7 +96,7 @@ export class ActionContext { /** * Gets the map of connections. */ - get conns(): Map> { + get conns(): Map>> { return this.#actorContext.conns; } diff --git a/packages/core/src/actor/config.ts b/packages/core/src/actor/config.ts index 17d42b5df..9fb01d329 100644 --- a/packages/core/src/actor/config.ts +++ b/packages/core/src/actor/config.ts @@ -2,6 +2,12 @@ import { z } from "zod"; import type { ActionContext } from "./action"; import type { Conn } from "./connection"; import type { ActorContext } from "./context"; +import type { + AnyDatabaseClient, + AnyDatabaseProvider, + DatabaseClientOf, + DatabaseProviderOf, +} from "@/db/mod"; // This schema is used to validate the input at runtime. The generic types are defined below in `ActorConfig`. // @@ -32,6 +38,7 @@ export const ActorConfigSchema = z .object({ createVarsTimeout: z.number().positive().default(5000), createConnStateTimeout: z.number().positive().default(5000), + migrationTimeout: z.number().positive().default(10000), onConnectTimeout: z.number().positive().default(5000), }) .strict() @@ -110,7 +117,7 @@ type CreateState = undefined, undefined, undefined, - undefined + AnyDatabaseClient >, opts: CreateStateOptions, ) => S | Promise; @@ -133,7 +140,7 @@ type CreateConnState = undefined, undefined, undefined, - undefined + AnyDatabaseClient >, opts: OnConnectOptions, ) => CS | Promise; @@ -165,14 +172,14 @@ type CreateVars = undefined, undefined, undefined, - undefined + AnyDatabaseClient >, driverCtx: unknown, ) => V | Promise; } | Record; -export interface Actions { +export interface Actions { [Action: string]: ( c: ActionContext, ...args: any[] @@ -205,8 +212,8 @@ interface BaseActorConfig< V, I, AD, - DB, - R extends Actions, + DB extends AnyDatabaseProvider, + R extends Actions>, > { /** * Called on the HTTP server before clients can interact with the actor. @@ -242,7 +249,7 @@ interface BaseActorConfig< * This is called before any other lifecycle hooks. */ onCreate?: ( - c: ActorContext, + c: ActorContext>, opts: OnCreateOptions, ) => void | Promise; @@ -254,7 +261,9 @@ interface BaseActorConfig< * * @returns Void or a Promise that resolves when startup is complete */ - onStart?: (c: ActorContext) => void | Promise; + onStart?: ( + c: ActorContext>, + ) => void | Promise; /** * Called when the actor's state changes. @@ -265,7 +274,7 @@ interface BaseActorConfig< * @param newState The updated state */ onStateChange?: ( - c: ActorContext, + c: ActorContext>, newState: S, ) => void; @@ -290,7 +299,7 @@ interface BaseActorConfig< * @throws Throw an error to reject the connection */ onBeforeConnect?: ( - c: ActorContext, + c: ActorContext>, opts: OnConnectOptions, ) => void | Promise; @@ -304,7 +313,7 @@ interface BaseActorConfig< * @returns Void or a Promise that resolves when connection handling is complete */ onConnect?: ( - c: ActorContext, + c: ActorContext>, conn: Conn, ) => void | Promise; @@ -318,7 +327,7 @@ interface BaseActorConfig< * @returns Void or a Promise that resolves when disconnect handling is complete */ onDisconnect?: ( - c: ActorContext, + c: ActorContext>, conn: Conn, ) => void | Promise; @@ -335,7 +344,7 @@ interface BaseActorConfig< * @returns The modified output to send to the client */ onBeforeActionResponse?: ( - c: ActorContext, + c: ActorContext>, name: string, args: unknown[], output: Out, @@ -344,32 +353,27 @@ interface BaseActorConfig< actions: R; } -export type DatabaseFactory = (ctx: { - createDatabase: () => Promise; -}) => Promise<{ - /** - * @experimental - */ - db?: DB; - /** - * @experimental - */ - onMigrate?: () => void | Promise; -}>; - -type ActorDatabaseConfig = +type ActorDatabase = | { /** * @experimental */ - db: DatabaseFactory; + db: DB; } | 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 ActorConfig = Omit< +export type ActorConfig< + S, + CP, + CS, + V, + I, + AD, + DB extends AnyDatabaseProvider, +> = Omit< z.infer, | "actions" | "onAuth" @@ -388,11 +392,20 @@ export type ActorConfig = Omit< | "createVars" | "db" > & - BaseActorConfig> & - CreateState & - CreateConnState & - CreateVars & - ActorDatabaseConfig; + BaseActorConfig< + S, + CP, + CS, + V, + I, + AD, + DB, + Actions> + > & + CreateState> & + CreateConnState> & + CreateVars> & + ActorDatabase>; // See description on `ActorConfig` export type ActorConfigInput< @@ -402,8 +415,8 @@ export type ActorConfigInput< V, I, AD, - DB, - R extends Actions, + DB extends AnyDatabaseProvider, + R extends Actions>, > = Omit< z.input, | "actions" @@ -427,7 +440,7 @@ export type ActorConfigInput< CreateState & CreateConnState & CreateVars & - ActorDatabaseConfig; + ActorDatabase; // For testing type definitions: export function test< @@ -437,8 +450,8 @@ export function test< V, I, AD, - DB, - R extends Actions, + DB extends AnyDatabaseProvider, + R extends Actions>, >( input: ActorConfigInput, ): ActorConfig { diff --git a/packages/core/src/actor/connection.ts b/packages/core/src/actor/connection.ts index d4dde5695..f5f54fa9d 100644 --- a/packages/core/src/actor/connection.ts +++ b/packages/core/src/actor/connection.ts @@ -6,6 +6,7 @@ import type { ActorInstance } from "./instance"; import type { PersistedConn } from "./persisted"; import { CachedSerializer } from "./protocol/serde"; import { generateSecureToken } from "./utils"; +import type { AnyDatabaseProvider } from "@/db/mod"; export function generateConnId(): string { return crypto.randomUUID(); @@ -26,7 +27,7 @@ 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; @@ -52,8 +53,8 @@ export class Conn { return this.__persist.p; } - public get auth(): unknown { - return this.__persist.a; + public get auth(): AD { + return this.__persist.a as AD; } public get _stateEnabled() { diff --git a/packages/core/src/actor/context.ts b/packages/core/src/actor/context.ts index 1e1a51670..76a265789 100644 --- a/packages/core/src/actor/context.ts +++ b/packages/core/src/actor/context.ts @@ -3,16 +3,19 @@ import type { Logger } from "@/common/log"; import type { Conn, ConnId } from "./connection"; import type { ActorInstance, SaveStateOptions } from "./instance"; import type { Schedule } from "./schedule"; -import { Client } from "@/client/client"; -import { Registry } from "@/registry/mod"; +import type { Client } from "@/client/client"; +import type { Registry } from "@/registry/mod"; +import type { AnyDatabaseClient, DatabaseProviderOf } from "@/db/mod"; /** * ActorContext class that provides access to actor methods and state */ -export class ActorContext { - #actor: ActorInstance; +export class ActorContext { + #actor: ActorInstance>; - constructor(actor: ActorInstance) { + constructor( + actor: ActorInstance>, + ) { this.#actor = actor; } @@ -85,7 +88,7 @@ export class ActorContext { /** * Gets the map of connections. */ - get conns(): Map> { + get conns(): Map>> { return this.#actor.conns; } diff --git a/packages/core/src/actor/definition.ts b/packages/core/src/actor/definition.ts index c4426772f..9d1925b18 100644 --- a/packages/core/src/actor/definition.ts +++ b/packages/core/src/actor/definition.ts @@ -1,3 +1,4 @@ +import type { AnyDatabaseProvider, DatabaseClientOf } from "@/db/mod"; import type { ActionContext } from "./action"; import type { Actions, ActorConfig } from "./config"; import type { ActorContext } from "./context"; @@ -28,7 +29,7 @@ export type ActorContextOf = infer DB, any > - ? ActorContext + ? ActorContext> : never; /** @@ -45,7 +46,7 @@ export type ActionContextOf = infer DB, any > - ? ActionContext + ? ActionContext> : never; export class ActorDefinition< @@ -55,8 +56,8 @@ export class ActorDefinition< V, I, AD, - DB, - R extends Actions, + DB extends AnyDatabaseProvider, + R extends Actions>, > { #config: ActorConfig; diff --git a/packages/core/src/actor/driver.ts b/packages/core/src/actor/driver.ts index 364cd1592..198b7a41b 100644 --- a/packages/core/src/actor/driver.ts +++ b/packages/core/src/actor/driver.ts @@ -2,6 +2,7 @@ import type * as messageToClient from "@/actor/protocol/message/to-client"; import type { CachedSerializer } from "@/actor/protocol/serde"; import type { AnyConn } from "./connection"; import type { AnyActorInstance } from "./instance"; +import type { ActorDatabaseConnectionDetails } from "@/db/mod"; export type ConnDrivers = Record; @@ -22,7 +23,7 @@ export interface ActorDriver { * @experimental * This is an experimental API that may change in the future. */ - getDatabase(actorId: string): Promise; + getDatabase?(actorId: string): Promise; // TODO: //destroy(): Promise; diff --git a/packages/core/src/actor/instance.ts b/packages/core/src/actor/instance.ts index a0f4190de..123e07ede 100644 --- a/packages/core/src/actor/instance.ts +++ b/packages/core/src/actor/instance.ts @@ -24,6 +24,11 @@ import { processMessage } from "./protocol/message/mod"; import { CachedSerializer } from "./protocol/serde"; import { Schedule } from "./schedule"; import { DeadlineError, Lock, deadline } from "./utils"; +import type { + AnyDatabaseClient, + AnyDatabaseProvider, + DatabaseClientOf, +} from "@/db/mod"; /** * Options for the `_saveState` method. @@ -110,9 +115,17 @@ export type ExtractActorConnState = ? ConnState : never; -export class ActorInstance { +export class ActorInstance< + S, + CP, + CS, + V, + I, + AD, + DB extends AnyDatabaseProvider, +> { // Shared actor context for this instance - actorContext: ActorContext; + actorContext: ActorContext>; isStopping = false; #persistChanged = false; @@ -151,7 +164,7 @@ export class ActorInstance { #schedule!: Schedule; // inspector!: ActorInspector; - #db!: DB; + #db!: DatabaseClientOf; get id() { return this.#actorId; @@ -209,7 +222,7 @@ export class ActorInstance { undefined, undefined, undefined, - undefined + AnyDatabaseClient >, this.#actorDriver.getContext(this.#actorId), ); @@ -240,13 +253,16 @@ export class ActorInstance { // Setup Database if ("db" in this.#config) { - const db = await this.#config.db({ - createDatabase: () => actorDriver.getDatabase(this.#actorId), - }); + const getDatabase = this.#actorDriver.getDatabase; + invariant(getDatabase, "Chosen actor driver does not support databases"); + const actorDatabase = await this.#config.db.setup({ + setupDatabase: () => getDatabase(this.#actorId), + }); logger().info("database migration starting"); - await db.onMigrate?.(); + await actorDatabase.onMigrate(); logger().info("database migration complete"); + this.#db = actorDatabase.client as DatabaseClientOf; } // Set alarm for next scheduled event if any exist after finishing initiation sequence @@ -565,7 +581,7 @@ export class ActorInstance { undefined, undefined, undefined, - undefined + AnyDatabaseClient >, { input }, ); @@ -679,7 +695,7 @@ export class ActorInstance { undefined, undefined, undefined, - undefined + AnyDatabaseClient >, onBeforeConnectOpts, ); @@ -901,11 +917,11 @@ export class ActorInstance { * @internal */ async executeAction( - ctx: ActionContext, + ctx: ActionContext>, actionName: string, args: unknown[], ): Promise { - invariant(this.#ready, "exucuting action before ready"); + invariant(this.#ready, "executing action before ready"); // Prevent calling private or reserved methods if (!(actionName in this.#config.actions)) { @@ -1063,7 +1079,7 @@ export class ActorInstance { * @experimental * @throws {DatabaseNotEnabled} If the database is not enabled. */ - get db(): DB { + get db(): DatabaseClientOf { if (!this.#db) { throw new errors.DatabaseNotEnabled(); } diff --git a/packages/core/src/actor/mod.ts b/packages/core/src/actor/mod.ts index e4d7f30c4..000a4bdde 100644 --- a/packages/core/src/actor/mod.ts +++ b/packages/core/src/actor/mod.ts @@ -1,3 +1,4 @@ +import type { AnyDatabaseProvider, DatabaseClientOf } from "@/db/mod"; import { type Actions, type ActorConfig, @@ -26,8 +27,8 @@ export function actor< V, I, AD, - DB, - R extends Actions, + DB extends AnyDatabaseProvider, + R extends Actions>, >( input: ActorConfigInput, ): ActorDefinition { diff --git a/packages/core/src/actor/protocol/message/mod.ts b/packages/core/src/actor/protocol/message/mod.ts index 3cfcb35c1..d1c0a3620 100644 --- a/packages/core/src/actor/protocol/message/mod.ts +++ b/packages/core/src/actor/protocol/message/mod.ts @@ -14,6 +14,11 @@ import * as errors from "../../errors"; import type { ActorInstance } from "../../instance"; import { logger } from "../../log"; import { assertUnreachable } from "../../utils"; +import type { + AnyDatabaseClient, + AnyDatabaseProvider, + DatabaseClientOf, +} from "@/db/mod"; export const TransportSchema = z.enum(["websocket", "sse"]); @@ -67,9 +72,17 @@ export async function parseMessage( return message; } -export interface ProcessMessageHandler { +export interface ProcessMessageHandler< + S, + CP, + CS, + V, + I, + AD, + DB extends AnyDatabaseProvider, +> { onExecuteAction?: ( - ctx: ActionContext, + ctx: ActionContext>, name: string, args: unknown[], ) => Promise; @@ -83,7 +96,15 @@ export interface ProcessMessageHandler { ) => Promise; } -export async function processMessage( +export async function processMessage< + S, + CP, + CS, + V, + I, + AD, + DB extends AnyDatabaseProvider, +>( message: wsToServer.ToServer, actor: ActorInstance, conn: Conn, @@ -111,7 +132,7 @@ export async function processMessage( argsCount: args.length, }); - const ctx = new ActionContext( + const ctx = new ActionContext>( actor.actorContext, conn, ); diff --git a/packages/core/src/db/mod.ts b/packages/core/src/db/mod.ts new file mode 100644 index 000000000..0fb263ac5 --- /dev/null +++ b/packages/core/src/db/mod.ts @@ -0,0 +1,27 @@ +export interface DatabaseSetupResult { + client: DB; + onMigrate: () => Promise; +} + +export type ActorDatabaseConnectionDetails = + | { url: string } + | { exec: (...args: any[]) => void } + | unknown; +export interface DatabaseSetupContext { + /** + * Sets up the database for the actor. + * @returns A promise that resolves to the database URL, database instance + */ + setupDatabase: () => Promise; +} + +export type DatabaseSetupFunction = ( + ctx: DatabaseSetupContext, +) => Promise>; + +export type AnyDatabaseProvider = DatabaseProviderOf; +export type DatabaseProviderOf = { setup: DatabaseSetupFunction }; +export type DatabaseClientOf = DB extends DatabaseProviderOf + ? C + : never; +export type AnyDatabaseClient = DatabaseClientOf; diff --git a/packages/core/src/drivers/memory/actor.ts b/packages/core/src/drivers/memory/actor.ts index 5f4032f00..6fa10711a 100644 --- a/packages/core/src/drivers/memory/actor.ts +++ b/packages/core/src/drivers/memory/actor.ts @@ -1,5 +1,6 @@ import type { ActorDriver, AnyActorInstance } from "@/driver-helpers/mod"; import type { MemoryGlobalState } from "./global-state"; +import type { ActorDatabaseConnectionDetails } from "@/db/mod"; export type ActorDriverContext = Record; @@ -33,7 +34,7 @@ export class MemoryActorDriver implements ActorDriver { }, delay); } - getDatabase(actorId: string): Promise { - return Promise.resolve(undefined); + async getDatabase(actorId: string): Promise { + return { url: ":memory:" }; } } diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 7db7488ac..12a180d9a 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -39,7 +39,6 @@ export type UpgradeWebSocket = ( */ export function createDefaultDriver(): DriverConfig { const driver = getEnvUniversal("RIVETKIT_DRIVER"); - console.log("driver", driver); if (!driver || driver === "memory") { logger().info("using default memory driver"); return createMemoryDriver(); diff --git a/packages/core/tests/actor-types.test.ts b/packages/core/tests/actor-types.test.ts index b7a8be8f8..defb0fc71 100644 --- a/packages/core/tests/actor-types.test.ts +++ b/packages/core/tests/actor-types.test.ts @@ -1,5 +1,6 @@ import type { ActorContext } from "@/actor/context"; import type { ActorContextOf, ActorDefinition } from "@/actor/definition"; +import { DatabaseSetupFunction } from "@/db/mod"; import { describe, expectTypeOf, it } from "vitest"; describe("ActorDefinition", () => { @@ -30,10 +31,10 @@ describe("ActorDefinition", () => { baz: string; } - interface TestDatabase { - onMigrate: () => void; - client: object; - } + type TestDatabase = DatabaseSetupFunction<{ + client: any; // Replace with actual database client type if needed + onMigrate: () => Promise | void; + }>; // For testing type utilities, we don't need a real actor instance // We just need a properly typed ActorDefinition to check against diff --git a/packages/db/package.json b/packages/db/package.json index 0077d44e0..e9d58c19f 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -37,7 +37,8 @@ "peerDependencies": { "drizzle-kit": "^0.31.2", "drizzle-orm": "^0.44.2", - "rivetkit": "*" + "better-sqlite3": "^11.10.0", + "@rivetkit/core": "*" }, "peerDependenciesMeta": { "drizzle-orm": { @@ -45,19 +46,19 @@ }, "drizzle-kit": { "optional": true + }, + "better-sqlite3": { + "optional": true } }, "devDependencies": { "@types/better-sqlite3": "^7.6.13", "@types/node": "^24.0.4", "drizzle-orm": "^0.44.2", - "rivetkit": "workspace:*", + "@rivetkit/core": "workspace:*", "tsup": "^8.3.6", "typescript": "^5.5.2", "vitest": "^3.1.1" }, - "stableVersion": "0.8.0", - "dependencies": { - "better-sqlite3": "^11.10.0" - } + "stableVersion": "0.8.0" } diff --git a/packages/db/src/config.ts b/packages/db/src/config.ts deleted file mode 100644 index 988e82142..000000000 --- a/packages/db/src/config.ts +++ /dev/null @@ -1,12 +0,0 @@ -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 index d7fe198d3..772886095 100644 --- a/packages/db/src/drizzle/mod.ts +++ b/packages/db/src/drizzle/mod.ts @@ -2,13 +2,14 @@ import { type BetterSQLite3Database, drizzle as sqliteDrizzle, } from "drizzle-orm/better-sqlite3"; +import type { + DatabaseProviderOf, + DatabaseSetupFunction, +} from "@rivetkit/core/db"; -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"; - +import Database from "better-sqlite3"; export * from "drizzle-orm/sqlite-core"; import { defineConfig as originalDefineConfig, type Config } from "drizzle-kit"; @@ -16,8 +17,7 @@ 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. + // Pre-configures the defineConfig function to use the durable-sqlite driver, for convenience. return originalDefineConfig({ dialect: "sqlite", driver: "durable-sqlite", @@ -32,45 +32,62 @@ interface DatabaseFactoryConfig< * The database schema. */ schema?: TSchema; - migrations?: any; + migrations?: Parameters[1]; } export function db< TSchema extends Record = Record, >( config?: DatabaseFactoryConfig, -): DatabaseFactory> { - return async (ctx) => { - const conn = await ctx.createDatabase(); +): { setup: DatabaseSetupFunction> } { + return { + setup: async (ctx) => { + const conn = await ctx.setupDatabase(); + + if (!conn) { + throw new Error( + "Cannot create database connection, or database feature is not enabled.", + ); + } + + if ( + typeof conn === "object" && + conn && + "url" in conn && + typeof conn.url === "string" + ) { + const client = sqliteDrizzle({ + client: new Database(conn.url), + ...config, + }); - if (!conn) { - throw new Error( - "Cannot create database connection, or database feature is not enabled.", - ); - } + return { + client, + onMigrate: async () => { + if (config?.migrations) { + await durableMigrate(client, config.migrations); + } + }, + }; + } + + if (typeof conn !== "object" || !("exec" in conn)) { + throw new Error( + "Invalid database connection. Expected an object with an 'exec' method.", + ); + } - 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); + if (config?.migrations) { + await durableMigrate(client, config.migrations); + } }, }; - } - - const client = sqliteDrizzle({ - client: new Database(conn as string), - ...config, - }); - - return { - client, - onMigrate: async () => { - await sqliteMigrate(client, config?.migrations); - }, - }; - }; + }, + } satisfies DatabaseProviderOf>; } diff --git a/packages/db/src/mod.ts b/packages/db/src/mod.ts index 47e0172d5..1e23c9b5a 100644 --- a/packages/db/src/mod.ts +++ b/packages/db/src/mod.ts @@ -1,5 +1,8 @@ +import type { + DatabaseProviderOf, + DatabaseSetupFunction, +} from "@rivetkit/core/db"; import * as SQLite from "better-sqlite3"; -import type { DatabaseFactory } from "./config"; /** * On serverless environments, we use a shim, as not all methods are available. @@ -12,35 +15,49 @@ interface DatabaseFactoryConfig { onMigrate?: (db: SQLiteShim) => void; } -export function db({ - onMigrate, -}: DatabaseFactoryConfig = {}): DatabaseFactory { - return async (ctx) => { - const conn = await ctx.createDatabase(); +export function db({ onMigrate }: DatabaseFactoryConfig = {}): { + setup: DatabaseSetupFunction; +} { + // @ts-ignore + return { + setup: async (ctx) => { + const conn = await ctx.setupDatabase(); - if (!conn) { - throw new Error( - "Cannot create database connection, or database feature is not enabled.", - ); - } + if (!conn) { + throw new Error( + "Cannot create database connection, or database feature is not enabled.", + ); + } + + if ( + typeof conn === "object" && + conn && + "url" in conn && + typeof conn.url === "string" + ) { + // if the connection is already an object with exec method, return it + // i.e. in serverless environments (cloudflare) + const client = new SQLite(conn.url); + return { + client, + onMigrate: () => { + return onMigrate?.(client) || Promise.resolve(); + }, + }; + } + + if (typeof conn !== "object" || !("exec" in conn)) { + throw new Error( + "Invalid database connection. Expected an object with an 'exec' method.", + ); + } - 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); + return onMigrate?.(conn as SQLiteShim) || Promise.resolve(); }, }; - } - - const client = new SQLite(conn as string); - return { - client, - onMigrate: () => { - onMigrate?.(client); - }, - }; - }; + }, + } satisfies DatabaseProviderOf; } diff --git a/packages/misc/sql-loader/package.json b/packages/misc/sql-loader/package.json new file mode 100644 index 000000000..824e3ea14 --- /dev/null +++ b/packages/misc/sql-loader/package.json @@ -0,0 +1,19 @@ +{ + "name": "@rivetkit/sql-loader", + "version": "0.0.1", + "license": "MIT", + "main": "./dist/register.js", + "type": "commonjs", + "files": [ + "dist/**/*.js" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "dev": "tsc -p tsconfig.json --watch", + "test": "echo \"No tests specified\" && exit 0" + }, + "devDependencies": { + "@types/node": "^20.4.3", + "typescript": "^5.8.3" + } +} \ No newline at end of file diff --git a/packages/misc/sql-loader/src/hook.ts b/packages/misc/sql-loader/src/hook.ts new file mode 100644 index 000000000..93b6f9a92 --- /dev/null +++ b/packages/misc/sql-loader/src/hook.ts @@ -0,0 +1,14 @@ +import type { LoadHook } from "node:module"; +import { readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; + +export const load: LoadHook = async (url, context, nextLoad) => { + if (url.endsWith(".sql")) { + return { + shortCircuit: true, + format: "module", + source: `export default \`${(await readFile(fileURLToPath(url), "utf8")).replace(/`/gm, "\\`")}\`;`, + }; + } + return nextLoad(url, context); +}; diff --git a/packages/misc/sql-loader/src/register.ts b/packages/misc/sql-loader/src/register.ts new file mode 100644 index 000000000..89385d2e5 --- /dev/null +++ b/packages/misc/sql-loader/src/register.ts @@ -0,0 +1,4 @@ +import { register } from "node:module"; +import { pathToFileURL } from "node:url"; + +register("./hook.js", pathToFileURL(__filename)); diff --git a/packages/misc/sql-loader/tsconfig.json b/packages/misc/sql-loader/tsconfig.json new file mode 100644 index 000000000..ed77ac4cc --- /dev/null +++ b/packages/misc/sql-loader/tsconfig.json @@ -0,0 +1,16 @@ +{ + "paths": ["src/**"], + "compilerOptions": { + "target": "es2017" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "module": "commonjs" /* Specify what module code is generated. */, + "sourceMap": true /* Create source map files for emitted JavaScript files. */, + "outDir": "./dist" /* Specify an output folder for all emitted files. */, + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + "strict": true /* Enable all strict type-checking options. */, + "skipLibCheck": true /* Skip type checking all .d.ts files. */, + "types": [ + "node" + ] /* Specify type package names to be included without being referenced in a source file. */ + } +} diff --git a/packages/misc/sql-loader/turbo.json b/packages/misc/sql-loader/turbo.json new file mode 100644 index 000000000..95960709b --- /dev/null +++ b/packages/misc/sql-loader/turbo.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e760b11f..8af02503d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -173,19 +173,25 @@ importers: '@rivetkit/db': specifier: workspace:0.9.0-rc.1 version: link:../../packages/db + better-sqlite3: + specifier: ^12.1.1 + version: 12.1.1 drizzle-kit: specifier: ^0.31.2 version: 0.31.2 drizzle-orm: specifier: ^0.44.2 - version: 0.44.2(@cloudflare/workers-types@4.20250619.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@11.10.0)(kysely@0.28.2) + version: 0.44.2(@cloudflare/workers-types@4.20250619.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.1.1)(kysely@0.28.2) devDependencies: + '@rivetkit/actor': + specifier: workspace:* + version: link:../../packages/actor + '@rivetkit/sql-loader': + specifier: workspace:* + version: link:../../packages/misc/sql-loader '@types/node': specifier: ^22.13.9 version: 22.15.32 - rivetkit: - specifier: workspace:* - version: link:../../packages/rivetkit tsx: specifier: ^3.12.7 version: 3.14.0 @@ -496,6 +502,9 @@ importers: specifier: ^0.31.2 version: 0.31.2 devDependencies: + '@rivetkit/core': + specifier: workspace:* + version: link:../core '@types/better-sqlite3': specifier: ^7.6.13 version: 7.6.13 @@ -505,9 +514,6 @@ importers: drizzle-orm: specifier: ^0.44.2 version: 0.44.2(@cloudflare/workers-types@4.20250619.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@11.10.0)(kysely@0.28.2) - rivetkit: - specifier: workspace:* - version: link:../rivetkit tsup: specifier: ^8.3.6 version: 8.5.0(@microsoft/api-extractor@7.52.8(@types/node@24.0.4))(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0) @@ -658,6 +664,15 @@ importers: specifier: ^3.101.0 version: 3.114.9(@cloudflare/workers-types@4.20250619.0) + packages/misc/sql-loader: + devDependencies: + '@types/node': + specifier: ^20.4.3 + version: 20.19.1 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + packages/platforms/cloudflare-workers: dependencies: hono: @@ -2023,6 +2038,9 @@ packages: '@types/node@18.19.112': resolution: {integrity: sha512-i+Vukt9POdS/MBI7YrrkkI5fMfwFtOjphSmt4WXYLfwqsfr6z/HdCx7LqT9M7JktGob8WNgj8nFB4TbGNE4Cog==} + '@types/node@20.19.1': + resolution: {integrity: sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA==} + '@types/node@22.15.32': resolution: {integrity: sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==} @@ -2127,9 +2145,15 @@ packages: '@volar/language-core@2.4.14': resolution: {integrity: sha512-X6beusV0DvuVseaOEy7GoagS4rYHgDHnTrdOj5jeUb49fW5ceQyP9Ej5rBhqgz2wJggl+2fDbbojq1XKaxDi6w==} + '@volar/language-core@2.4.15': + resolution: {integrity: sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==} + '@volar/source-map@2.4.14': resolution: {integrity: sha512-5TeKKMh7Sfxo8021cJfmBzcjfY1SsXsPMMjMvjY7ivesdnybqqS+GxGAoXHAOUawQTwtdUxgP65Im+dEmvWtYQ==} + '@volar/source-map@2.4.15': + resolution: {integrity: sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==} + '@volar/typescript@2.4.14': resolution: {integrity: sha512-p8Z6f/bZM3/HyCdRNFZOEEzts51uV8WHeN8Tnfnm2EBv6FDB2TQLzfVx7aJvnl8ofKAOnS64B2O8bImBFaauRw==} @@ -2244,6 +2268,10 @@ packages: better-sqlite3@11.10.0: resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==} + better-sqlite3@12.1.1: + resolution: {integrity: sha512-xjl/TjWLy/6yLa5wkbQSjTgIgSiaEJy3XzjF5TAdiWaAsu/v0OCkYOc6tos+PkM/k4qURN2pFKTsbcG3gk29Uw==} + engines: {node: 20.x || 22.x || 23.x || 24.x} + bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -5331,6 +5359,10 @@ snapshots: dependencies: undici-types: 5.26.5 + '@types/node@20.19.1': + dependencies: + undici-types: 6.21.0 + '@types/node@22.15.32': dependencies: undici-types: 6.21.0 @@ -5412,14 +5444,6 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@22.15.32)(tsx@3.14.0)(yaml@2.8.0))': - dependencies: - '@vitest/spy': 3.2.4 - estree-walker: 3.0.3 - magic-string: 0.30.17 - optionalDependencies: - vite: 6.3.5(@types/node@22.15.32)(tsx@3.14.0)(yaml@2.8.0) - '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.0.4)(tsx@4.20.3)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.4 @@ -5479,8 +5503,14 @@ snapshots: dependencies: '@volar/source-map': 2.4.14 + '@volar/language-core@2.4.15': + dependencies: + '@volar/source-map': 2.4.15 + '@volar/source-map@2.4.14': {} + '@volar/source-map@2.4.15': {} + '@volar/typescript@2.4.14': dependencies: '@volar/language-core': 2.4.14 @@ -5507,7 +5537,7 @@ snapshots: '@vue/language-core@2.2.0(typescript@5.8.3)': dependencies: - '@volar/language-core': 2.4.14 + '@volar/language-core': 2.4.15 '@vue/compiler-dom': 3.5.17 '@vue/compiler-vue2': 2.7.16 '@vue/shared': 3.5.17 @@ -5614,6 +5644,11 @@ snapshots: bindings: 1.5.0 prebuild-install: 7.1.3 + better-sqlite3@12.1.1: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 @@ -5863,6 +5898,13 @@ snapshots: better-sqlite3: 11.10.0 kysely: 0.28.2 + drizzle-orm@0.44.2(@cloudflare/workers-types@4.20250619.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.1.1)(kysely@0.28.2): + optionalDependencies: + '@cloudflare/workers-types': 4.20250619.0 + '@types/better-sqlite3': 7.6.13 + better-sqlite3: 12.1.1 + kysely: 0.28.2 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -7559,7 +7601,7 @@ snapshots: dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@22.15.32)(tsx@3.14.0)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.0.4)(tsx@4.20.3)(yaml@2.8.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 01d48480c..b9a0f309c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,9 +1,8 @@ packages: - - 'packages/*' - - 'packages/platforms/*' - - 'packages/drivers/*' - - 'packages/components/*' - - 'packages/misc/*' - - 'packages/frameworks/*' - - 'examples/*' - + - packages/* + - packages/platforms/* + - packages/drivers/* + - packages/components/* + - packages/misc/* + - packages/frameworks/* + - examples/*