rivetkit 2.3.3 → 2.3.5-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/dist/browser/client.d.ts +157 -9
  2. package/dist/browser/client.js +518 -542
  3. package/dist/browser/client.js.map +1 -1
  4. package/dist/browser/inspector/client.d.ts +101 -2
  5. package/dist/browser/inspector/client.js +1245 -227
  6. package/dist/browser/inspector/client.js.map +1 -1
  7. package/dist/tsup/agent-os/index.cjs +103 -36
  8. package/dist/tsup/agent-os/index.cjs.map +1 -1
  9. package/dist/tsup/agent-os/index.d.cts +159 -11
  10. package/dist/tsup/agent-os/index.d.ts +159 -11
  11. package/dist/tsup/agent-os/index.js +103 -36
  12. package/dist/tsup/agent-os/index.js.map +1 -1
  13. package/dist/tsup/{chunk-34P567PO.cjs → chunk-4XKED5P5.cjs} +145 -176
  14. package/dist/tsup/chunk-4XKED5P5.cjs.map +1 -0
  15. package/dist/tsup/{chunk-PYO2U53V.cjs → chunk-6HQCLWAW.cjs} +384 -4
  16. package/dist/tsup/chunk-6HQCLWAW.cjs.map +1 -0
  17. package/dist/tsup/{chunk-JZ7TWV65.js → chunk-7AFZMFPQ.js} +15 -1
  18. package/dist/tsup/chunk-7AFZMFPQ.js.map +1 -0
  19. package/dist/tsup/{chunk-BLDO2FKO.js → chunk-B4IZU537.js} +382 -2
  20. package/dist/tsup/chunk-B4IZU537.js.map +1 -0
  21. package/dist/tsup/{chunk-3MHDOUD7.cjs → chunk-G3CCRCTK.cjs} +14 -9
  22. package/dist/tsup/chunk-G3CCRCTK.cjs.map +1 -0
  23. package/dist/tsup/{chunk-CXRPENO3.js → chunk-GAV4PKFT.js} +15 -46
  24. package/dist/tsup/{chunk-CXRPENO3.js.map → chunk-GAV4PKFT.js.map} +1 -1
  25. package/dist/tsup/{chunk-HXUEHHJF.js → chunk-JI6GZ2C2.js} +2 -11
  26. package/dist/tsup/{chunk-HXUEHHJF.js.map → chunk-JI6GZ2C2.js.map} +1 -1
  27. package/dist/tsup/{chunk-YUEC5SEY.cjs → chunk-M3FCXL3Q.cjs} +3 -3
  28. package/dist/tsup/{chunk-YUEC5SEY.cjs.map → chunk-M3FCXL3Q.cjs.map} +1 -1
  29. package/dist/tsup/{chunk-KEKWJECX.js → chunk-MGTSVSUC.js} +2 -2
  30. package/dist/tsup/{chunk-VE2X4KMG.cjs → chunk-MYWZCDML.cjs} +2 -11
  31. package/dist/tsup/chunk-MYWZCDML.cjs.map +1 -0
  32. package/dist/tsup/{chunk-DFA7SYSW.js → chunk-NIC3QLKL.js} +3 -3
  33. package/dist/tsup/{chunk-BZM4PMFS.js → chunk-P4OGQ4JL.js} +5 -7
  34. package/dist/tsup/{chunk-BZM4PMFS.js.map → chunk-P4OGQ4JL.js.map} +1 -1
  35. package/dist/tsup/{chunk-NT2NK3VT.js → chunk-PAG6G46L.js} +2 -2
  36. package/dist/tsup/{chunk-XIX5DOZN.js → chunk-SHBE77IT.js} +14 -9
  37. package/dist/tsup/chunk-SHBE77IT.js.map +1 -0
  38. package/dist/tsup/{chunk-TYLP3U4Y.cjs → chunk-TNIWIUNV.cjs} +9 -11
  39. package/dist/tsup/chunk-TNIWIUNV.cjs.map +1 -0
  40. package/dist/tsup/{chunk-S644FL73.cjs → chunk-TR5NLR5V.cjs} +4 -4
  41. package/dist/tsup/{chunk-S644FL73.cjs.map → chunk-TR5NLR5V.cjs.map} +1 -1
  42. package/dist/tsup/{chunk-ZA7FLHKH.cjs → chunk-UEULFXUI.cjs} +16 -2
  43. package/dist/tsup/chunk-UEULFXUI.cjs.map +1 -0
  44. package/dist/tsup/{chunk-X63ROSOG.cjs → chunk-UJ2FQY63.cjs} +7 -7
  45. package/dist/tsup/{chunk-X63ROSOG.cjs.map → chunk-UJ2FQY63.cjs.map} +1 -1
  46. package/dist/tsup/client/mod.cjs +7 -8
  47. package/dist/tsup/client/mod.cjs.map +1 -1
  48. package/dist/tsup/client/mod.d.cts +5 -4
  49. package/dist/tsup/client/mod.d.ts +5 -4
  50. package/dist/tsup/client/mod.js +6 -7
  51. package/dist/tsup/common/log.cjs +2 -2
  52. package/dist/tsup/common/log.js +1 -1
  53. package/dist/tsup/common/websocket.cjs +3 -3
  54. package/dist/tsup/common/websocket.js +2 -2
  55. package/dist/tsup/{config-D49x8NpL.d.cts → config-BxOimoGX.d.cts} +16 -0
  56. package/dist/tsup/{config-D49x8NpL.d.ts → config-BxOimoGX.d.ts} +16 -0
  57. package/dist/tsup/{config-ZkIsRFsN.d.ts → config-BzdqbLAP.d.ts} +164 -27
  58. package/dist/tsup/{config-BfBhjEbk.d.cts → config-CPPEwn_e.d.cts} +164 -27
  59. package/dist/tsup/{context-sZ9GlaBK.d.cts → context-DSA9gFXB.d.cts} +2 -2
  60. package/dist/tsup/{context-B7-84u9K.d.ts → context-wWv-K2K_.d.ts} +2 -2
  61. package/dist/tsup/db/drizzle.cjs +120 -68
  62. package/dist/tsup/db/drizzle.cjs.map +1 -1
  63. package/dist/tsup/db/drizzle.d.cts +8 -3
  64. package/dist/tsup/db/drizzle.d.ts +8 -3
  65. package/dist/tsup/db/drizzle.js +120 -68
  66. package/dist/tsup/db/drizzle.js.map +1 -1
  67. package/dist/tsup/db/mod.cjs +74 -22
  68. package/dist/tsup/db/mod.cjs.map +1 -1
  69. package/dist/tsup/db/mod.d.cts +4 -3
  70. package/dist/tsup/db/mod.d.ts +4 -3
  71. package/dist/tsup/db/mod.js +74 -22
  72. package/dist/tsup/db/mod.js.map +1 -1
  73. package/dist/tsup/dynamic/mod.cjs +2 -2
  74. package/dist/tsup/dynamic/mod.d.cts +3 -3
  75. package/dist/tsup/dynamic/mod.d.ts +3 -3
  76. package/dist/tsup/dynamic/mod.js +1 -1
  77. package/dist/tsup/inspector/mod.cjs +4 -5
  78. package/dist/tsup/inspector/mod.cjs.map +1 -1
  79. package/dist/tsup/inspector/mod.js +3 -4
  80. package/dist/tsup/inspector/mod.js.map +1 -1
  81. package/dist/tsup/inspector-tab/mod.cjs +2 -2
  82. package/dist/tsup/inspector-tab/mod.cjs.map +1 -1
  83. package/dist/tsup/inspector-tab/mod.d.cts +5 -5
  84. package/dist/tsup/inspector-tab/mod.d.ts +5 -5
  85. package/dist/tsup/inspector-tab/mod.js +1 -1
  86. package/dist/tsup/inspector-tab/mod.js.map +1 -1
  87. package/dist/tsup/mod.cjs +605 -158
  88. package/dist/tsup/mod.cjs.map +1 -1
  89. package/dist/tsup/mod.d.cts +5 -5
  90. package/dist/tsup/mod.d.ts +5 -5
  91. package/dist/tsup/mod.js +528 -81
  92. package/dist/tsup/mod.js.map +1 -1
  93. package/dist/tsup/test/mod.cjs +10 -11
  94. package/dist/tsup/test/mod.cjs.map +1 -1
  95. package/dist/tsup/test/mod.d.cts +3 -3
  96. package/dist/tsup/test/mod.d.ts +3 -3
  97. package/dist/tsup/test/mod.js +6 -7
  98. package/dist/tsup/test/mod.js.map +1 -1
  99. package/dist/tsup/utils-CV0akuDD.d.ts +162 -0
  100. package/dist/tsup/utils-DQDCphrl.d.cts +162 -0
  101. package/dist/tsup/utils.cjs +6 -2
  102. package/dist/tsup/utils.cjs.map +1 -1
  103. package/dist/tsup/utils.d.cts +3 -146
  104. package/dist/tsup/utils.d.ts +3 -146
  105. package/dist/tsup/utils.js +5 -1
  106. package/dist/tsup/workflow/mod.cjs +198 -151
  107. package/dist/tsup/workflow/mod.cjs.map +1 -1
  108. package/dist/tsup/workflow/mod.d.cts +5 -5
  109. package/dist/tsup/workflow/mod.d.ts +5 -5
  110. package/dist/tsup/workflow/mod.js +187 -140
  111. package/dist/tsup/workflow/mod.js.map +1 -1
  112. package/package.json +8 -8
  113. package/src/actor/config.test.ts +14 -0
  114. package/src/actor/config.ts +104 -16
  115. package/src/actor/definition.ts +18 -7
  116. package/src/agent-os/actor/index.ts +2 -2
  117. package/src/common/bare/generated/actor-runtime-socket-protocol/v1.ts +848 -0
  118. package/src/common/bare/generated/inspector/v6.ts +1367 -0
  119. package/src/common/database/config.ts +24 -0
  120. package/src/common/database/mod.test.ts +148 -59
  121. package/src/common/database/mod.ts +93 -23
  122. package/src/common/database/native-database.test.ts +9 -0
  123. package/src/common/database/native-database.ts +151 -0
  124. package/src/common/database/shared.ts +15 -0
  125. package/src/common/encoding.ts +28 -21
  126. package/src/db/drizzle.test.ts +157 -0
  127. package/src/db/drizzle.ts +170 -79
  128. package/src/dynamic/isolate-runtime.ts +0 -2
  129. package/src/engine-client/driver.ts +0 -15
  130. package/src/engine-client/mod.ts +0 -41
  131. package/src/inspector/client.browser.ts +69 -8
  132. package/src/inspector-tab/mod.ts +1 -0
  133. package/src/registry/config/index.ts +1 -35
  134. package/src/registry/napi-runtime.ts +204 -16
  135. package/src/registry/native.ts +200 -30
  136. package/src/registry/runtime.ts +146 -3
  137. package/src/registry/wasm-runtime.ts +217 -6
  138. package/src/utils.ts +3 -0
  139. package/src/workflow/context.ts +5 -27
  140. package/src/workflow/driver.test.ts +62 -0
  141. package/src/workflow/driver.ts +231 -113
  142. package/src/workflow/mod.ts +3 -3
  143. package/dist/tsup/chunk-34P567PO.cjs.map +0 -1
  144. package/dist/tsup/chunk-3MHDOUD7.cjs.map +0 -1
  145. package/dist/tsup/chunk-APW73HCH.cjs +0 -382
  146. package/dist/tsup/chunk-APW73HCH.cjs.map +0 -1
  147. package/dist/tsup/chunk-BLDO2FKO.js.map +0 -1
  148. package/dist/tsup/chunk-DHXWD4DF.js +0 -382
  149. package/dist/tsup/chunk-DHXWD4DF.js.map +0 -1
  150. package/dist/tsup/chunk-JZ7TWV65.js.map +0 -1
  151. package/dist/tsup/chunk-PYO2U53V.cjs.map +0 -1
  152. package/dist/tsup/chunk-TYLP3U4Y.cjs.map +0 -1
  153. package/dist/tsup/chunk-VE2X4KMG.cjs.map +0 -1
  154. package/dist/tsup/chunk-XIX5DOZN.js.map +0 -1
  155. package/dist/tsup/chunk-ZA7FLHKH.cjs.map +0 -1
  156. package/src/actor/driver.ts +0 -141
  157. package/src/actor/instance/mod.ts +0 -2484
  158. package/src/drivers/engine/actor-driver.ts +0 -2473
  159. /package/dist/tsup/{chunk-KEKWJECX.js.map → chunk-MGTSVSUC.js.map} +0 -0
  160. /package/dist/tsup/{chunk-DFA7SYSW.js.map → chunk-NIC3QLKL.js.map} +0 -0
  161. /package/dist/tsup/{chunk-NT2NK3VT.js.map → chunk-PAG6G46L.js.map} +0 -0
@@ -23,6 +23,11 @@ export interface SqliteExecuteResult extends SqliteQueryResult {
23
23
  lastInsertRowId?: number | null;
24
24
  }
25
25
 
26
+ export interface SqliteBatchStatement {
27
+ sql: string;
28
+ params?: SqliteBindings;
29
+ }
30
+
26
31
  export interface SqliteNativeMetrics {
27
32
  requestBuildNs: number;
28
33
  serializeNs: number;
@@ -43,6 +48,10 @@ export interface SqliteDatabase {
43
48
  callback?: (row: unknown[], columns: string[]) => void,
44
49
  ): Promise<void>;
45
50
  execute(sql: string, params?: SqliteBindings): Promise<SqliteExecuteResult>;
51
+ executeBatch(
52
+ statements: SqliteBatchStatement[],
53
+ ): Promise<SqliteExecuteResult[]>;
54
+ beginTransaction(timeoutMs?: number): Promise<SqliteTransactionDatabase>;
46
55
  run(sql: string, params?: SqliteBindings): Promise<void>;
47
56
  query(sql: string, params?: SqliteBindings): Promise<SqliteQueryResult>;
48
57
  nativeMetrics?():
@@ -52,6 +61,16 @@ export interface SqliteDatabase {
52
61
  close(): Promise<void>;
53
62
  }
54
63
 
64
+ export interface SqliteTransactionDatabase {
65
+ exec(
66
+ sql: string,
67
+ callback?: (row: unknown[], columns: string[]) => void,
68
+ ): Promise<void>;
69
+ execute(sql: string, params?: SqliteBindings): Promise<SqliteExecuteResult>;
70
+ commit(): Promise<void>;
71
+ rollback(): Promise<void>;
72
+ }
73
+
55
74
  /**
56
75
  * Provider for opening native databases from the active runtime.
57
76
  */
@@ -143,6 +162,11 @@ export type RawAccess = {
143
162
  * Executes a raw SQL query.
144
163
  */
145
164
  execute: ExecuteFunction;
165
+ /** Runs a callback in an isolated SQLite transaction. */
166
+ transaction: <T>(
167
+ callback: (tx: RawAccess) => Promise<T> | T,
168
+ options?: { timeout?: number },
169
+ ) => Promise<T>;
146
170
  /**
147
171
  * Returns native SQLite metrics when the active runtime supports them.
148
172
  */
@@ -1,17 +1,21 @@
1
- import { describe, expect, test } from "vitest";
1
+ import { pino } from "pino";
2
+ import { beforeEach, describe, expect, test } from "vitest";
3
+ import { configureBaseLogger } from "@/common/log";
2
4
  import type {
3
5
  DatabaseProviderContext,
4
6
  SqliteBindings,
5
7
  SqliteDatabase,
6
8
  SqliteExecuteResult,
9
+ SqliteTransactionDatabase,
7
10
  } from "./config";
8
11
  import { db } from "./mod";
9
12
 
13
+ let logLines: string[];
14
+
10
15
  class FakeSqliteDatabase implements SqliteDatabase {
11
- executeCalls: {
12
- sql: string;
13
- params?: SqliteBindings;
14
- }[] = [];
16
+ failSql = new Map<string, Error>();
17
+ executeCalls: { sql: string; params?: SqliteBindings }[] = [];
18
+ transactionTimeouts: Array<number | undefined> = [];
15
19
 
16
20
  async exec(): Promise<void> {}
17
21
 
@@ -19,18 +23,36 @@ class FakeSqliteDatabase implements SqliteDatabase {
19
23
  sql: string,
20
24
  params?: SqliteBindings,
21
25
  ): Promise<SqliteExecuteResult> {
22
- this.executeCalls.push({
23
- sql,
24
- params,
25
- });
26
+ this.record(sql, params);
27
+ return emptyResult();
28
+ }
29
+
30
+ async beginTransaction(
31
+ timeoutMs?: number,
32
+ ): Promise<SqliteTransactionDatabase> {
33
+ this.transactionTimeouts.push(timeoutMs);
34
+ this.record("BEGIN");
26
35
  return {
27
- columns: [],
28
- rows: [],
29
- changes: 0,
30
- lastInsertRowId: null,
36
+ exec: async () => {},
37
+ execute: async (sql, params) => {
38
+ this.record(sql, params);
39
+ return emptyResult();
40
+ },
41
+ commit: async () => this.record("COMMIT"),
42
+ rollback: async () => this.record("ROLLBACK"),
31
43
  };
32
44
  }
33
45
 
46
+ async executeBatch(
47
+ statements: Array<{ sql: string; params?: SqliteBindings }>,
48
+ ): Promise<SqliteExecuteResult[]> {
49
+ const results: SqliteExecuteResult[] = [];
50
+ for (const statement of statements) {
51
+ results.push(await this.execute(statement.sql, statement.params));
52
+ }
53
+ return results;
54
+ }
55
+
34
56
  async run(sql: string, params?: SqliteBindings): Promise<void> {
35
57
  await this.execute(sql, params);
36
58
  }
@@ -41,6 +63,21 @@ class FakeSqliteDatabase implements SqliteDatabase {
41
63
  }
42
64
 
43
65
  async close(): Promise<void> {}
66
+
67
+ private record(sql: string, params?: SqliteBindings): void {
68
+ this.executeCalls.push({ sql, params });
69
+ const error = this.failSql.get(sql);
70
+ if (error) throw error;
71
+ }
72
+ }
73
+
74
+ function emptyResult(): SqliteExecuteResult {
75
+ return {
76
+ columns: [],
77
+ rows: [],
78
+ changes: 0,
79
+ lastInsertRowId: null,
80
+ };
44
81
  }
45
82
 
46
83
  function testProviderContext(
@@ -54,84 +91,136 @@ function testProviderContext(
54
91
  batchDelete: async () => {},
55
92
  deleteRange: async () => {},
56
93
  },
57
- nativeDatabaseProvider: {
58
- open: async () => database,
59
- },
94
+ nativeDatabaseProvider: { open: async () => database },
60
95
  };
61
96
  }
62
97
 
63
98
  describe("db", () => {
64
- test("runs onMigrate inside a sqlite savepoint", async () => {
99
+ beforeEach(() => {
100
+ logLines = [];
101
+ configureBaseLogger(
102
+ pino(
103
+ { level: "warn", base: {}, timestamp: false },
104
+ { write: (line: string) => logLines.push(line) },
105
+ ),
106
+ );
107
+ });
108
+
109
+ test("runs onMigrate through the shared transaction handle", async () => {
65
110
  const nativeDb = new FakeSqliteDatabase();
66
111
  const provider = db({
67
112
  onMigrate: async (client) => {
68
113
  await client.execute(
69
- "CREATE TABLE items(id INTEGER PRIMARY KEY, value TEXT)",
114
+ "CREATE TABLE items(id INTEGER PRIMARY KEY)",
70
115
  );
71
- await client.execute("SELECT COUNT(*) AS count FROM items");
72
116
  },
73
117
  });
74
118
  const client = await provider.createClient(
75
119
  testProviderContext(nativeDb),
76
120
  );
77
-
78
121
  await provider.onMigrate(client);
79
122
 
80
- expect(nativeDb.executeCalls).toEqual([
81
- {
82
- sql: "SAVEPOINT __rivet_on_migrate",
83
- params: undefined,
84
- },
85
- {
86
- sql: "CREATE TABLE items(id INTEGER PRIMARY KEY, value TEXT)",
87
- params: undefined,
88
- },
89
- {
90
- sql: "SELECT COUNT(*) AS count FROM items",
91
- params: undefined,
92
- },
93
- {
94
- sql: "RELEASE SAVEPOINT __rivet_on_migrate",
95
- params: undefined,
96
- },
123
+ expect(nativeDb.executeCalls.map(({ sql }) => sql)).toEqual([
124
+ "BEGIN",
125
+ "SAVEPOINT __rivet_on_migrate",
126
+ "CREATE TABLE items(id INTEGER PRIMARY KEY)",
127
+ "RELEASE SAVEPOINT __rivet_on_migrate",
128
+ "COMMIT",
97
129
  ]);
130
+ expect(nativeDb.transactionTimeouts).toEqual([300_000]);
98
131
  });
99
132
 
100
- test("rolls back the migration savepoint when onMigrate fails", async () => {
133
+ test("rolls back migrations when onMigrate fails", async () => {
101
134
  const nativeDb = new FakeSqliteDatabase();
102
135
  const provider = db({
103
- onMigrate: async (client) => {
104
- await client.execute(
105
- "CREATE TABLE items(id INTEGER PRIMARY KEY, value TEXT)",
106
- );
136
+ onMigrate: async () => {
107
137
  throw new Error("migration failed");
108
138
  },
109
139
  });
110
140
  const client = await provider.createClient(
111
141
  testProviderContext(nativeDb),
112
142
  );
113
-
114
143
  await expect(provider.onMigrate(client)).rejects.toThrow(
115
144
  "migration failed",
116
145
  );
146
+ expect(nativeDb.executeCalls.map(({ sql }) => sql)).toEqual([
147
+ "BEGIN",
148
+ "SAVEPOINT __rivet_on_migrate",
149
+ "ROLLBACK TO SAVEPOINT __rivet_on_migrate",
150
+ "RELEASE SAVEPOINT __rivet_on_migrate",
151
+ "ROLLBACK",
152
+ ]);
153
+ });
117
154
 
118
- expect(nativeDb.executeCalls).toEqual([
119
- {
120
- sql: "SAVEPOINT __rivet_on_migrate",
121
- params: undefined,
122
- },
123
- {
124
- sql: "CREATE TABLE items(id INTEGER PRIMARY KEY, value TEXT)",
125
- params: undefined,
126
- },
127
- {
128
- sql: "ROLLBACK TO SAVEPOINT __rivet_on_migrate",
129
- params: undefined,
130
- },
131
- {
132
- sql: "RELEASE SAVEPOINT __rivet_on_migrate",
133
- params: undefined,
155
+ test("commits transaction work and forwards timeout", async () => {
156
+ const nativeDb = new FakeSqliteDatabase();
157
+ const client = await db().createClient(testProviderContext(nativeDb));
158
+ const value = await client.transaction(
159
+ async (tx) => {
160
+ await tx.execute(
161
+ "INSERT INTO items(value) VALUES (?)",
162
+ "inside",
163
+ );
164
+ return 42;
134
165
  },
166
+ { timeout: 120_000 },
167
+ );
168
+ expect(value).toBe(42);
169
+ expect(nativeDb.transactionTimeouts).toEqual([120_000]);
170
+ expect(nativeDb.executeCalls.map(({ sql }) => sql)).toEqual([
171
+ "BEGIN",
172
+ "INSERT INTO items(value) VALUES (?)",
173
+ "COMMIT",
174
+ ]);
175
+ });
176
+
177
+ test("rolls back a transaction when the callback throws", async () => {
178
+ const nativeDb = new FakeSqliteDatabase();
179
+ const client = await db().createClient(testProviderContext(nativeDb));
180
+ await expect(
181
+ client.transaction(async (tx) => {
182
+ await tx.execute(
183
+ "INSERT INTO items(value) VALUES (?)",
184
+ "inside",
185
+ );
186
+ throw new Error("callback failed");
187
+ }),
188
+ ).rejects.toThrow("callback failed");
189
+ expect(nativeDb.executeCalls.map(({ sql }) => sql)).toEqual([
190
+ "BEGIN",
191
+ "INSERT INTO items(value) VALUES (?)",
192
+ "ROLLBACK",
135
193
  ]);
136
194
  });
195
+
196
+ test("validates transaction timeouts", async () => {
197
+ const client = await db().createClient(
198
+ testProviderContext(new FakeSqliteDatabase()),
199
+ );
200
+ for (const timeout of [0, -1, Number.NaN, Number.POSITIVE_INFINITY]) {
201
+ await expect(
202
+ client.transaction(async () => {}, { timeout }),
203
+ ).rejects.toThrow("positive finite");
204
+ }
205
+ });
206
+
207
+ test("warns once for manual cross-call transactions and names the opt-out", async () => {
208
+ const client = await db().createClient(
209
+ testProviderContext(new FakeSqliteDatabase()),
210
+ );
211
+ await client.execute("BEGIN");
212
+ await client.execute("COMMIT");
213
+ expect(logLines).toHaveLength(1);
214
+ expect(JSON.parse(logLines[0] ?? "{}").msg).toContain(
215
+ "Set warnOnManualTransactions: false",
216
+ );
217
+ });
218
+
219
+ test("can suppress the manual transaction warning", async () => {
220
+ const client = await db({
221
+ warnOnManualTransactions: false,
222
+ }).createClient(testProviderContext(new FakeSqliteDatabase()));
223
+ await client.execute("BEGIN");
224
+ expect(logLines).toHaveLength(0);
225
+ });
137
226
  });
@@ -1,10 +1,23 @@
1
- import type { DatabaseProvider, RawAccess, SqliteDatabase } from "./config";
2
- import { isSqliteBindingObject, toSqliteBindings } from "./shared";
1
+ import { getLogger } from "@/common/log";
2
+ import type {
3
+ DatabaseProvider,
4
+ RawAccess,
5
+ SqliteDatabase,
6
+ SqliteTransactionDatabase,
7
+ } from "./config";
8
+ import {
9
+ isManualTransactionControl,
10
+ isSqliteBindingObject,
11
+ MIGRATION_TRANSACTION_TIMEOUT_MS,
12
+ toSqliteBindings,
13
+ validateTransactionTimeout,
14
+ } from "./shared";
3
15
 
4
16
  export type { RawAccess } from "./config";
5
17
 
6
18
  interface DatabaseFactoryConfig {
7
19
  onMigrate?: (db: RawAccess) => Promise<void> | void;
20
+ warnOnManualTransactions?: boolean;
8
21
  }
9
22
 
10
23
  function hasMultipleStatements(query: string): boolean {
@@ -14,6 +27,7 @@ function hasMultipleStatements(query: string): boolean {
14
27
 
15
28
  export function db({
16
29
  onMigrate,
30
+ warnOnManualTransactions = true,
17
31
  }: DatabaseFactoryConfig = {}): DatabaseProvider<RawAccess> {
18
32
  return {
19
33
  createClient: async (ctx) => {
@@ -26,6 +40,7 @@ export function db({
26
40
 
27
41
  const db = await nativeDatabaseProvider.open(ctx.actorId);
28
42
  let closed = false;
43
+ let manualTransactionWarned = false;
29
44
  const ensureOpen = () => {
30
45
  if (closed) {
31
46
  throw new Error(
@@ -34,7 +49,10 @@ export function db({
34
49
  }
35
50
  };
36
51
 
37
- const client: RawAccess = {
52
+ const createClient = (
53
+ target: SqliteDatabase | SqliteTransactionDatabase,
54
+ transactionScoped = false,
55
+ ): RawAccess => ({
38
56
  execute: async <
39
57
  TRow extends Record<string, unknown> = Record<
40
58
  string,
@@ -45,6 +63,19 @@ export function db({
45
63
  ...args: unknown[]
46
64
  ): Promise<TRow[]> => {
47
65
  ensureOpen();
66
+ if (
67
+ !transactionScoped &&
68
+ warnOnManualTransactions &&
69
+ !manualTransactionWarned &&
70
+ !hasMultipleStatements(query) &&
71
+ isManualTransactionControl(query)
72
+ ) {
73
+ manualTransactionWarned = true;
74
+ getLogger("database").warn(
75
+ { actorId: ctx.actorId },
76
+ "Manual cross-call SQLite transactions can interleave with other actor work. Use db.transaction() for coordinated transactions. Set warnOnManualTransactions: false in your db(...) configuration to disable this warning.",
77
+ );
78
+ }
48
79
 
49
80
  const kvReadsBefore = ctx.metrics?.totalKvReads ?? 0;
50
81
  const kvWritesBefore = ctx.metrics?.totalKvWrites ?? 0;
@@ -57,7 +88,7 @@ export function db({
57
88
  isSqliteBindingObject(args[0])
58
89
  ? toSqliteBindings(args[0])
59
90
  : toSqliteBindings(args);
60
- const { rows, columns } = await db.execute(
91
+ const { rows, columns } = await target.execute(
61
92
  query,
62
93
  bindings,
63
94
  );
@@ -67,13 +98,16 @@ export function db({
67
98
  }
68
99
 
69
100
  if (!hasMultipleStatements(query)) {
70
- const { rows, columns } = await db.execute(query);
101
+ const { rows, columns } = await target.execute(
102
+ query,
103
+ undefined,
104
+ );
71
105
  return rows.map((row) =>
72
106
  rowToObject<TRow>(row, columns),
73
107
  );
74
108
  }
75
109
 
76
- return await execMultiStatement<TRow>(db, query);
110
+ return await execMultiStatement<TRow>(target, query);
77
111
  } finally {
78
112
  const durationMs = performance.now() - start;
79
113
  ctx.metrics?.trackSql(query, durationMs);
@@ -92,6 +126,28 @@ export function db({
92
126
  }
93
127
  }
94
128
  },
129
+ transaction: async <T>(
130
+ callback: (tx: RawAccess) => Promise<T> | T,
131
+ options?: { timeout?: number },
132
+ ): Promise<T> => {
133
+ validateTransactionTimeout(options?.timeout);
134
+ const transaction = await db.beginTransaction(
135
+ options?.timeout,
136
+ );
137
+ const tx = createClient(transaction, true);
138
+ try {
139
+ const result = await callback(tx);
140
+ await transaction.commit();
141
+ return result;
142
+ } catch (error) {
143
+ try {
144
+ await transaction.rollback();
145
+ } catch {
146
+ // Preserve the callback or commit error after expiry cleanup.
147
+ }
148
+ throw error;
149
+ }
150
+ },
95
151
  close: async () => {
96
152
  if (!closed) {
97
153
  closed = true;
@@ -99,12 +155,15 @@ export function db({
99
155
  }
100
156
  },
101
157
  nativeMetrics: () => db.nativeMetrics?.() ?? null,
102
- };
158
+ });
159
+ const client = createClient(db);
103
160
  return client;
104
161
  },
105
162
  onMigrate: async (client) => {
106
163
  if (onMigrate) {
107
- await withMigrationSavepoint(client, () => onMigrate(client));
164
+ await withMigrationSavepoint(client, (leased) =>
165
+ onMigrate(leased),
166
+ );
108
167
  }
109
168
  },
110
169
  };
@@ -122,7 +181,7 @@ function rowToObject<TRow extends Record<string, unknown>>(
122
181
  }
123
182
 
124
183
  async function execMultiStatement<TRow extends Record<string, unknown>>(
125
- db: SqliteDatabase,
184
+ db: Pick<SqliteDatabase, "exec">,
126
185
  query: string,
127
186
  ): Promise<TRow[]> {
128
187
  const results: Record<string, unknown>[] = [];
@@ -138,19 +197,30 @@ async function execMultiStatement<TRow extends Record<string, unknown>>(
138
197
 
139
198
  async function withMigrationSavepoint<T>(
140
199
  client: RawAccess,
141
- callback: () => Promise<T> | T,
200
+ callback: (leased: RawAccess) => Promise<T> | T,
142
201
  ): Promise<T> {
143
- await client.execute("SAVEPOINT __rivet_on_migrate");
144
- try {
145
- const result = await callback();
146
- await client.execute("RELEASE SAVEPOINT __rivet_on_migrate");
147
- return result;
148
- } catch (error) {
149
- try {
150
- await client.execute("ROLLBACK TO SAVEPOINT __rivet_on_migrate");
151
- } finally {
152
- await client.execute("RELEASE SAVEPOINT __rivet_on_migrate");
153
- }
154
- throw error;
155
- }
202
+ return await client.transaction(
203
+ async (transaction) => {
204
+ await transaction.execute("SAVEPOINT __rivet_on_migrate");
205
+ try {
206
+ const result = await callback(transaction);
207
+ await transaction.execute(
208
+ "RELEASE SAVEPOINT __rivet_on_migrate",
209
+ );
210
+ return result;
211
+ } catch (error) {
212
+ try {
213
+ await transaction.execute(
214
+ "ROLLBACK TO SAVEPOINT __rivet_on_migrate",
215
+ );
216
+ } finally {
217
+ await transaction.execute(
218
+ "RELEASE SAVEPOINT __rivet_on_migrate",
219
+ );
220
+ }
221
+ throw error;
222
+ }
223
+ },
224
+ { timeout: MIGRATION_TRANSACTION_TIMEOUT_MS },
225
+ );
156
226
  }
@@ -19,6 +19,15 @@ function deferred<T>() {
19
19
  }
20
20
 
21
21
  class FakeNativeDatabase implements JsNativeDatabaseLike {
22
+ async beginTransaction() {
23
+ return {
24
+ exec: async (_sql: string) => this.exec(),
25
+ execute: async (sql: string, params?: NativeParams) =>
26
+ this.execute(sql, params),
27
+ commit: async () => {},
28
+ rollback: async () => {},
29
+ };
30
+ }
22
31
  active = 0;
23
32
  maxActive = 0;
24
33
  closed = false;