···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.
···0
+21
src/lib/jetstream.ts
···000000000000000000000
···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<script lang="ts">
2- import '../app.css';
3-4- let { children } = $props();
5</script>
67{@render children()}
···1<script lang="ts">
2+ import "../app.css";
3+4+ let { children } = $props();
5</script>
67{@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>
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
···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
019}
···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+ "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
20}