@omg-dev/server 0.4.34 → 0.4.37
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.
- package/dist/index.mjs +1 -0
- package/package.json +4 -4
- package/src/db.ts +7 -0
- package/src/test/db-contention.test.ts +63 -0
package/dist/index.mjs
CHANGED
|
@@ -831,6 +831,7 @@ var VibesAuthRequiredError = class extends Error {
|
|
|
831
831
|
function openDb(path) {
|
|
832
832
|
const bun = new Database(path, { create: true });
|
|
833
833
|
bun.exec("PRAGMA journal_mode=WAL;");
|
|
834
|
+
bun.exec("PRAGMA busy_timeout=5000;");
|
|
834
835
|
function now() {
|
|
835
836
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
836
837
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omg-dev/server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.37",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@restatedev/restate-sdk": "1.14.5",
|
|
17
|
-
"@omg-dev/auth": "0.4.
|
|
18
|
-
"@omg-dev/schema": "0.4.
|
|
19
|
-
"@omg-dev/stream": "0.4.
|
|
17
|
+
"@omg-dev/auth": "0.4.37",
|
|
18
|
+
"@omg-dev/schema": "0.4.37",
|
|
19
|
+
"@omg-dev/stream": "0.4.37",
|
|
20
20
|
"web-push": "^3.6.7"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
package/src/db.ts
CHANGED
|
@@ -68,6 +68,13 @@ export function openDb(path: string): VibesDb {
|
|
|
68
68
|
const bun = new Database(path, { create: true })
|
|
69
69
|
// Enable WAL mode for better concurrency
|
|
70
70
|
bun.exec("PRAGMA journal_mode=WAL;")
|
|
71
|
+
// Litestream is a second SQLite process, not a passive file reader. Its
|
|
72
|
+
// periodic checkpoint briefly owns SQLite's write lock. A connection with
|
|
73
|
+
// the default zero busy timeout turns that valid handoff into SQLITE_BUSY,
|
|
74
|
+
// which made unrelated control-plane writes fail at the checkpoint boundary.
|
|
75
|
+
// Put all writers through SQLite's process-safe lock queue instead of trying
|
|
76
|
+
// to coordinate selected JavaScript call sites inside this process.
|
|
77
|
+
bun.exec("PRAGMA busy_timeout=5000;")
|
|
71
78
|
|
|
72
79
|
function now(): string {
|
|
73
80
|
return new Date().toISOString()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "bun:test"
|
|
2
|
+
import fs from "node:fs"
|
|
3
|
+
import os from "node:os"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
import { openDb, type VibesDb } from "../db.ts"
|
|
6
|
+
|
|
7
|
+
const dbPaths: string[] = []
|
|
8
|
+
|
|
9
|
+
function tmpDbPath(): string {
|
|
10
|
+
const dbPath = path.join(os.tmpdir(), `vibes-contention-${crypto.randomUUID()}.db`)
|
|
11
|
+
dbPaths.push(dbPath)
|
|
12
|
+
return dbPath
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
for (const dbPath of dbPaths.splice(0)) {
|
|
17
|
+
for (const suffix of ["", "-shm", "-wal"]) {
|
|
18
|
+
try {
|
|
19
|
+
fs.unlinkSync(dbPath + suffix)
|
|
20
|
+
} catch {}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe("SQLite writer coordination", () => {
|
|
26
|
+
it("waits for a second process to release Litestream's write lock", async () => {
|
|
27
|
+
const dbPath = tmpDbPath()
|
|
28
|
+
const app: VibesDb = openDb(dbPath)
|
|
29
|
+
app.raw().exec("CREATE TABLE writes (value TEXT)")
|
|
30
|
+
|
|
31
|
+
const locker = Bun.spawn(
|
|
32
|
+
[
|
|
33
|
+
process.execPath,
|
|
34
|
+
"-e",
|
|
35
|
+
`
|
|
36
|
+
import { Database } from "bun:sqlite";
|
|
37
|
+
const db = new Database(process.argv[1]);
|
|
38
|
+
db.exec("BEGIN IMMEDIATE");
|
|
39
|
+
console.log("locked");
|
|
40
|
+
await Bun.sleep(150);
|
|
41
|
+
db.exec("COMMIT");
|
|
42
|
+
db.close();
|
|
43
|
+
`,
|
|
44
|
+
dbPath,
|
|
45
|
+
],
|
|
46
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const reader = locker.stdout.getReader()
|
|
50
|
+
const firstOutput = await reader.read()
|
|
51
|
+
expect(new TextDecoder().decode(firstOutput.value)).toContain("locked")
|
|
52
|
+
|
|
53
|
+
expect(() =>
|
|
54
|
+
app.raw().prepare("INSERT INTO writes (value) VALUES (?)").run("saved"),
|
|
55
|
+
).not.toThrow()
|
|
56
|
+
expect(await locker.exited).toBe(0)
|
|
57
|
+
expect(
|
|
58
|
+
app.raw().prepare("SELECT value FROM writes").get() as { value: string },
|
|
59
|
+
).toEqual({ value: "saved" })
|
|
60
|
+
|
|
61
|
+
app.close()
|
|
62
|
+
})
|
|
63
|
+
})
|