Skip to content

Commit 47d3238

Browse files
committed
feat: sqlite support
1 parent c9f072b commit 47d3238

24 files changed

+6069
-194
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"dev": "npx turbo watch dev",
1818
"build": "npx turbo build",
1919
"test": "npx turbo test",
20+
"test:watch": "npx turbo watch test",
2021
"check-types": "npx turbo check-types",
2122
"fmt": "yarn biome check --write .",
2223
"dev-docs": "cd docs && yarn dlx mintlify@latest dev",
@@ -28,6 +29,7 @@
2829
"devDependencies": {
2930
"@biomejs/biome": "^1.9.4",
3031
"@types/ws": "^8.5.14",
32+
"better-sqlite3": "^11.9.1",
3133
"dedent": "^1.5.3",
3234
"lefthook": "^1.6.12",
3335
"turbo": "^2.0.1",

packages/actor-core/package.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@
5252
"default": "./dist/actor/errors.cjs"
5353
}
5454
},
55+
"./sql": {
56+
"import": {
57+
"types": "./dist/actor/sql/mod.d.ts",
58+
"default": "./dist/actor/sql/mod.js"
59+
},
60+
"require": {
61+
"types": "./dist/actor/sql/mod.d.cts",
62+
"default": "./dist/actor/sql/mod.cjs"
63+
}
64+
},
5565
"./utils": {
5666
"import": {
5767
"types": "./dist/utils.d.ts",
@@ -135,7 +145,7 @@
135145
},
136146
"sideEffects": false,
137147
"scripts": {
138-
"build": "tsup src/mod.ts src/client/mod.ts src/common/log.ts src/actor/errors.ts src/topologies/coordinate/mod.ts src/topologies/partition/mod.ts src/utils.ts src/driver-helpers/mod.ts src/cli/mod.ts src/actor/protocol/inspector/mod.ts src/actor/protocol/http/rpc.ts src/test/mod.ts",
148+
"build": "tsup src/mod.ts src/client/mod.ts src/common/log.ts src/actor/errors.ts src/actor/sql/mod.ts src/topologies/coordinate/mod.ts src/topologies/partition/mod.ts src/utils.ts src/driver-helpers/mod.ts src/cli/mod.ts src/actor/protocol/inspector/mod.ts src/actor/protocol/http/rpc.ts src/test/mod.ts",
139149
"check-types": "tsc --noEmit",
140150
"boop": "tsc --outDir dist/test -d",
141151
"test": "vitest run",
@@ -153,6 +163,8 @@
153163
"@types/invariant": "^2",
154164
"@types/node": "^22.13.1",
155165
"@types/ws": "^8",
166+
"drizzle-kit": "^0.30.5",
167+
"drizzle-orm": "^0.41.0",
156168
"eventsource": "^3.0.5",
157169
"tsup": "^8.4.0",
158170
"typescript": "^5.7.3",

packages/actor-core/src/actor/action.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import type { ActorTags } from "@/common/utils";
55
import type { Schedule } from "./schedule";
66
import type { ConnId } from "./connection";
77
import type { SaveStateOptions } from "./instance";
8-
import { Actions } from "./config";
98
import { ActorContext } from "./context";
9+
import { SqlConnection } from "@/actor/sql/mod";
1010

1111
/**
1212
* Context for a remote procedure call.
@@ -36,6 +36,10 @@ export class ActionContext<S, CP, CS, V> {
3636
return this.#actorContext.state;
3737
}
3838

39+
get sql(): SqlConnection {
40+
return this.#actorContext.sql
41+
}
42+
3943
/**
4044
* Get the actor variables
4145
*/

packages/actor-core/src/actor/config.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const ActorConfigSchema = z
1919
onBeforeActionResponse: z.function().optional(),
2020
actions: z.record(z.function()),
2121
state: z.any().optional(),
22+
sql: z.boolean().default(false),
2223
createState: z.function().optional(),
2324
connState: z.any().optional(),
2425
createConnState: z.function().optional(),
@@ -81,7 +82,11 @@ export interface OnConnectOptions<CP> {
8182
// This must have only one or the other or else S will not be able to be inferred
8283
type CreateState<S, CP, CS, V> =
8384
| { state: S }
84-
| { createState: (c: ActorContext<undefined, undefined, undefined, undefined>) => S | Promise<S> }
85+
| {
86+
createState: (
87+
c: ActorContext<undefined, undefined, undefined, undefined>,
88+
) => S | Promise<S>;
89+
}
8590
| Record<never, never>;
8691

8792
// Creates connection state config
@@ -114,7 +119,10 @@ type CreateVars<S, CP, CS, V> =
114119
/**
115120
* @experimental
116121
*/
117-
createVars: (c: ActorContext<undefined, undefined, undefined, undefined>, driverCtx: unknown) => V | Promise<V>;
122+
createVars: (
123+
c: ActorContext<undefined, undefined, undefined, undefined>,
124+
driverCtx: unknown,
125+
) => V | Promise<V>;
118126
}
119127
| Record<never, never>;
120128

packages/actor-core/src/actor/context.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ActorInstance, SaveStateOptions } from "./instance";
44
import { Conn, ConnId } from "./connection";
55
import { ActorTags } from "@/common/utils";
66
import { Schedule } from "./schedule";
7+
import { SqlConnection } from "@/actor/sql/mod";
78

89

910
/**
@@ -23,6 +24,10 @@ export class ActorContext<S, CP, CS, V> {
2324
return this.#actor.state;
2425
}
2526

27+
get sql(): SqlConnection {
28+
return this.#actor.sql;
29+
}
30+
2631
/**
2732
* Get the actor variables
2833
*/

packages/actor-core/src/actor/driver.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@ import type * as messageToClient from "@/actor/protocol/message/to-client";
22
import type { CachedSerializer } from "@/actor/protocol/serde";
33
import type { AnyActorInstance } from "./instance";
44
import { AnyConn } from "./connection";
5+
import type { SqlConnection } from "./sql/mod";
56

67
export type ConnDrivers = Record<string, ConnDriver>;
78

89
export type KvKey = unknown[];
910
export type KvValue = unknown;
1011

11-
1212
export interface ActorDriver {
1313
//load(): Promise<LoadOutput>;
1414
get context(): unknown;
1515

16+
// SQL connection
17+
createSqlConnection?(): SqlConnection;
18+
1619
// HACK: Clean these up
1720
kvGet(actorId: string, key: KvKey): Promise<KvValue | undefined>;
1821
kvGetBatch(actorId: string, key: KvKey[]): Promise<(KvValue | undefined)[]>;

packages/actor-core/src/actor/errors.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ export class StateNotEnabled extends ActorError {
4848
}
4949
}
5050

51+
export class SqlNotEnabled extends ActorError {
52+
constructor() {
53+
super(
54+
"sql_not_enabled",
55+
"SQL not enabled.",
56+
);
57+
}
58+
}
59+
5160
export class ConnStateNotEnabled extends ActorError {
5261
constructor() {
5362
super(
@@ -164,6 +173,15 @@ export class Unsupported extends ActorError {
164173
}
165174
}
166175

176+
export class DriverDoesNotSupportSql extends ActorError {
177+
constructor() {
178+
super(
179+
"driver_does_not_support_sql",
180+
"Driver does not support SQL. Check https://actorcore.org for supported platforms.",
181+
);
182+
}
183+
}
184+
167185
/**
168186
* Options for the UserError class.
169187
*/

packages/actor-core/src/actor/instance.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { CachedSerializer } from "./protocol/serde";
1818
import { Inspector } from "@/actor/inspect";
1919
import { ActorContext } from "./context";
2020
import invariant from "invariant";
21+
import { SqlConnection } from "./sql/mod";
2122

2223
/**
2324
* Options for the `_saveState` method.
@@ -97,6 +98,8 @@ export class ActorInstance<S, CP, CS, V> {
9798
/** Raw state without the proxy wrapper */
9899
#persistRaw!: PersistedActor<S, CP, CS>;
99100

101+
#sqlConn?: SqlConnection;
102+
100103
#writePersistLock = new Lock<void>(void 0);
101104

102105
#lastSaveTime = 0;
@@ -158,6 +161,13 @@ export class ActorInstance<S, CP, CS, V> {
158161
this.#schedule = new Schedule(this, actorDriver);
159162
this.inspector = new Inspector(this);
160163

164+
// Create SQL connection
165+
if (this.#config.sql) {
166+
if (!this.#actorDriver.createSqlConnection) throw new errors.DriverDoesNotSupportSql();
167+
this.#sqlConn = this.#actorDriver.createSqlConnection();
168+
}
169+
170+
161171
// Initialize server
162172
//
163173
// Store the promise so network requests can await initialization
@@ -214,6 +224,16 @@ export class ActorInstance<S, CP, CS, V> {
214224
}
215225
}
216226

227+
get sqlEnabled() {
228+
return this.#config.sql;
229+
}
230+
231+
#validateSqlEnabled() {
232+
if (!this.sqlEnabled) {
233+
throw new errors.SqlNotEnabled();
234+
}
235+
}
236+
217237
get #connStateEnabled() {
218238
return "createConnState" in this.#config || "connState" in this.#config;
219239
}
@@ -846,6 +866,12 @@ export class ActorInstance<S, CP, CS, V> {
846866
return this.#persist.s;
847867
}
848868

869+
get sql(): SqlConnection {
870+
this.#validateSqlEnabled();
871+
invariant(this.#sqlConn !== undefined, "#sqlConn is undefined");
872+
return this.#sqlConn;
873+
}
874+
849875
/**
850876
* Sets the current state.
851877
*
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export interface SqlConnection {
2+
// TDOO: Remove this
3+
HACK_raw: any;
4+
5+
// TODO: Find methods that are required for Drizzle-compat
6+
// TODO: Support both sync and async
7+
}
8+

packages/actor-core/src/test/driver/actor.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1-
import type { ActorDriver, KvKey, KvValue, AnyActorInstance } from "@/driver-helpers/mod";
1+
import type {
2+
ActorDriver,
3+
KvKey,
4+
KvValue,
5+
AnyActorInstance,
6+
} from "@/driver-helpers/mod";
27
import type { TestGlobalState } from "./global_state";
8+
import { SqlConnection } from "@/actor/sql/mod";
9+
import Database from "better-sqlite3";
10+
import { TestSqlConnection } from "./sql";
311

412
export interface ActorDriverContext {
513
state: TestGlobalState;
@@ -16,6 +24,10 @@ export class TestActorDriver implements ActorDriver {
1624
return { state: this.#state };
1725
}
1826

27+
createSqlConnection(): SqlConnection {
28+
return new TestSqlConnection(new Database(":memory:"));
29+
}
30+
1931
async kvGet(actorId: string, key: KvKey): Promise<KvValue | undefined> {
2032
const serializedKey = this.#serializeKey(key);
2133
const value = this.#state.getKv(actorId, serializedKey);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { SqlConnection } from "@/actor/sql/mod";
2+
import { Database } from "better-sqlite3";
3+
4+
export class TestSqlConnection implements SqlConnection {
5+
#raw: Database;
6+
7+
// TODO: This is a temporary hack for Drizzle
8+
get HACK_raw() {
9+
return this.#raw;
10+
}
11+
12+
constructor(db: Database) {
13+
this.#raw = db;
14+
}
15+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { defineConfig } from "drizzle-kit";
2+
3+
export default defineConfig({
4+
out: "./tests/schemas/chat-room/drizzle",
5+
dialect: "sqlite",
6+
schema: "./tests/schemas/chat-room/schema.ts",
7+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
CREATE TABLE `messages` (
2+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
3+
`username` text,
4+
`message` text
5+
);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
PRAGMA foreign_keys=OFF;--> statement-breakpoint
2+
CREATE TABLE `__new_messages` (
3+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
4+
`username` text NOT NULL,
5+
`message` text NOT NULL
6+
);
7+
--> statement-breakpoint
8+
INSERT INTO `__new_messages`("id", "username", "message") SELECT "id", "username", "message" FROM `messages`;--> statement-breakpoint
9+
DROP TABLE `messages`;--> statement-breakpoint
10+
ALTER TABLE `__new_messages` RENAME TO `messages`;--> statement-breakpoint
11+
PRAGMA foreign_keys=ON;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE `messages` ADD `createdAt` integer DEFAULT (unixepoch() * 1000) NOT NULL;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CREATE INDEX `created_at_idx` ON `messages` (`createdAt`);
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"version": "6",
3+
"dialect": "sqlite",
4+
"id": "78d399d7-9b51-4e04-ad31-1c3dc909e24a",
5+
"prevId": "00000000-0000-0000-0000-000000000000",
6+
"tables": {
7+
"messages": {
8+
"name": "messages",
9+
"columns": {
10+
"id": {
11+
"name": "id",
12+
"type": "integer",
13+
"primaryKey": true,
14+
"notNull": true,
15+
"autoincrement": true
16+
},
17+
"username": {
18+
"name": "username",
19+
"type": "text",
20+
"primaryKey": false,
21+
"notNull": false,
22+
"autoincrement": false
23+
},
24+
"message": {
25+
"name": "message",
26+
"type": "text",
27+
"primaryKey": false,
28+
"notNull": false,
29+
"autoincrement": false
30+
}
31+
},
32+
"indexes": {},
33+
"foreignKeys": {},
34+
"compositePrimaryKeys": {},
35+
"uniqueConstraints": {},
36+
"checkConstraints": {}
37+
}
38+
},
39+
"views": {},
40+
"enums": {},
41+
"_meta": {
42+
"schemas": {},
43+
"tables": {},
44+
"columns": {}
45+
},
46+
"internal": {
47+
"indexes": {}
48+
}
49+
}

0 commit comments

Comments
 (0)