diff --git a/apps/mana/apps/web/src/lib/data/database.ts b/apps/mana/apps/web/src/lib/data/database.ts index ea860ab3a..c8cb50d0b 100644 --- a/apps/mana/apps/web/src/lib/data/database.ts +++ b/apps/mana/apps/web/src/lib/data/database.ts @@ -346,20 +346,39 @@ const pendingChangesTable = db.table('_pendingChanges'); * `_pendingChanges` from there hits `NotFoundError: object store not in * scope`. We defer the write to a microtask so it runs in a fresh * transaction after the user's commit lands. + * + * After the row lands, `pendingChangeListener` (set by sync.ts at startup) + * is invoked with the change's appId so the unified sync engine can push + * immediately. Without that listener, pending rows would only ever drain + * on the next page reload via drainLeftoverPending. */ +let pendingChangeListener: ((appId: string) => void) | null = null; + +export function setPendingChangeListener(fn: ((appId: string) => void) | null): void { + pendingChangeListener = fn; +} + function trackPendingChange(table: string, change: Record): void { // setTimeout (not queueMicrotask) is required: Dexie binds the active // transaction to the current Zone via Promise scheduling, and a microtask // is still considered "inside" the transaction. setTimeout(0) breaks out // completely so the new add() spawns its own implicit transaction. setTimeout(() => { - pendingChangesTable.add(change).catch((err: unknown) => { - if (isQuotaError(err)) { - notifyQuotaExceeded({ table, op: 'pending-change', cleaned: 0, recovered: false }); - } else { - console.error('[mana-sync] failed to record pending change:', err); - } - }); + pendingChangesTable + .add(change) + .then(() => { + const appId = change.appId; + if (typeof appId === 'string' && pendingChangeListener) { + pendingChangeListener(appId); + } + }) + .catch((err: unknown) => { + if (isQuotaError(err)) { + notifyQuotaExceeded({ table, op: 'pending-change', cleaned: 0, recovered: false }); + } else { + console.error('[mana-sync] failed to record pending change:', err); + } + }); }, 0); } diff --git a/apps/mana/apps/web/src/lib/data/sync.ts b/apps/mana/apps/web/src/lib/data/sync.ts index 1cf435317..b6509c990 100644 --- a/apps/mana/apps/web/src/lib/data/sync.ts +++ b/apps/mana/apps/web/src/lib/data/sync.ts @@ -21,6 +21,7 @@ import { fromSyncName, beginApplyingTables, FIELD_TIMESTAMPS_KEY, + setPendingChangeListener, } from './database'; import { isQuotaError, cleanupTombstones, notifyQuotaExceeded } from './quota'; import { emitSyncTelemetry, categorizeSyncError } from './sync-telemetry'; @@ -551,11 +552,20 @@ export function createUnifiedSync(serverUrl: string, getToken: () => Promise { + if (channels.has(appId)) schedulePush(appId); + }); + // Drain leftover pending changes from previous sessions. Without this, - // pending writes survive across reloads but never push because - // schedulePush() only fires on fresh Dexie writes — channels start - // cold and the queue stays stuck until the user happens to mutate - // the same table again. + // pending writes that survived a reload would sit until the user + // happens to mutate the same table again — even with the listener + // above wired up, leftovers never trigger it. drainLeftoverPending(); // Listen for online/offline @@ -585,6 +595,7 @@ export function createUnifiedSync(serverUrl: string, getToken: () => Promise