this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Switch to @libsql/client to support remote database connections (#4)

---------

Co-authored-by: futurGH <me@futuristick.ca>

authored by

Ahmed Soliman
futurGH
and committed by
GitHub
c7460efe 39db9cad

+269 -69
+1 -1
package.json
··· 41 41 "@atcute/client": "^2.0.3", 42 42 "@atcute/ozone": "^1.0.4", 43 43 "@fastify/websocket": "^10.0.1", 44 + "@libsql/client": "^0.14.0", 44 45 "@noble/curves": "^1.6.0", 45 46 "@noble/hashes": "^1.5.0", 46 47 "fastify": "^4.28.1", 47 - "libsql": "^0.4.6", 48 48 "prompts": "^2.4.2", 49 49 "uint8arrays": "^5.1.0" 50 50 },
+109 -3
pnpm-lock.yaml
··· 23 23 '@fastify/websocket': 24 24 specifier: ^10.0.1 25 25 version: 10.0.1 26 + '@libsql/client': 27 + specifier: ^0.14.0 28 + version: 0.14.0 26 29 '@noble/curves': 27 30 specifier: ^1.6.0 28 31 version: 1.6.0 ··· 32 35 fastify: 33 36 specifier: ^4.28.1 34 37 version: 4.28.1 35 - libsql: 36 - specifier: ^0.4.6 37 - version: 0.4.6 38 38 prompts: 39 39 specifier: ^2.4.2 40 40 version: 2.4.2 ··· 173 173 '@humanwhocodes/object-schema@2.0.2': 174 174 resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} 175 175 176 + '@libsql/client@0.14.0': 177 + resolution: {integrity: sha512-/9HEKfn6fwXB5aTEEoMeFh4CtG0ZzbncBb1e++OCdVpgKZ/xyMsIVYXm0w7Pv4RUel803vE6LwniB3PqD72R0Q==} 178 + 179 + '@libsql/core@0.14.0': 180 + resolution: {integrity: sha512-nhbuXf7GP3PSZgdCY2Ecj8vz187ptHlZQ0VRc751oB2C1W8jQUXKKklvt7t1LJiUTQBVJuadF628eUk+3cRi4Q==} 181 + 176 182 '@libsql/darwin-arm64@0.4.6': 177 183 resolution: {integrity: sha512-45i604CJ2Lubbg7NqtDodjarF6VgST8rS5R8xB++MoRqixtDns9PZ6tocT9pRJDWuTWEiy2sjthPOFWMKwYAsg==} 178 184 cpu: [arm64] ··· 183 189 cpu: [x64] 184 190 os: [darwin] 185 191 192 + '@libsql/hrana-client@0.7.0': 193 + resolution: {integrity: sha512-OF8fFQSkbL7vJY9rfuegK1R7sPgQ6kFMkDamiEccNUvieQ+3urzfDFI616oPl8V7T9zRmnTkSjMOImYCAVRVuw==} 194 + 195 + '@libsql/isomorphic-fetch@0.3.1': 196 + resolution: {integrity: sha512-6kK3SUK5Uu56zPq/Las620n5aS9xJq+jMBcNSOmjhNf/MUvdyji4vrMTqD7ptY7/4/CAVEAYDeotUz60LNQHtw==} 197 + engines: {node: '>=18.0.0'} 198 + 199 + '@libsql/isomorphic-ws@0.1.5': 200 + resolution: {integrity: sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==} 201 + 186 202 '@libsql/linux-arm64-gnu@0.4.6': 187 203 resolution: {integrity: sha512-DMPavVyY6vYPAYcQR1iOotHszg+5xSjHSg6F9kNecPX0KKdGq84zuPJmORfKOPtaWvzPewNFdML/e+s1fu09XQ==} 188 204 cpu: [arm64] ··· 407 423 resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} 408 424 engines: {node: '>= 8'} 409 425 426 + data-uri-to-buffer@4.0.1: 427 + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} 428 + engines: {node: '>= 12'} 429 + 410 430 debug@4.3.4: 411 431 resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} 412 432 engines: {node: '>=6.0'} ··· 530 550 fastq@1.17.1: 531 551 resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} 532 552 553 + fetch-blob@3.2.0: 554 + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} 555 + engines: {node: ^12.20 || >= 14.13} 556 + 533 557 file-entry-cache@6.0.1: 534 558 resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} 535 559 engines: {node: ^10.12.0 || >=12.0.0} ··· 552 576 553 577 flatted@3.2.9: 554 578 resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} 579 + 580 + formdata-polyfill@4.0.10: 581 + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} 582 + engines: {node: '>=12.20.0'} 555 583 556 584 forwarded@0.2.0: 557 585 resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} ··· 633 661 isexe@2.0.0: 634 662 resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 635 663 664 + js-base64@3.7.7: 665 + resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} 666 + 636 667 js-yaml@4.1.0: 637 668 resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} 638 669 hasBin: true ··· 701 732 natural-compare@1.4.0: 702 733 resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} 703 734 735 + node-domexception@1.0.0: 736 + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} 737 + engines: {node: '>=10.5.0'} 738 + 739 + node-fetch@3.3.2: 740 + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} 741 + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 742 + 704 743 on-exit-leak-free@2.1.2: 705 744 resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} 706 745 engines: {node: '>=14.0.0'} ··· 767 806 process@0.11.10: 768 807 resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} 769 808 engines: {node: '>= 0.6.0'} 809 + 810 + promise-limit@2.7.0: 811 + resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==} 770 812 771 813 prompts@2.4.2: 772 814 resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} ··· 930 972 util-deprecate@1.0.2: 931 973 resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 932 974 975 + web-streams-polyfill@3.3.3: 976 + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} 977 + engines: {node: '>= 8'} 978 + 933 979 which@2.0.2: 934 980 resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 935 981 engines: {node: '>= 8'} ··· 1065 1111 1066 1112 '@humanwhocodes/object-schema@2.0.2': {} 1067 1113 1114 + '@libsql/client@0.14.0': 1115 + dependencies: 1116 + '@libsql/core': 0.14.0 1117 + '@libsql/hrana-client': 0.7.0 1118 + js-base64: 3.7.7 1119 + libsql: 0.4.6 1120 + promise-limit: 2.7.0 1121 + transitivePeerDependencies: 1122 + - bufferutil 1123 + - utf-8-validate 1124 + 1125 + '@libsql/core@0.14.0': 1126 + dependencies: 1127 + js-base64: 3.7.7 1128 + 1068 1129 '@libsql/darwin-arm64@0.4.6': 1069 1130 optional: true 1070 1131 1071 1132 '@libsql/darwin-x64@0.4.6': 1072 1133 optional: true 1073 1134 1135 + '@libsql/hrana-client@0.7.0': 1136 + dependencies: 1137 + '@libsql/isomorphic-fetch': 0.3.1 1138 + '@libsql/isomorphic-ws': 0.1.5 1139 + js-base64: 3.7.7 1140 + node-fetch: 3.3.2 1141 + transitivePeerDependencies: 1142 + - bufferutil 1143 + - utf-8-validate 1144 + 1145 + '@libsql/isomorphic-fetch@0.3.1': {} 1146 + 1147 + '@libsql/isomorphic-ws@0.1.5': 1148 + dependencies: 1149 + '@types/ws': 8.5.12 1150 + ws: 8.18.0 1151 + transitivePeerDependencies: 1152 + - bufferutil 1153 + - utf-8-validate 1154 + 1074 1155 '@libsql/linux-arm64-gnu@0.4.6': 1075 1156 optional: true 1076 1157 ··· 1300 1381 shebang-command: 2.0.0 1301 1382 which: 2.0.2 1302 1383 1384 + data-uri-to-buffer@4.0.1: {} 1385 + 1303 1386 debug@4.3.4: 1304 1387 dependencies: 1305 1388 ms: 2.1.2 ··· 1472 1555 dependencies: 1473 1556 reusify: 1.0.4 1474 1557 1558 + fetch-blob@3.2.0: 1559 + dependencies: 1560 + node-domexception: 1.0.0 1561 + web-streams-polyfill: 3.3.3 1562 + 1475 1563 file-entry-cache@6.0.1: 1476 1564 dependencies: 1477 1565 flat-cache: 3.2.0 ··· 1498 1586 rimraf: 3.0.2 1499 1587 1500 1588 flatted@3.2.9: {} 1589 + 1590 + formdata-polyfill@4.0.10: 1591 + dependencies: 1592 + fetch-blob: 3.2.0 1501 1593 1502 1594 forwarded@0.2.0: {} 1503 1595 ··· 1571 1663 1572 1664 isexe@2.0.0: {} 1573 1665 1666 + js-base64@3.7.7: {} 1667 + 1574 1668 js-yaml@4.1.0: 1575 1669 dependencies: 1576 1670 argparse: 2.0.1 ··· 1644 1738 1645 1739 natural-compare@1.4.0: {} 1646 1740 1741 + node-domexception@1.0.0: {} 1742 + 1743 + node-fetch@3.3.2: 1744 + dependencies: 1745 + data-uri-to-buffer: 4.0.1 1746 + fetch-blob: 3.2.0 1747 + formdata-polyfill: 4.0.10 1748 + 1647 1749 on-exit-leak-free@2.1.2: {} 1648 1750 1649 1751 once@1.4.0: ··· 1709 1811 process-warning@4.0.0: {} 1710 1812 1711 1813 process@0.11.10: {} 1814 + 1815 + promise-limit@2.7.0: {} 1712 1816 1713 1817 prompts@2.4.2: 1714 1818 dependencies: ··· 1843 1947 punycode: 2.3.1 1844 1948 1845 1949 util-deprecate@1.0.2: {} 1950 + 1951 + web-streams-polyfill@3.3.3: {} 1846 1952 1847 1953 which@2.0.2: 1848 1954 dependencies:
+159 -65
src/LabelerServer.ts
··· 6 6 ToolsOzoneModerationEmitEvent, 7 7 } from "@atcute/client/lexicons"; 8 8 import { fastifyWebsocket } from "@fastify/websocket"; 9 + import { Client, createClient } from "@libsql/client"; 9 10 import fastify, { 10 11 type FastifyInstance, 11 12 type FastifyListenOptions, 12 13 type FastifyRequest, 13 14 } from "fastify"; 14 - import Database, { type Database as SQLiteDatabase } from "libsql"; 15 15 import type { WebSocket } from "ws"; 16 16 import { parsePrivateKey, verifyJwt } from "./util/crypto.js"; 17 17 import { formatLabel, labelIsSigned, signLabel } from "./util/labels.js"; ··· 50 50 * @param did The DID to check. 51 51 */ 52 52 auth?: (did: string) => boolean | Promise<boolean>; 53 + 53 54 /** 54 55 * The path to the SQLite `.db` database file. 55 56 * @default labels.db 56 57 */ 57 58 dbPath?: string; 59 + 60 + /** 61 + * The URL of the remote SQLite database. 62 + * If provided, {@link dbPath} is ignored. 63 + */ 64 + dbUrl?: string; 65 + 66 + /** 67 + * The authentication token for the remote SQLite database. 68 + * Required if {@link dbUrl} is provided. 69 + */ 70 + dbToken?: string; 58 71 } 59 72 60 73 export class LabelerServer { ··· 62 75 app: FastifyInstance; 63 76 64 77 /** The SQLite database instance. */ 65 - db: SQLiteDatabase; 78 + db: Client; 66 79 67 80 /** The DID of the labeler account. */ 68 81 did: At.DID; ··· 77 90 #signingKey: Uint8Array; 78 91 79 92 /** 93 + * Promise that resolves when database initialization is complete. 94 + * This should be awaited before any database operations. 95 + */ 96 + private readonly dbInitLock?: Promise<void>; 97 + 98 + /** 80 99 * Create a labeler server. 81 100 * @param options Configuration options. 82 101 */ ··· 92 111 throw new Error(INVALID_SIGNING_KEY_ERROR); 93 112 } 94 113 95 - this.db = new Database(options.dbPath ?? "labels.db"); 96 - this.db.pragma("journal_mode = WAL"); 97 - this.db.exec(` 98 - CREATE TABLE IF NOT EXISTS labels ( 99 - id INTEGER PRIMARY KEY AUTOINCREMENT, 100 - src TEXT NOT NULL, 101 - uri TEXT NOT NULL, 102 - cid TEXT, 103 - val TEXT NOT NULL, 104 - neg BOOLEAN DEFAULT FALSE, 105 - cts DATETIME NOT NULL, 106 - exp DATETIME, 107 - sig BLOB 108 - ); 109 - `); 114 + if (options.dbUrl) { 115 + if (!options.dbToken) { 116 + throw new Error( 117 + "The `dbToken` option is required when using a remote database URL.", 118 + ); 119 + } 120 + this.db = createClient({ url: options.dbUrl, authToken: options.dbToken }); 121 + } else { 122 + this.db = createClient({ url: "file:" + (options.dbPath ?? "labels.db") }); 123 + } 124 + 125 + this.dbInitLock = this.initializeDatabase(); 110 126 111 127 this.app = fastify(); 112 128 void this.app.register(fastifyWebsocket).then(() => { ··· 123 139 } 124 140 125 141 /** 142 + * Initializes the database with the required schema. 143 + * @returns A promise that resolves when initialization is complete 144 + */ 145 + private async initializeDatabase() { 146 + await this.db.execute("PRAGMA journal_mode = WAL").catch(() => { 147 + console.warn( 148 + "Unable to set WAL mode — performance and concurrent access may be impacted.", 149 + ); 150 + }); 151 + 152 + await this.db.execute(` 153 + CREATE TABLE IF NOT EXISTS labels ( 154 + id INTEGER PRIMARY KEY AUTOINCREMENT, 155 + src TEXT NOT NULL, 156 + uri TEXT NOT NULL, 157 + cid TEXT, 158 + val TEXT NOT NULL, 159 + neg BOOLEAN DEFAULT FALSE, 160 + cts DATETIME NOT NULL, 161 + exp DATETIME, 162 + sig BLOB 163 + ); 164 + `).catch((error) => { 165 + console.error("Failed to initialize database:", error); 166 + throw error; 167 + }); 168 + } 169 + 170 + /** 126 171 * Start the server. 127 172 * @param port The port to listen on. 128 173 * @param callback A callback to run when the server is started. ··· 169 214 * @param label The label to insert. 170 215 * @returns The inserted label. 171 216 */ 172 - private saveLabel(label: UnsignedLabel): SavedLabel { 217 + private async saveLabel(label: UnsignedLabel): Promise<SavedLabel> { 218 + await this.dbInitLock; 219 + 173 220 const signed = labelIsSigned(label) ? label : signLabel(label, this.#signingKey); 221 + const { src, uri, cid, val, neg, cts, exp, sig } = signed; 174 222 175 - const stmt = this.db.prepare(` 176 - INSERT INTO labels (src, uri, cid, val, neg, cts, exp, sig) 177 - VALUES (?, ?, ?, ?, ?, ?, ?, ?) 178 - `); 223 + const sql = ` 224 + INSERT INTO labels (src, uri, cid, val, neg, cts, exp, sig) 225 + VALUES (?, ?, ?, ?, ?, ?, ?, ?) 226 + RETURNING id 227 + `; 179 228 180 - const { src, uri, cid, val, neg, cts, exp, sig } = signed; 181 - const result = stmt.run(src, uri, cid, val, neg ? 1 : 0, cts, exp, sig); 182 - if (!result.changes) throw new Error("Failed to insert label"); 229 + const args = [src, uri, cid || null, val, neg ? 1 : 0, cts, exp || null, sig]; 183 230 184 - const id = Number(result.lastInsertRowid); 231 + const result = await this.db.execute({ sql, args }); 232 + if (!result.rows.length) throw new Error("Failed to insert label"); 233 + 234 + const id = Number(result.rows[0].id); 185 235 186 236 this.emitLabel(id, signed); 187 237 return { id, ...signed }; ··· 192 242 * @param label The label to create. 193 243 * @returns The created label. 194 244 */ 195 - createLabel(label: CreateLabelData): SavedLabel { 196 - return this.saveLabel( 245 + async createLabel(label: CreateLabelData): Promise<SavedLabel> { 246 + return await this.saveLabel( 197 247 excludeNullish({ 198 248 ...label, 199 249 src: (label.src ?? this.did) as At.DID, ··· 208 258 * @param labels The labels to create. 209 259 * @returns The created labels. 210 260 */ 211 - createLabels( 261 + async createLabels( 212 262 subject: { uri: string; cid?: string | undefined }, 213 263 labels: { create?: Array<string>; negate?: Array<string> }, 214 - ): Array<SavedLabel> { 264 + ): Promise<Array<SavedLabel>> { 265 + await this.dbInitLock; 266 + 215 267 const { uri, cid } = subject; 216 268 const { create, negate } = labels; 217 269 218 270 const createdLabels: Array<SavedLabel> = []; 219 271 if (create) { 220 272 for (const val of create) { 221 - const created = this.createLabel({ uri, cid, val }); 273 + const created = await this.createLabel({ uri, cid, val }); 222 274 createdLabels.push(created); 223 275 } 224 276 } 225 277 if (negate) { 226 278 for (const val of negate) { 227 - const negated = this.createLabel({ uri, cid, val, neg: true }); 279 + const negated = await this.createLabel({ uri, cid, val, neg: true }); 228 280 createdLabels.push(negated); 229 281 } 230 282 } ··· 278 330 * Handler for [com.atproto.label.queryLabels](https://github.com/bluesky-social/atproto/blob/main/lexicons/com/atproto/label/queryLabels.json). 279 331 */ 280 332 queryLabelsHandler: QueryHandler<ComAtprotoLabelQueryLabels.Params> = async (req, res) => { 333 + await this.dbInitLock; 334 + 281 335 let uriPatterns: Array<string>; 282 336 if (!req.query.uriPatterns) { 283 337 uriPatterns = []; ··· 327 381 return pattern.slice(0, -1) + "%"; 328 382 }); 329 383 330 - const stmt = this.db.prepare(` 331 - SELECT * FROM labels 332 - WHERE 1 = 1 333 - ${patterns.length ? "AND " + patterns.map(() => "uri LIKE ?").join(" OR ") : ""} 334 - ${sources.length ? `AND src IN (${sources.map(() => "?").join(", ")})` : ""} 335 - ${cursor ? "AND id > ?" : ""} 336 - ORDER BY id ASC 337 - LIMIT ? 338 - `); 384 + const conditions: string[] = []; 385 + const params: any[] = []; 339 386 340 - const params = []; 341 - if (patterns.length) params.push(...patterns); 342 - if (sources.length) params.push(...sources); 343 - if (cursor) params.push(cursor); 387 + if (patterns.length) { 388 + conditions.push("(" + patterns.map(() => "uri LIKE ?").join(" OR ") + ")"); 389 + params.push(...patterns); 390 + } 391 + 392 + if (sources.length) { 393 + conditions.push(`src IN (${sources.map(() => "?").join(", ")})`); 394 + params.push(...sources); 395 + } 396 + 397 + if (cursor) { 398 + conditions.push("id > ?"); 399 + params.push(cursor); 400 + } 401 + 344 402 params.push(limit); 345 403 346 - const rows = stmt.all(params) as Array<SavedLabel>; 404 + const whereClause = conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""; 405 + const result = await this.db.execute({ 406 + sql: ` 407 + SELECT * FROM labels 408 + ${whereClause} 409 + ORDER BY id ASC 410 + LIMIT ? 411 + `, 412 + args: params, 413 + }); 414 + 415 + const rows = result.rows.map((row) => ({ 416 + id: Number(row.id), 417 + src: row.src as At.DID, 418 + uri: row.uri as string, 419 + val: row.val as string, 420 + neg: Boolean(row.neg), 421 + cts: row.cts as string, 422 + ...(row.cid ? { cid: row.cid as string } : {}), 423 + ...(row.exp ? { exp: row.exp as string } : {}), 424 + ...(row.sig ? { sig: row.sig as Uint8Array } : {}), 425 + })); 347 426 const labels = rows.map(formatLabel); 348 427 349 428 const nextCursor = rows[rows.length - 1]?.id?.toString(10) || "0"; ··· 354 433 /** 355 434 * Handler for [com.atproto.label.subscribeLabels](https://github.com/bluesky-social/atproto/blob/main/lexicons/com/atproto/label/subscribeLabels.json). 356 435 */ 357 - subscribeLabelsHandler: SubscriptionHandler<{ cursor?: string }> = (ws, req) => { 436 + subscribeLabelsHandler: SubscriptionHandler<{ cursor?: string }> = async (ws, req) => { 437 + await this.dbInitLock; 438 + 358 439 const cursor = parseInt(req.query.cursor ?? "NaN", 10); 359 440 360 441 if (!Number.isNaN(cursor)) { 361 - const latest = this.db.prepare(` 362 - SELECT MAX(id) AS id FROM labels 363 - `).get() as { id: number }; 364 - if (cursor > (latest.id ?? 0)) { 442 + const latest = await this.db.execute({ 443 + sql: "SELECT MAX(id) AS id FROM labels", 444 + args: [], 445 + }); 446 + if (cursor > (Number(latest.rows[0]?.id) ?? 0)) { 365 447 const errorBytes = frameToBytes("error", { 366 448 error: "FutureCursor", 367 449 message: "Cursor is in the future", ··· 370 452 ws.terminate(); 371 453 } 372 454 373 - const stmt = this.db.prepare<[number]>(` 374 - SELECT * FROM labels 375 - WHERE id > ? 376 - ORDER BY id ASC 377 - `); 378 - 379 455 try { 380 - for (const row of stmt.iterate(cursor)) { 381 - const { id: seq, ...label } = row as SavedLabel; 382 - const bytes = frameToBytes( 383 - "message", 384 - { seq, labels: [formatLabel(label)] }, 385 - "#labels", 386 - ); 456 + const result = await this.db.execute({ 457 + sql: ` 458 + SELECT * FROM labels 459 + WHERE id > ? 460 + ORDER BY id ASC 461 + `, 462 + args: [cursor], 463 + }); 464 + 465 + for (const row of result.rows) { 466 + const { id: seq, src, uri, cid, val, neg, cts, exp, sig } = row; 467 + const label = { 468 + src: src as At.DID, 469 + uri: uri as string, 470 + val: val as string, 471 + neg: Boolean(neg), 472 + cts: cts as string, 473 + ...(cid ? { cid: cid as string } : {}), 474 + ...(exp ? { exp: exp as string } : {}), 475 + ...(sig ? { sig: sig as Uint8Array } : {}), 476 + }; 477 + const bytes = frameToBytes("message", { 478 + seq: Number(seq), 479 + labels: [formatLabel(label)], 480 + }, "#labels"); 387 481 ws.send(bytes); 388 482 } 389 483 } catch (e) { ··· 447 541 throw new XRPCError(400, { kind: "InvalidRequest", description: "Invalid subject" }); 448 542 } 449 543 450 - const labels = this.createLabels({ uri, cid }, { 544 + const labels = await this.createLabels({ uri, cid }, { 451 545 create: event.createLabelVals, 452 546 negate: event.negateLabelVals, 453 547 });