diff --git a/.gitignore b/.gitignore index add2716..6aaf8bd 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json *.db *.db-journal +*.db-shm +*.db-wal diff --git a/deno.json b/deno.json index c26f659..bc6d46b 100644 --- a/deno.json +++ b/deno.json @@ -1,12 +1,13 @@ { "tasks": { - "jetstream:prod": "deno run --allow-net --allow-read --allow-write --allow-env jetstream.ts", - "jetstream": "deno run --watch --allow-net --allow-read --allow-write --allow-env jetstream.ts", - "server": "deno run --watch --allow-net --allow-read --allow-write --allow-env server.ts", - "server:prod": "deno run --allow-net --allow-read --allow-write --allow-env server.ts", - "migrate": "deno run --allow-read --allow-write --allow-env src/migrations/init.ts" + "jetstream:prod": "deno run --allow-net --allow-read --allow-write --allow-env --allow-ffi --unstable-ffi jetstream.ts", + "jetstream": "deno run --watch --allow-net --allow-read --allow-write --allow-env --allow-ffi --unstable-ffi jetstream.ts", + "server": "deno run --watch --allow-net --allow-read --allow-write --allow-env --allow-ffi --unstable-ffi server.ts", + "server:prod": "deno run --allow-net --allow-read --allow-write --allow-env --allow-ffi --unstable-ffi server.ts", + "migrate": "deno run --allow-net --allow-read --allow-write --allow-env --allow-ffi --unstable-ffi src/migrations/init.ts" }, "imports": { + "@db/sqlite": "jsr:@db/sqlite@^0.13.0", "@oak/oak": "jsr:@oak/oak@^17.2.0", "@skyware/jetstream": "npm:@skyware/jetstream@^0.2.5", "@std/assert": "jsr:@std/assert@1" diff --git a/deno.lock b/deno.lock index e959212..5be5f47 100644 --- a/deno.lock +++ b/deno.lock @@ -1,6 +1,9 @@ { "version": "5", "specifiers": { + "jsr:@db/sqlite@*": "0.13.0", + "jsr:@db/sqlite@0.13": "0.13.0", + "jsr:@denosaurs/plug@1": "1.1.0", "jsr:@oak/commons@1": "1.0.0", "jsr:@oak/oak@^17.2.0": "17.2.0", "jsr:@std/assert@1": "1.0.18", @@ -8,14 +11,35 @@ "jsr:@std/crypto@1": "1.0.3", "jsr:@std/encoding@1": "1.0.6", "jsr:@std/encoding@^1.0.5": "1.0.6", + "jsr:@std/fmt@1": "1.0.9", + "jsr:@std/fs@1": "1.0.19", "jsr:@std/http@1": "1.0.12", "jsr:@std/internal@^1.0.12": "1.0.12", + "jsr:@std/internal@^1.0.9": "1.0.12", "jsr:@std/media-types@1": "1.1.0", "jsr:@std/path@1": "1.1.4", + "jsr:@std/path@1.0": "1.0.9", + "jsr:@std/path@^1.1.1": "1.1.4", "npm:@skyware/jetstream@~0.2.5": "0.2.5", "npm:path-to-regexp@^6.3.0": "6.3.0" }, "jsr": { + "@db/sqlite@0.13.0": { + "integrity": "4545c635e0b3d4ddfdc0f2240f932f24b8ad0178e9c2e3a0f9403e7b18ae2fb5", + "dependencies": [ + "jsr:@denosaurs/plug", + "jsr:@std/path@1.0" + ] + }, + "@denosaurs/plug@1.1.0": { + "integrity": "eb2f0b7546c7bca2000d8b0282c54d50d91cf6d75cb26a80df25a6de8c4bc044", + "dependencies": [ + "jsr:@std/encoding@1", + "jsr:@std/fmt", + "jsr:@std/fs", + "jsr:@std/path@1" + ] + }, "@oak/commons@1.0.0": { "integrity": "49805b55603c3627a9d6235c0655aa2b6222d3036b3a13ff0380c16368f607ac", "dependencies": [ @@ -35,14 +59,14 @@ "jsr:@std/bytes", "jsr:@std/http", "jsr:@std/media-types", - "jsr:@std/path", + "jsr:@std/path@1", "npm:path-to-regexp" ] }, "@std/assert@1.0.18": { "integrity": "270245e9c2c13b446286de475131dc688ca9abcd94fc5db41d43a219b34d1c78", "dependencies": [ - "jsr:@std/internal" + "jsr:@std/internal@^1.0.12" ] }, "@std/bytes@1.0.4": { @@ -54,6 +78,16 @@ "@std/encoding@1.0.6": { "integrity": "ca87122c196e8831737d9547acf001766618e78cd8c33920776c7f5885546069" }, + "@std/fmt@1.0.9": { + "integrity": "2487343e8899fb2be5d0e3d35013e54477ada198854e52dd05ed0422eddcabe0" + }, + "@std/fs@1.0.19": { + "integrity": "051968c2b1eae4d2ea9f79a08a3845740ef6af10356aff43d3e2ef11ed09fb06", + "dependencies": [ + "jsr:@std/internal@^1.0.9", + "jsr:@std/path@^1.1.1" + ] + }, "@std/http@1.0.12": { "integrity": "85246d8bfe9c8e2538518725b158bdc31f616e0869255f4a8d9e3de919cab2aa", "dependencies": [ @@ -66,10 +100,13 @@ "@std/media-types@1.1.0": { "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" }, + "@std/path@1.0.9": { + "integrity": "260a49f11edd3db93dd38350bf9cd1b4d1366afa98e81b86167b4e3dd750129e" + }, "@std/path@1.1.4": { "integrity": "1d2d43f39efb1b42f0b1882a25486647cb851481862dc7313390b2bb044314b5", "dependencies": [ - "jsr:@std/internal" + "jsr:@std/internal@^1.0.12" ] } }, @@ -156,6 +193,7 @@ }, "workspace": { "dependencies": [ + "jsr:@db/sqlite@0.13", "jsr:@oak/oak@^17.2.0", "jsr:@std/assert@1", "npm:@skyware/jetstream@~0.2.5" diff --git a/jetstream.ts b/jetstream.ts index ccf159a..224bdf3 100644 --- a/jetstream.ts +++ b/jetstream.ts @@ -77,5 +77,6 @@ jetstream.start(); setInterval(() => { if (jetstream.cursor) { saveCursor(jetstream.cursor); + console.log(`[jetstream] updated cursor: ${jetstream.cursor}`); } }, 5000); diff --git a/src/data/db.ts b/src/data/db.ts index 0034a51..ea2baf3 100644 --- a/src/data/db.ts +++ b/src/data/db.ts @@ -1,33 +1,32 @@ -import { DB } from "https://deno.land/x/sqlite/mod.ts"; +import { Database } from "@db/sqlite"; import type { Note } from "./note.ts"; -export const db = new DB(Deno.env.get("SQLITE_PATH") ?? "notes.db"); +export const db = new Database(Deno.env.get("SQLITE_PATH") ?? "notes.db"); try { - const [[journalMode]] = db.query<[string]>("PRAGMA journal_mode=WAL"); - db.query("PRAGMA busy_timeout=5000"); - console.log(`[db] journal_mode=${journalMode}, busy_timeout=5000`); + db.exec("PRAGMA journal_mode=WAL"); + db.exec("PRAGMA busy_timeout=5000"); + const [row] = db.prepare("PRAGMA journal_mode").all<{ journal_mode: string }>(); + console.log(`[db] journal_mode=${row.journal_mode}, busy_timeout=5000`); } catch (e) { console.error("[db] failed to set PRAGMAs:", e); } -export const getNotes = (cursor?: string, limit = 20) => { - const rows = cursor - ? db.query<[string, string, string, string, string]>( - "SELECT did, rkey, title, publishedAt, createdAt FROM note WHERE rkey < ? ORDER BY rkey DESC LIMIT ?", - [cursor, limit], - ) - : db.query<[string, string, string, string, string]>( - "SELECT did, rkey, title, publishedAt, createdAt FROM note ORDER BY rkey DESC LIMIT ?", - [limit], - ); +type NoteRow = { + did: string; + rkey: string; + title: string; + publishedAt: string; + createdAt: string; +}; - const notes = rows.map(([did, rkey, title, publishedAt, createdAt]) => ({ - did, - rkey, - title, - publishedAt, - createdAt, - })); +export const getNotes = (cursor?: string, limit = 20) => { + const notes = cursor + ? db.prepare( + "SELECT did, rkey, title, publishedAt, createdAt FROM note WHERE rkey < ? ORDER BY rkey DESC LIMIT ?", + ).all(cursor, limit) + : db.prepare( + "SELECT did, rkey, title, publishedAt, createdAt FROM note ORDER BY rkey DESC LIMIT ?", + ).all(limit); return { notes, @@ -36,23 +35,13 @@ export const getNotes = (cursor?: string, limit = 20) => { }; export const getNotesByDid = (did: string, cursor?: string, limit = 20) => { - const rows = cursor - ? db.query<[string, string, string, string, string]>( + const notes = cursor + ? db.prepare( "SELECT did, rkey, title, publishedAt, createdAt FROM note WHERE did = ? AND rkey < ? ORDER BY rkey DESC LIMIT ?", - [did, cursor, limit], - ) - : db.query<[string, string, string, string, string]>( + ).all(did, cursor, limit) + : db.prepare( "SELECT did, rkey, title, publishedAt, createdAt FROM note WHERE did = ? ORDER BY rkey DESC LIMIT ?", - [did, limit], - ); - - const notes = rows.map(([did, rkey, title, publishedAt, createdAt]) => ({ - did, - rkey, - title, - publishedAt, - createdAt, - })); + ).all(did, limit); return { notes, @@ -61,35 +50,26 @@ export const getNotesByDid = (did: string, cursor?: string, limit = 20) => { }; export const deleteNote = ({ did, rkey }: { did: string; rkey: string }) => { - db.query("DELETE FROM note WHERE did = ? AND rkey = ?", [did, rkey]); + db.exec("DELETE FROM note WHERE did = ? AND rkey = ?", did, rkey); }; export const getCursor = (): string | undefined => { - const rows = db.query<[string]>( + const row = db.prepare( "SELECT value FROM state WHERE key = 'cursor'", - ); - return rows[0]?.[0]; + ).get<{ value: string }>(); + return row?.value; }; export const saveCursor = (cursor: number) => { - db.query( + db.exec( "INSERT OR REPLACE INTO state (key, value) VALUES ('cursor', ?)", - [String(cursor)], + String(cursor), ); }; export const upsertNote = (note: Note) => { const now = new Date().toISOString(); - const params = [ - note.title, - note.publishedAt ? new Date(note.publishedAt).toISOString() : now, - note.createdAt ? new Date(note.createdAt).toISOString() : now, - note.did, - note.rkey, - ]; - console.log(`[db] upsertNote params: ${JSON.stringify(params)}`); - - db.query( + db.exec( ` INSERT INTO note ( title, @@ -104,6 +84,10 @@ export const upsertNote = (note: Note) => { title = excluded.title, publishedAt = excluded.publishedAt `, - params, + note.title, + note.publishedAt ? new Date(note.publishedAt).toISOString() : now, + note.createdAt ? new Date(note.createdAt).toISOString() : now, + note.did, + note.rkey, ); }; diff --git a/src/migrations/init.ts b/src/migrations/init.ts index 7dfea35..61207a1 100644 --- a/src/migrations/init.ts +++ b/src/migrations/init.ts @@ -1,6 +1,6 @@ import { db } from "../data/db.ts"; -db.execute(` +db.exec(` CREATE TABLE IF NOT EXISTS note ( title TEXT NOT NULL, publishedAt DATETIME, @@ -11,7 +11,7 @@ db.execute(` ); `); -db.execute(` +db.exec(` CREATE TABLE IF NOT EXISTS state ( key TEXT PRIMARY KEY, value TEXT NOT NULL