1e6fb78d58b5
build each public repo's Atlas after its sync, so the first visitor after a new save gets a warm page (v3.0.186)
- time
- 2026-10-01 20:40 UTC · 1 week ago
- machine
- Junes-MacBook-Pro.local
- agent
- claude-opus-5-5
- session
- atlas speed
- parent
- dc678fae81ff
↓ kepr-1e6fb78d.tar.gzatlas at this save
files changed6 files
package.json+1 −1src/bucket/sync.ts+3 −0src/main.ts+9 −1src/pages/atlas.ts+10 −0src/pages/layout.ts+4 −1tests/sync.test.ts+11 −0package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "kepr",3- "version": "3.0.185",3+ "version": "3.0.186",44 "private": true,55 "type": "module",66 "scripts": {src/bucket/sync.ts+3 −0
@@ -44,6 +44,8 @@4444 bucket: Bucket;4545 kepr: Database;4646 mirrorDir: string;47+ /** Called after each repo that synced without error. */48+ synced?: (repo: string) => void;4749 };4850 4951 let running: Promise<void> | null = null;@@ -113,6 +115,7 @@113115 }114116 }115117 recordSync(target.kepr, repo, Math.floor(Date.now() / 1000), error);118+ if (error === null) target.synced?.(repo);116119 }117120 }118121 src/main.ts+9 −1
@@ -3,6 +3,7 @@33 import { createSetupApp } from "./admin/setup.ts";44 import { openBucket } from "./bucket/reader.ts";55 import { startSyncLoop } from "./bucket/sync.ts";6+import { warmAtlas } from "./pages/atlas.ts";67 import { createApp } from "./server/app.ts";78 import { startBackupLoop } from "./server/backup.ts";89 import { CONFIG_FILE, type Config, ConfigError, loadConfig } from "./server/config.ts";@@ -20,7 +21,14 @@2021 const bucket = openBucket(config.bucket);2122 startBackupLoop(kepr, config);2223 startMailLoop(kepr, config);23- startSyncLoop({ bucket, kepr, mirrorDir: join(config.dataDir, "mirror") });24+ const mirrorDir = join(config.dataDir, "mirror");25+ // The first visitor after a new save would otherwise wait a second or two on a big repo.26+ startSyncLoop({27+ bucket,28+ kepr,29+ mirrorDir,30+ synced: (repo) => warmAtlas({ kepr, mirrorDir }, repo),31+ });2432 return createApp(config, kepr, bucket).fetch;2533 }2634 src/pages/atlas.ts+10 −0
@@ -3,6 +3,7 @@33 import {44 filesAt,55 fileTouches,6+ headId,67 objectBytes,78 type Save,89 type Touches,@@ -13,6 +14,8 @@1314 import {1415 formatDate,1516 formatSize,17+ openPublicRepo,18+ type PageContext,1619 type PublicRepo,1720 pathUrl,1821 plural,@@ -119,6 +122,13 @@119122 if (models.size >= MODELS) models.delete(models.keys().next().value as string);120123 models.set(key, model);121124 return model;125+}126+127+/** Builds a public repo's Atlas at its head ahead of the first visitor, as the sync calls it. */128+export function warmAtlas(ctx: Pick<PageContext, "kepr" | "mirrorDir">, name: string): void {129+ const repo = openPublicRepo(ctx, name);130+ const head = repo && headId(repo.mirror);131+ if (repo && head) atlasModel(repo, head);122132 }123133 124134 /**src/pages/layout.ts+4 −1
@@ -23,7 +23,10 @@2323 /**2424 * The gate of §20: a private repo and an unknown one both come back null, and so both 404 alike.2525 */26-export function openPublicRepo(ctx: PageContext, name: string): PublicRepo | null {26+export function openPublicRepo(27+ ctx: Pick<PageContext, "kepr" | "mirrorDir">,28+ name: string,29+): PublicRepo | null {2730 const settings = publicRepo(ctx.kepr, name);2831 return settings ? { settings, mirror: openMirror(ctx.mirrorDir, name) } : null;2932 }tests/sync.test.ts+11 −0
@@ -136,6 +136,17 @@136136 expect(saveByPrefix(mirror, "af96c81")?.message).toBe("made by Koh 3");137137 });138138 139+test("tells its target about each repo that synced cleanly, and not one that failed", async () => {140+ const fx = await makeKohBucket("told");141+ const t = await syncTarget(fx);142+ const synced: string[] = [];143+ await syncAll({ ...t, synced: (repo) => synced.push(repo) });144+ expect(synced).toEqual(["told"]);145+ await writeFile(join(fx.bucketDir, fx.repo, "format"), "koh-remote 2");146+ await syncAll({ ...t, synced: (repo) => synced.push(repo) });147+ expect(synced).toEqual(["told"]);148+});149+139150 test("a sync lands history in batches, so one cut short resumes where it stopped", async () => {140151 const fx = await makeKohBucket("batched");141152 const bucket = openBucket({ kind: "folder", path: fx.bucketDir });