Skip to content

Commit 75e1b6b

Browse files
committed
Persist LORO snapshots in redb, get rid of localstorage for commits, fix
chatroom
1 parent 99cb5c3 commit 75e1b6b

File tree

17 files changed

+222
-222
lines changed

17 files changed

+222
-222
lines changed

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@
6161
"Bash(export PATH=\"/opt/homebrew/bin:$PATH\")",
6262
"mcp__charlotte__charlotte_click_at",
6363
"Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:5173/)",
64-
"Bash(node -e ':*)"
64+
"Bash(node -e ':*)",
65+
"Bash(pnpm build:wasm)"
6566
]
6667
}
6768
}

browser/data-browser/public/wasm/client-db-worker.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,23 @@ async function handleMessage(msg) {
6767
await db.populate();
6868
return;
6969

70+
case 'exportAllResources':
71+
await ensureInit();
72+
return db.exportAllResources();
73+
74+
case 'importAllResources':
75+
await ensureInit();
76+
return db.importAllResources(msg.jsonArray);
77+
78+
case 'putLoroSnapshot':
79+
await ensureInit();
80+
db.putLoroSnapshot(msg.subject, msg.data);
81+
return;
82+
83+
case 'getLoroSnapshot':
84+
await ensureInit();
85+
return db.getLoroSnapshot(msg.subject);
86+
7087
default:
7188
throw new Error(`Unknown message type: ${msg.type}`);
7289
}

browser/data-browser/src/helpers/initClientDb.ts

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,13 @@ import { ClientDbWorker, type Store } from '@tomic/lib';
22

33
// Track the current worker so we can terminate it on HMR reload.
44
let currentWorker: ClientDbWorker | undefined;
5-
let offlineRestored = false;
65

76
/**
87
* Initialize the WASM ClientDb in a Web Worker and attach it to the Store.
98
* Uses OPFS for persistent storage — data survives page reloads.
109
* Falls back to in-memory if OPFS is unavailable.
1110
*/
1211
export function initClientDb(store: Store): void {
13-
// Restore offline-saved resources from localStorage FIRST (synchronous).
14-
// These contain Loro snapshots that the WASM DB can't store.
15-
// Only run once — HMR re-runs must not overwrite in-memory state.
16-
if (!offlineRestored) {
17-
offlineRestored = true;
18-
const count = store.restoreOfflineResources();
19-
20-
if (count > 0) {
21-
console.info(`[Offline] Restored ${count} resources from localStorage`);
22-
}
23-
}
24-
2512
if (typeof Worker === 'undefined') return;
2613

2714
// Terminate previous worker (important for Vite HMR — releases OPFS lock).
@@ -98,17 +85,6 @@ export function initClientDb(store: Store): void {
9885
console.info(
9986
`[ClientDb] WASM database ready, seeded ${properties.length} properties + ${otherPromises.length} resources`,
10087
);
101-
102-
// Debug: check if any loroUpdate was lost during seeding
103-
for (const key of Object.keys(localStorage)) {
104-
if (!key.startsWith('atomic.offline.')) continue;
105-
const subject = key.slice('atomic.offline.'.length);
106-
const resource = store.resources.get(subject);
107-
108-
if (resource && !resource.get('https://atomicdata.dev/properties/loroUpdate')) {
109-
console.error(`[ClientDb] loroUpdate LOST after seeding for ${subject.slice(0, 40)}`);
110-
}
111-
}
11288
});
11389

11490
// Tell the clientDb to wait for seeding before reporting as ready.

browser/data-browser/src/locales/de.po

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -653,7 +653,6 @@ msgstr "Nachrichtentext kopieren"
653653
#: src/chunks/TablePage/PropertyForm/NumberPropertyForm.tsx
654654
#: src/views/BookmarkPage/BookmarkPreview.tsx
655655
#: src/views/ChatRoomPage.tsx
656-
#: src/views/ChatRoomPage.tsx
657656
msgid "loading..."
658657
msgstr "Laden..."
659658

@@ -4059,12 +4058,10 @@ msgstr ""
40594058
msgid "Open tag page"
40604059
msgstr ""
40614060

4062-
#: src/views/ChatRoomPage.tsx
40634061
#: src/views/ChatRoomPage.tsx
40644062
msgid "No messages yet"
40654063
msgstr ""
40664064

4067-
#: src/views/ChatRoomPage.tsx
40684065
#: src/views/ChatRoomPage.tsx
40694066
msgid "Be the first to say something"
40704067
msgstr ""

browser/data-browser/src/locales/es.po

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -664,7 +664,6 @@ msgstr "Copiar texto del mensaje"
664664
#: src/chunks/TablePage/PropertyForm/NumberPropertyForm.tsx
665665
#: src/views/BookmarkPage/BookmarkPreview.tsx
666666
#: src/views/ChatRoomPage.tsx
667-
#: src/views/ChatRoomPage.tsx
668667
msgid "loading..."
669668
msgstr "cargando..."
670669

@@ -4045,12 +4044,10 @@ msgstr ""
40454044
msgid "Open tag page"
40464045
msgstr ""
40474046

4048-
#: src/views/ChatRoomPage.tsx
40494047
#: src/views/ChatRoomPage.tsx
40504048
msgid "No messages yet"
40514049
msgstr ""
40524050

4053-
#: src/views/ChatRoomPage.tsx
40544051
#: src/views/ChatRoomPage.tsx
40554052
msgid "Be the first to say something"
40564053
msgstr ""

browser/data-browser/src/locales/fr.po

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,6 @@ msgstr "Copier le texte du message"
677677
#: src/chunks/TablePage/PropertyForm/NumberPropertyForm.tsx
678678
#: src/views/BookmarkPage/BookmarkPreview.tsx
679679
#: src/views/ChatRoomPage.tsx
680-
#: src/views/ChatRoomPage.tsx
681680
msgid "loading..."
682681
msgstr "chargement..."
683682

@@ -4055,12 +4054,10 @@ msgstr ""
40554054
msgid "Open tag page"
40564055
msgstr ""
40574056

4058-
#: src/views/ChatRoomPage.tsx
40594057
#: src/views/ChatRoomPage.tsx
40604058
msgid "No messages yet"
40614059
msgstr ""
40624060

4063-
#: src/views/ChatRoomPage.tsx
40644061
#: src/views/ChatRoomPage.tsx
40654062
msgid "Be the first to say something"
40664063
msgstr ""

browser/lib/src/client-db.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,18 @@ export class ClientDbWorker {
163163
return result as number;
164164
}
165165

166+
/** Store a Loro CRDT snapshot for a resource subject. */
167+
async putLoroSnapshot(subject: string, data: Uint8Array): Promise<void> {
168+
await this.send({ type: 'putLoroSnapshot', subject, data });
169+
}
170+
171+
/** Retrieve a Loro CRDT snapshot for a resource subject. Returns null if not found. */
172+
async getLoroSnapshot(subject: string): Promise<Uint8Array | null> {
173+
const result = await this.send({ type: 'getLoroSnapshot', subject });
174+
175+
return (result as Uint8Array | null) ?? null;
176+
}
177+
166178
/** Whether the worker has been initialized and seeded. */
167179
get isReady(): boolean {
168180
return this.ready && this.seeded;

browser/lib/src/client-db.worker.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ export type WorkerRequest =
3333
| { id: number; type: 'allSubjects' }
3434
| { id: number; type: 'populate' }
3535
| { id: number; type: 'exportAllResources' }
36-
| { id: number; type: 'importAllResources'; jsonArray: string };
36+
| { id: number; type: 'importAllResources'; jsonArray: string }
37+
| { id: number; type: 'putLoroSnapshot'; subject: string; data: Uint8Array }
38+
| { id: number; type: 'getLoroSnapshot'; subject: string };
3739

3840
/** Message types sent from worker back to main thread */
3941
export type WorkerResponse =
@@ -122,6 +124,19 @@ async function handleMessage(msg: WorkerRequest): Promise<unknown> {
122124
return db!.importAllResources(msg.jsonArray);
123125
}
124126

127+
case 'putLoroSnapshot': {
128+
await ensureInit();
129+
db!.putLoroSnapshot(msg.subject, msg.data);
130+
131+
return;
132+
}
133+
134+
case 'getLoroSnapshot': {
135+
await ensureInit();
136+
137+
return db!.getLoroSnapshot(msg.subject);
138+
}
139+
125140
default:
126141
throw new Error(`Unknown message type: ${(msg as WorkerRequest).type}`);
127142
}

browser/lib/src/commit.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ const serializeMap = {
330330
};
331331

332332
/** Replaces the keys of a Commit object with their respective json-ad key */
333-
function commitToJsonADObject(
333+
export function commitToJsonADObject(
334334
commit: UnsignedCommit | Commit,
335335
origin?: string,
336336
): JSONADObject {

browser/lib/src/offline-persistence.test.ts

Lines changed: 2 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,23 @@
11
import { describe, it, beforeEach } from 'vitest';
22
import { Agent, Store, core, commits, JSCryptoProvider } from './index.js';
33

4-
/** Creates a fresh Store with the given agent, restoring any offline data. */
4+
/** Creates a fresh Store with the given agent. */
55
function freshStore(agent: Agent): Store {
66
const store = new Store({ serverUrl: 'https://example.com' });
77
store.setAgent(agent);
8-
store.restoreOfflineResources();
98

109
return store;
1110
}
1211

13-
describe('Offline persistence across reloads', () => {
12+
describe('Offline persistence', () => {
1413
let agent: Agent;
1514

1615
beforeEach(async () => {
17-
localStorage.clear();
1816
const keys = await Agent.generateKeyPair();
1917
const provider = new JSCryptoProvider(keys.privateKey);
2018
agent = new Agent(provider, `did:ad:agent:${keys.publicKey}`);
2119
});
2220

23-
it('resource subject stays the same after reload + re-edit', async ({
24-
expect,
25-
}) => {
26-
// Session 1: create drive + doc, save offline
27-
const store1 = freshStore(agent);
28-
const drive = await store1.createDrive('Test Drive');
29-
expect(drive.get(core.properties.write)).toContain(agent.subject);
30-
31-
const doc = await store1.newResource({
32-
parent: drive.subject,
33-
propVals: { [core.properties.name]: 'My Doc' },
34-
});
35-
await doc.save();
36-
const subject = doc.subject;
37-
expect(subject).toMatch(/^did:ad:/);
38-
39-
// Session 2: reload, edit, save
40-
const store2 = freshStore(agent);
41-
const doc2 = store2.getResourceLoading(subject);
42-
expect(doc2.get(core.properties.name)).toBe('My Doc');
43-
44-
await doc2.set(core.properties.name, 'Updated', false);
45-
await doc2.save();
46-
expect(doc2.subject).toBe(subject);
47-
48-
// Session 3: reload, verify latest edit persisted
49-
const store3 = freshStore(agent);
50-
const doc3 = store3.getResourceLoading(subject);
51-
expect(doc3.get(core.properties.name)).toBe('Updated');
52-
});
53-
54-
it('multiple edits across reloads all persist', async ({ expect }) => {
55-
const store1 = freshStore(agent);
56-
const res = await store1.newResource({
57-
noParent: true,
58-
propVals: { [core.properties.name]: 'v1' },
59-
});
60-
await res.save();
61-
const subject = res.subject;
62-
63-
for (const version of ['v2', 'v3', 'v4']) {
64-
const store = freshStore(agent);
65-
const r = store.getResourceLoading(subject);
66-
await r.set(core.properties.name, version, false);
67-
await r.save();
68-
expect(r.subject).toBe(subject);
69-
}
70-
71-
const storeFinal = freshStore(agent);
72-
const rFinal = storeFinal.getResourceLoading(subject);
73-
expect(rFinal.get(core.properties.name)).toBe('v4');
74-
});
75-
7621
it('offline save sets createdAt for sorting', async ({ expect }) => {
7722
const store = freshStore(agent);
7823
const drive = await store.createDrive('Timestamp Test');

0 commit comments

Comments
 (0)