tracks lexicons and how many times they appeared on the jetstream

feat: track nsids via jetstream

ptr.pet 0058b537 b704a38d

verified
+5
.gitignore
··· 21 21 # Vite 22 22 vite.config.js.timestamp-* 23 23 vite.config.ts.timestamp-* 24 + 25 + events.sqlite 26 + events.sqlite-journal 27 + events.sqlite-wal 28 + events.sqlite-shm
+33 -1
bun.lock
··· 3 3 "workspaces": { 4 4 "": { 5 5 "name": "nsid-tracker", 6 + "dependencies": { 7 + "@atcute/jetstream": "^1.0.2", 8 + }, 6 9 "devDependencies": { 7 10 "@eslint/compat": "^1.2.5", 8 11 "@eslint/js": "^9.18.0", ··· 10 13 "@sveltejs/kit": "^2.22.0", 11 14 "@sveltejs/vite-plugin-svelte": "^6.0.0", 12 15 "@tailwindcss/vite": "^4.0.0", 16 + "bun-types": "^1.2.18", 13 17 "eslint": "^9.18.0", 14 18 "eslint-plugin-svelte": "^3.0.0", 15 19 "globals": "^16.0.0", ··· 25 29 "packages": { 26 30 "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], 27 31 32 + "@atcute/jetstream": ["@atcute/jetstream@1.0.2", "", { "dependencies": { "@atcute/lexicons": "^1.0.2", "@badrap/valita": "^0.4.2", "@mary-ext/event-iterator": "^1.0.0", "@mary-ext/simple-event-emitter": "^1.0.0", "partysocket": "^1.1.4", "type-fest": "^4.41.0", "yocto-queue": "^1.2.1" } }, "sha512-ZtdNNxl4zq9cgUpXSL9F+AsXUZt0Zuyj0V7974D7LxdMxfTItPnMZ9dRG8GoFkkGz3+pszdsG888Ix8C0F2+mA=="], 33 + 34 + "@atcute/lexicons": ["@atcute/lexicons@1.1.0", "", { "dependencies": { "esm-env": "^1.2.2" } }, "sha512-LFqwnria78xLYb62Ri/+WwQpUTgZp2DuyolNGIIOV1dpiKhFFFh//nscHMA6IExFLQRqWDs3tTjy7zv0h3sf1Q=="], 35 + 36 + "@badrap/valita": ["@badrap/valita@0.4.5", "", {}, "sha512-4QwGbuhh/JesHRQj79mO/l37PvJj4l/tlAu7+S1n4h47qwaNpZ0WDvIwUGLYUsdi9uQ5UPpiG9wb1Wm3XUFBUQ=="], 37 + 28 38 "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.6", "", { "os": "aix", "cpu": "ppc64" }, "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw=="], 29 39 30 40 "@esbuild/android-arm": ["@esbuild/android-arm@0.25.6", "", { "os": "android", "cpu": "arm" }, "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg=="], ··· 115 125 116 126 "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="], 117 127 128 + "@mary-ext/event-iterator": ["@mary-ext/event-iterator@1.0.0", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-l6gCPsWJ8aRCe/s7/oCmero70kDHgIK5m4uJvYgwEYTqVxoBOIXbKr5tnkLqUHEg6mNduB4IWvms3h70Hp9ADQ=="], 129 + 130 + "@mary-ext/simple-event-emitter": ["@mary-ext/simple-event-emitter@1.0.0", "", {}, "sha512-meA/zJZKIN1RVBNEYIbjufkUrW7/tRjHH60FjolpG1ixJKo76TB208qefQLNdOVDA7uIG0CGEDuhmMirtHKLAg=="], 131 + 118 132 "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], 119 133 120 134 "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], ··· 209 223 210 224 "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], 211 225 226 + "@types/node": ["@types/node@24.0.14", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw=="], 227 + 228 + "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], 229 + 212 230 "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.37.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.37.0", "@typescript-eslint/type-utils": "8.37.0", "@typescript-eslint/utils": "8.37.0", "@typescript-eslint/visitor-keys": "8.37.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.37.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA=="], 213 231 214 232 "@typescript-eslint/parser": ["@typescript-eslint/parser@8.37.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.37.0", "@typescript-eslint/types": "8.37.0", "@typescript-eslint/typescript-estree": "8.37.0", "@typescript-eslint/visitor-keys": "8.37.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA=="], ··· 249 267 250 268 "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], 251 269 270 + "bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="], 271 + 252 272 "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], 253 273 254 274 "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], ··· 271 291 272 292 "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], 273 293 294 + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], 295 + 274 296 "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], 275 297 276 298 "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], ··· 308 330 "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], 309 331 310 332 "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], 333 + 334 + "event-target-polyfill": ["event-target-polyfill@0.0.4", "", {}, "sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ=="], 311 335 312 336 "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], 313 337 ··· 438 462 "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], 439 463 440 464 "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], 465 + 466 + "partysocket": ["partysocket@1.1.4", "", { "dependencies": { "event-target-polyfill": "^0.0.4" } }, "sha512-jXP7PFj2h5/v4UjDS8P7MZy6NJUQ7sspiFyxL4uc/+oKOL+KdtXzHnTV8INPGxBrLTXgalyG3kd12Qm7WrYc3A=="], 441 467 442 468 "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], 443 469 ··· 513 539 514 540 "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], 515 541 542 + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], 543 + 516 544 "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], 517 545 518 546 "typescript-eslint": ["typescript-eslint@8.37.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.37.0", "@typescript-eslint/parser": "8.37.0", "@typescript-eslint/typescript-estree": "8.37.0", "@typescript-eslint/utils": "8.37.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA=="], 547 + 548 + "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], 519 549 520 550 "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], 521 551 ··· 533 563 534 564 "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], 535 565 536 - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], 566 + "yocto-queue": ["yocto-queue@1.2.1", "", {}, "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg=="], 537 567 538 568 "zimmerframe": ["zimmerframe@1.1.2", "", {}, "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w=="], 539 569 ··· 562 592 "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], 563 593 564 594 "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], 595 + 596 + "p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], 565 597 566 598 "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="], 567 599
+34 -30
package.json
··· 1 1 { 2 - "name": "nsid-tracker", 3 - "private": true, 4 - "version": "0.0.1", 5 - "type": "module", 6 - "scripts": { 7 - "dev": "vite dev", 8 - "build": "vite build", 9 - "preview": "vite preview", 10 - "prepare": "svelte-kit sync || echo ''", 11 - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 12 - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 13 - "lint": "eslint ." 14 - }, 15 - "devDependencies": { 16 - "@eslint/compat": "^1.2.5", 17 - "@eslint/js": "^9.18.0", 18 - "@sveltejs/adapter-auto": "^6.0.0", 19 - "@sveltejs/kit": "^2.22.0", 20 - "@sveltejs/vite-plugin-svelte": "^6.0.0", 21 - "@tailwindcss/vite": "^4.0.0", 22 - "eslint": "^9.18.0", 23 - "eslint-plugin-svelte": "^3.0.0", 24 - "globals": "^16.0.0", 25 - "svelte": "^5.0.0", 26 - "svelte-check": "^4.0.0", 27 - "tailwindcss": "^4.0.0", 28 - "typescript": "^5.0.0", 29 - "typescript-eslint": "^8.20.0", 30 - "vite": "^7.0.4" 31 - } 2 + "name": "nsid-tracker", 3 + "private": true, 4 + "version": "0.0.1", 5 + "type": "module", 6 + "scripts": { 7 + "dev": "vite dev", 8 + "build": "vite build", 9 + "preview": "vite preview", 10 + "prepare": "svelte-kit sync || echo ''", 11 + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 12 + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 13 + "lint": "eslint ." 14 + }, 15 + "devDependencies": { 16 + "@eslint/compat": "^1.2.5", 17 + "@eslint/js": "^9.18.0", 18 + "@sveltejs/adapter-auto": "^6.0.0", 19 + "@sveltejs/kit": "^2.22.0", 20 + "@sveltejs/vite-plugin-svelte": "^6.0.0", 21 + "@tailwindcss/vite": "^4.0.0", 22 + "bun-types": "^1.2.18", 23 + "eslint": "^9.18.0", 24 + "eslint-plugin-svelte": "^3.0.0", 25 + "globals": "^16.0.0", 26 + "svelte": "^5.0.0", 27 + "svelte-check": "^4.0.0", 28 + "tailwindcss": "^4.0.0", 29 + "typescript": "^5.0.0", 30 + "typescript-eslint": "^8.20.0", 31 + "vite": "^7.0.4" 32 + }, 33 + "dependencies": { 34 + "@atcute/jetstream": "^1.0.2" 35 + } 32 36 }
+4
src/hooks.server.ts
··· 1 + import { startTracking } from "$lib/jetstream.js"; 2 + 3 + // Start tracking when the server starts 4 + startTracking().catch(console.error);
+113
src/lib/db.ts
··· 1 + import { Database } from "bun:sqlite"; 2 + 3 + export interface EventRecord { 4 + nsid: string; 5 + timestamp: number; 6 + count: number; 7 + deleted_count: number; 8 + } 9 + 10 + class EventTracker { 11 + private db: Database; 12 + private insertNsidQuery; 13 + private insertEventQuery; 14 + private updateCountQuery; 15 + private getNsidCountQuery; 16 + private getEventCountQuery; 17 + 18 + constructor() { 19 + this.db = new Database("events.sqlite"); 20 + // init db 21 + this.db.run("PRAGMA journal_mode = WAL;"); 22 + // events 23 + this.db.run(` 24 + CREATE TABLE IF NOT EXISTS events ( 25 + nsid_idx INTEGER NOT NULL, 26 + timestamp INTEGER NOT NULL, 27 + deleted INTEGER NOT NULL, 28 + PRIMARY KEY (nsid_idx, timestamp) 29 + ) 30 + `); 31 + // aggregated counts 32 + this.db.run(` 33 + CREATE TABLE IF NOT EXISTS nsid_counts ( 34 + nsid_idx INTEGER PRIMARY KEY, 35 + count INTEGER NOT NULL, 36 + deleted_count INTEGER NOT NULL, 37 + last_updated INTEGER NOT NULL 38 + ) 39 + `); 40 + this.db.run(` 41 + CREATE TABLE IF NOT EXISTS nsid_types ( 42 + id INTEGER PRIMARY KEY AUTOINCREMENT, 43 + nsid TEXT UNIQUE NOT NULL 44 + ); 45 + `); 46 + // compile queries 47 + this.insertNsidQuery = this.db.query( 48 + "INSERT OR IGNORE INTO nsid_types (nsid) VALUES (?)", 49 + ); 50 + this.insertEventQuery = this.db.query(` 51 + INSERT OR IGNORE INTO events (nsid_idx, timestamp, deleted) 52 + VALUES ( 53 + (SELECT id FROM nsid_types WHERE nsid = ?), 54 + ?, 55 + ? 56 + ) 57 + `); 58 + this.updateCountQuery = this.db.query(` 59 + INSERT INTO nsid_counts (nsid_idx, count, deleted_count, last_updated) 60 + VALUES ( 61 + (SELECT id FROM nsid_types WHERE nsid = $nsid), 62 + 1 - $deleted, 63 + $deleted, 64 + $timestamp 65 + ) 66 + ON CONFLICT(nsid_idx) DO UPDATE SET 67 + count = count + (1 - $deleted), 68 + deleted_count = deleted_count + $deleted, 69 + last_updated = $timestamp 70 + `); 71 + this.getNsidCountQuery = this.db.query(` 72 + SELECT 73 + (SELECT nsid FROM nsid_types WHERE id = nsid_idx) as nsid, 74 + count, 75 + deleted_count, 76 + last_updated as timestamp 77 + FROM nsid_counts 78 + ORDER BY count DESC 79 + `); 80 + this.getEventCountQuery = this.db.query( 81 + `SELECT COUNT(*) as count FROM events`, 82 + ); 83 + } 84 + 85 + addEvent = (nsid: string, timestamp: number, deleted: boolean) => { 86 + const tx = this.db.transaction(() => { 87 + this.insertNsidQuery.run(nsid); 88 + this.insertEventQuery.run(nsid, timestamp, deleted); 89 + this.updateCountQuery.run({ 90 + $nsid: nsid, 91 + $deleted: deleted, 92 + $timestamp: timestamp, 93 + }); 94 + }); 95 + 96 + tx(); 97 + }; 98 + 99 + getNsidCounts = (): EventRecord[] => { 100 + return this.getNsidCountQuery.all() as EventRecord[]; 101 + }; 102 + 103 + getEventCount = (): number => { 104 + const result = this.getEventCountQuery.get() as { count: number }; 105 + return result.count; 106 + }; 107 + 108 + close = () => { 109 + this.db.close(); 110 + }; 111 + } 112 + 113 + export const eventTracker = new EventTracker();
-1
src/lib/index.ts
··· 1 - // place files you want to import through the `$lib` alias in this folder.
+21
src/lib/jetstream.ts
··· 1 + import { JetstreamSubscription } from "@atcute/jetstream"; 2 + import { eventTracker } from "./db.js"; 3 + 4 + let subscription: JetstreamSubscription | null = null; 5 + 6 + export const startTracking = async () => { 7 + subscription = new JetstreamSubscription({ 8 + url: "wss://jetstream2.us-east.bsky.network", 9 + // Don't filter by collections - we want to track all of them 10 + }); 11 + 12 + for await (const event of subscription) { 13 + if (event.kind !== "commit") { 14 + continue; 15 + } 16 + 17 + const { operation, collection } = event.commit; 18 + 19 + eventTracker.addEvent(collection, event.time_us, operation === "delete"); 20 + } 21 + };
+3 -3
src/routes/+layout.svelte
··· 1 1 <script lang="ts"> 2 - import '../app.css'; 3 - 4 - let { children } = $props(); 2 + import "../app.css"; 3 + 4 + let { children } = $props(); 5 5 </script> 6 6 7 7 {@render children()}
+150 -2
src/routes/+page.svelte
··· 1 - <h1>Welcome to SvelteKit</h1> 2 - <p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p> 1 + <script lang="ts"> 2 + import { onMount } from "svelte"; 3 + import type { EventRecord } from "$lib/db.js"; 4 + 5 + let events: EventRecord[] = []; 6 + let totalEvents = 0; 7 + let isLoading = true; 8 + let error: string | null = null; 9 + 10 + const loadData = async () => { 11 + try { 12 + isLoading = true; 13 + error = null; 14 + 15 + const response = await fetch("/api/events"); 16 + if (!response.ok) { 17 + throw new Error(`HTTP error! status: ${response.status}`); 18 + } 19 + 20 + const data = await response.json(); 21 + events = data.events; 22 + totalEvents = data.totalEvents; 23 + } catch (err) { 24 + error = 25 + err instanceof Error 26 + ? err.message 27 + : "an unknown error occurred"; 28 + console.error("Error loading data:", err); 29 + } finally { 30 + isLoading = false; 31 + } 32 + }; 33 + 34 + onMount(() => { 35 + loadData(); 36 + }); 37 + 38 + const formatNumber = (num: number): string => { 39 + return num.toLocaleString(); 40 + }; 41 + 42 + const formatTimestamp = (timestamp: number): string => { 43 + return new Date(timestamp / 1000).toLocaleString(); 44 + }; 45 + </script> 46 + 47 + <svelte:head> 48 + <title>bluesky event tracker</title> 49 + <meta name="description" content="tracks bluesky events by collection" /> 50 + </svelte:head> 51 + 52 + <div class="max-w-[50vw] mx-auto p-2"> 53 + <header class="text-center mb-8"> 54 + <h1 class="text-4xl font-bold mb-2 text-gray-900"> 55 + 🦋 bluesky event tracker 56 + </h1> 57 + <p class="text-gray-600"> 58 + real-time tracking of bluesky events by collection from the 59 + jetstream 60 + </p> 61 + </header> 62 + 63 + <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8"> 64 + <div 65 + class="bg-gradient-to-r from-blue-50 to-blue-100 p-6 rounded-lg border border-blue-200" 66 + > 67 + <h3 class="text-sm font-medium text-blue-700 mb-2">total events</h3> 68 + <p class="text-3xl font-bold text-blue-900"> 69 + {formatNumber(totalEvents)} 70 + </p> 71 + </div> 72 + <div 73 + class="bg-gradient-to-r from-green-50 to-green-100 p-6 rounded-lg border border-green-200" 74 + > 75 + <h3 class="text-sm font-medium text-green-700 mb-2"> 76 + unique collections 77 + </h3> 78 + <p class="text-3xl font-bold text-green-900"> 79 + {formatNumber(events.length)} 80 + </p> 81 + </div> 82 + </div> 83 + 84 + <div class="text-center mb-8"> 85 + <button 86 + on:click={loadData} 87 + disabled={isLoading} 88 + class="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-white px-6 py-3 rounded-lg font-medium transition-colors" 89 + > 90 + {isLoading ? "loading..." : "refresh"} 91 + </button> 92 + </div> 93 + 94 + {#if error} 95 + <div 96 + class="bg-red-100 border border-red-300 text-red-700 px-4 py-3 rounded-lg mb-6" 97 + > 98 + <p>Error: {error}</p> 99 + </div> 100 + {/if} 101 + 102 + {#if isLoading} 103 + <div class="text-center py-12"> 104 + <div 105 + class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" 106 + ></div> 107 + <p class="mt-4 text-gray-600">loading events...</p> 108 + </div> 109 + {:else if events.length > 0} 110 + <div class="mb-8"> 111 + <h2 class="text-2xl font-bold mb-6 text-gray-900"> 112 + events by collection 113 + </h2> 114 + <div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4"> 115 + {#each events as event, index (event.nsid)} 116 + <div 117 + class="bg-white border border-gray-200 rounded-lg p-6 hover:shadow-lg transition-shadow duration-200 hover:-translate-y-1 transform" 118 + > 119 + <div class="flex justify-between items-start mb-3"> 120 + <div 121 + class="text-sm font-bold text-blue-600 bg-blue-100 px-3 py-1 rounded-full" 122 + > 123 + #{index + 1} 124 + </div> 125 + </div> 126 + <div 127 + class="font-mono text-sm text-gray-700 mb-2 break-all leading-relaxed" 128 + > 129 + {event.nsid} 130 + </div> 131 + <div class="text-lg font-bold text-green-600"> 132 + {formatNumber(event.count)} created 133 + </div> 134 + <div class="text-lg font-bold text-red-600 mb-3"> 135 + {formatNumber(event.deleted_count)} deleted 136 + </div> 137 + <div class="text-xs text-gray-500"> 138 + last: {formatTimestamp(event.timestamp)} 139 + </div> 140 + </div> 141 + {/each} 142 + </div> 143 + </div> 144 + {:else} 145 + <div class="text-center py-12 bg-gray-50 rounded-lg"> 146 + <div class="text-gray-400 text-4xl mb-4">📊</div> 147 + <p class="text-gray-600">no events tracked yet.</p> 148 + </div> 149 + {/if} 150 + </div>
+17
src/routes/api/events/+server.ts
··· 1 + import { json } from "@sveltejs/kit"; 2 + import { eventTracker } from "$lib/db.js"; 3 + 4 + export const GET = async () => { 5 + try { 6 + const events = eventTracker.getNsidCounts(); 7 + const totalEvents = eventTracker.getEventCount(); 8 + 9 + return json({ 10 + events, 11 + totalEvents, 12 + }); 13 + } catch (error) { 14 + console.error("error fetching events:", error); 15 + return json({ error: "failed to fetch events" }, { status: 500 }); 16 + } 17 + };
+18 -17
tsconfig.json
··· 1 1 { 2 - "extends": "./.svelte-kit/tsconfig.json", 3 - "compilerOptions": { 4 - "allowJs": true, 5 - "checkJs": true, 6 - "esModuleInterop": true, 7 - "forceConsistentCasingInFileNames": true, 8 - "resolveJsonModule": true, 9 - "skipLibCheck": true, 10 - "sourceMap": true, 11 - "strict": true, 12 - "moduleResolution": "bundler" 13 - } 14 - // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 15 - // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 16 - // 17 - // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 - // from the referenced tsconfig.json - TypeScript does not merge them in 2 + "extends": "./.svelte-kit/tsconfig.json", 3 + "compilerOptions": { 4 + "allowJs": true, 5 + "checkJs": true, 6 + "esModuleInterop": true, 7 + "forceConsistentCasingInFileNames": true, 8 + "resolveJsonModule": true, 9 + "skipLibCheck": true, 10 + "sourceMap": true, 11 + "strict": true, 12 + "moduleResolution": "bundler", 13 + "types": ["bun-types"] 14 + } 15 + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 16 + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 17 + // 18 + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 19 + // from the referenced tsconfig.json - TypeScript does not merge them in 19 20 }