+5
.gitignore
+5
.gitignore
+33
-1
bun.lock
+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
+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
+4
src/hooks.server.ts
+113
src/lib/db.ts
+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
src/lib/index.ts
···
1
-
// place files you want to import through the `$lib` alias in this folder.
+21
src/lib/jetstream.ts
+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
+3
-3
src/routes/+layout.svelte
+150
-2
src/routes/+page.svelte
+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
+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
+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
}