Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 1 | /** |
| 2 | * Conversations already seen, kept in the browser so switching back to one | |
| 3 | * draws it at once (routes/workspace/chat/channel.tsx `clientLoader`), | |
| 4 | * while the server is asked again behind it. The latest ~20, newest kept; | |
| 5 | * in memory, and in IndexedDB when the browser allows it so a reload is | |
| 6 | * instant too. Every IndexedDB step may fail (private windows, full | |
| 7 | * disks); then only memory is used. | |
| 8 | */ | |
| 9 | ||
| 10 | /** How many conversations are kept. */ | |
| 11 | export const KEEP = 20; | |
| 12 | ||
| 13 | /** A conversation as it was last read, and when. */ | |
| 14 | export type Kept<T> = { value: T; at: number }; | |
| 15 | ||
| 16 | /** A small LRU: the most recently used last. Pure, so it is tested on its own. */ | |
| 17 | export class Recent<T> { | |
| 18 | private items = new Map<string, Kept<T>>(); | |
| 19 | private readonly keep: number; | |
| 20 | constructor(keep = KEEP) { | |
| 21 | this.keep = keep; | |
| 22 | } | |
| 23 | get(key: string): Kept<T> | undefined { | |
| 24 | const found = this.items.get(key); | |
| 25 | if (found) { | |
| 26 | this.items.delete(key); | |
| 27 | this.items.set(key, found); | |
| 28 | } | |
| 29 | return found; | |
| 30 | } | |
| 31 | has(key: string): boolean { | |
| 32 | return this.items.has(key); | |
| 33 | } | |
| 34 | set(key: string, value: T, at = Date.now()): void { | |
| 35 | this.items.delete(key); | |
| 36 | this.items.set(key, { value, at }); | |
| 37 | while (this.items.size > this.keep) { | |
| 38 | const oldest = this.items.keys().next().value; | |
| 39 | if (oldest === undefined) break; | |
| 40 | this.items.delete(oldest); | |
| 41 | } | |
| 42 | } | |
| 43 | keys(): string[] { | |
| 44 | return [...this.items.keys()]; | |
| 45 | } | |
| 46 | } | |
| 47 | ||
| 48 | const DB = "g1t-chat"; | |
| 49 | const STORE = "conversations"; | |
| 50 | ||
| 51 | function openDb(): Promise<IDBDatabase | null> { | |
| 52 | return new Promise((resolve) => { | |
| 53 | try { | |
| 54 | if (typeof indexedDB === "undefined") return resolve(null); | |
| 55 | const request = indexedDB.open(DB, 1); | |
| 56 | request.onupgradeneeded = () => { | |
| 57 | try { | |
| 58 | request.result.createObjectStore(STORE); | |
| 59 | } catch { | |
| 60 | // Already there. | |
| 61 | } | |
| 62 | }; | |
| 63 | request.onsuccess = () => resolve(request.result); | |
| 64 | request.onerror = () => resolve(null); | |
| 65 | request.onblocked = () => resolve(null); | |
| 66 | } catch { | |
| 67 | resolve(null); | |
| 68 | } | |
| 69 | }); | |
| 70 | } | |
| 71 | ||
| 72 | let db: Promise<IDBDatabase | null> | null = null; | |
| 73 | const database = () => (db ??= openDb()); | |
| 74 | ||
| 75 | /** The conversations kept, shared by every page in this tab. */ | |
| 76 | export function createConversationCache<T>() { | |
| 77 | const memory = new Recent<T>(); | |
| 78 | return { | |
| 79 | peek: (key: string) => memory.get(key)?.value, | |
| 80 | has: (key: string) => memory.has(key), | |
| 81 | set(key: string, value: T) { | |
| 82 | memory.set(key, value); | |
| 83 | void database().then((store) => { | |
| 84 | if (!store) return; | |
| 85 | try { | |
| 86 | const tx = store.transaction(STORE, "readwrite"); | |
| 87 | tx.objectStore(STORE).put({ value, at: Date.now() }, key); | |
| 88 | // Keep the store as small as memory: drop what memory let go. | |
| 89 | const kept = new Set(memory.keys()); | |
| 90 | const keys = tx.objectStore(STORE).getAllKeys(); | |
| 91 | keys.onsuccess = () => { | |
| 92 | for (const stored of keys.result) if (!kept.has(String(stored))) tx.objectStore(STORE).delete(stored); | |
| 93 | }; | |
| 94 | } catch { | |
| 95 | // Memory still has it. | |
| 96 | } | |
| 97 | }); | |
| 98 | }, | |
| 99 | /** From IndexedDB after a reload, when memory has nothing yet. */ | |
| 100 | async load(key: string): Promise<T | undefined> { | |
| 101 | const found = memory.get(key)?.value; | |
| 102 | if (found !== undefined) return found; | |
| 103 | const store = await database(); | |
| 104 | if (!store) return undefined; | |
| 105 | return new Promise((resolve) => { | |
| 106 | try { | |
| 107 | const request = store.transaction(STORE, "readonly").objectStore(STORE).get(key); | |
| 108 | request.onsuccess = () => { | |
| 109 | const kept = request.result as Kept<T> | undefined; | |
| 110 | if (kept) memory.set(key, kept.value, kept.at); | |
| 111 | resolve(kept?.value); | |
| 112 | }; | |
| 113 | request.onerror = () => resolve(undefined); | |
| 114 | } catch { | |
| 115 | resolve(undefined); | |
| 116 | } | |
| 117 | }); | |
| 118 | }, | |
| 119 | }; | |
| 120 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.