feat(todo): wire up browser sync with Go server

- Auth store starts/stops sync on login/logout
- LocalStore queues all existing records for initial sync (guest→auth transition)
- LocalCollection.queueAllForSync() creates pending inserts for all local records
- Skips initial queue if sync cursor exists (already synced before)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-27 11:50:43 +01:00
parent 8f56feb115
commit b85c32fcce
3 changed files with 62 additions and 1 deletions

View file

@ -6,6 +6,7 @@
import { browser } from '$app/environment';
import { initializeWebAuth, type UserData } from '@manacore/shared-auth';
import { apiClient } from '$lib/api/client';
import { todoStore } from '$lib/data/local-store';
// Default URLs for local development only
const DEV_AUTH_URL = 'http://localhost:3001';
@ -221,6 +222,9 @@ export const authStore = {
apiClient.setAccessToken(token);
}
// Start syncing local data to server
todoStore.startSync(() => authStore.getValidToken());
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
@ -264,6 +268,9 @@ export const authStore = {
* Sign out
*/
async signOut() {
// Stop syncing before clearing auth
todoStore.stopSync();
const authService = getAuthService();
if (!authService) {
user = null;

View file

@ -281,6 +281,30 @@ export class LocalCollection<T extends BaseRecord> {
return this._table.where('updatedAt').above(since).toArray();
}
/**
* Queue all existing local records as pending inserts.
* Used for initial sync after login ensures guest data gets pushed to server.
*/
async queueAllForSync(): Promise<number> {
const allRecords = await this._table.filter((r) => !r.deletedAt).toArray();
let count = 0;
await this._db.transaction('rw', [this._db._pendingChanges], async () => {
for (const record of allRecords) {
await this._db._pendingChanges.add({
collection: this.name,
recordId: record.id,
op: 'insert',
data: record as unknown as Record<string, unknown>,
createdAt: new Date().toISOString(),
});
count++;
}
});
return count;
}
// ─── Internal ───────────────────────────────────────────────
private async _trackChange(

View file

@ -147,7 +147,10 @@ export class LocalStore {
this._syncEngine.registerCollection(col);
}
this._syncEngine.start();
// Queue existing local data for initial sync (guest → authenticated transition)
this._queueInitialSync().then(() => {
this._syncEngine?.start();
});
}
/**
@ -203,6 +206,33 @@ export class LocalStore {
await this.db.reset();
}
/**
* Queue all existing local records for sync if this is the first sync.
* Handles the guestauthenticated transition: local data gets pushed to server.
*/
private async _queueInitialSync(): Promise<void> {
// Check if we've synced before — if any collection has a cursor, skip
for (const [name] of this._collections) {
const cursor = await this.db.getSyncCursor(name);
if (cursor !== '1970-01-01T00:00:00.000Z') {
// Already synced before — pending changes from writes will handle it
return;
}
}
// First sync: queue all local records as pending inserts
let total = 0;
for (const col of this._collections.values()) {
const count = await col.queueAllForSync();
total += count;
}
if (total > 0) {
// eslint-disable-next-line no-console
console.log(`[LocalStore] Queued ${total} local records for initial sync`);
}
}
/**
* Close the database connection.
*/