···3636 last_seen_at INTEGER NOT NULL
3737 );
3838 CREATE INDEX IF NOT EXISTS idx_browser_session_did ON browser_session(did);
3939+4040+ CREATE TABLE IF NOT EXISTS cached_feed (
4141+ did TEXT NOT NULL,
4242+ rkey TEXT NOT NULL,
4343+ url TEXT NOT NULL,
4444+ site_url TEXT,
4545+ title TEXT,
4646+ created_at TEXT NOT NULL,
4747+ PRIMARY KEY (did, rkey)
4848+ );
4949+5050+ CREATE TABLE IF NOT EXISTS cached_save (
5151+ did TEXT NOT NULL,
5252+ rkey TEXT NOT NULL,
5353+ url TEXT NOT NULL,
5454+ title TEXT,
5555+ created_at TEXT NOT NULL,
5656+ read_at TEXT,
5757+ PRIMARY KEY (did, rkey)
5858+ );
5959+6060+ CREATE TABLE IF NOT EXISTS cache_meta (
6161+ did TEXT NOT NULL,
6262+ collection TEXT NOT NULL,
6363+ last_synced_at INTEGER NOT NULL,
6464+ PRIMARY KEY (did, collection)
6565+ );
3966 `);
4067 instance = d;
4168 migrateFileStores(d);
···84111 }
85112 }
86113}
114114+
+31-4
src/server/index.ts
···1212import { buildOAuth } from "./oauth.js";
1313import { AtprotoRepo } from "./atproto.js";
1414import { Syncer } from "./sync.js";
1515+import { RecordCache } from "./record-cache.js";
1616+import { JetstreamListener } from "./jetstream.js";
1517import { authRoutes } from "./routes-auth.js";
1618import { apiRoutes } from "./routes-api.js";
1719import { deviceRoutes } from "./routes-device.js";
···2022const bodies = new BodyCache();
2123const deviceTokens = new DeviceTokenStore(config.dataDir);
2224const oauth = await buildOAuth();
2525+const cache = new RecordCache();
23262427const app = new Hono();
2528app.use(logger());
···3538}
36393740app.route("/auth", authRoutes(oauth, deviceTokens));
3838-app.route("/api", apiRoutes(oauth, mf));
3939-app.route("/device", deviceRoutes(oauth, mf, bodies, deviceTokens));
4141+app.route("/api", apiRoutes(oauth, mf, cache));
4242+app.route("/device", deviceRoutes(oauth, mf, bodies, deviceTokens, cache));
40434144const packageRoot = resolve(import.meta.dirname, "../..");
4245const publicDir = resolve(packageRoot, "dist/public");
···5053 return c.html(indexHtml);
5154});
52555656+// Populate cache on startup for all stored DIDs.
5757+(async () => {
5858+ for (const did of oauth.listDids()) {
5959+ const session = await oauth.getSessionForDid(did);
6060+ if (!session) continue;
6161+ try {
6262+ await cache.syncAll(new AtprotoRepo(session));
6363+ console.log(`cache: populated for ${did}`);
6464+ } catch (e) {
6565+ console.error(`cache: startup sync failed for ${did}:`, e);
6666+ }
6767+ }
6868+})();
6969+7070+// Jetstream listener for real-time cache updates.
7171+const jetstream = new JetstreamListener(cache, () => oauth.listDids());
7272+jetstream.start();
7373+5374// Background sync every 5 minutes for every stored DID.
5475setInterval(
5576 async () => {
5677 for (const did of oauth.listDids()) {
5757- if (!minifluxAllowed(did)) continue;
5878 const session = await oauth.getSessionForDid(did);
5979 if (!session) continue;
6060- const syncer = new Syncer(config.dataDir, new AtprotoRepo(session), mf);
8080+ const repo = new AtprotoRepo(session);
8181+ try {
8282+ await cache.syncAll(repo);
8383+ } catch (e) {
8484+ console.error(`cache: periodic sync failed for ${did}:`, e);
8585+ }
8686+ if (!minifluxAllowed(did)) continue;
8787+ const syncer = new Syncer(config.dataDir, repo, mf);
6188 try {
6289 const res = await syncer.run();
6390 if (res.added || res.removed) {