Skip to content
120 linesCodeBlameRaw

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. */
11export const KEEP = 20;
12
13/** A conversation as it was last read, and when. */
14export 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. */
17export 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
48const DB = "g1t-chat";
49const STORE = "conversations";
50
51function 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
72let db: Promise<IDBDatabase | null> | null = null;
73const database = () => (db ??= openDb());
74
75/** The conversations kept, shared by every page in this tab. */
76export 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.