mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 06:21:09 +02:00
fix(mana/web/sync): push fresh writes immediately via listener bridge
createUnifiedSync exported onPendingChange but nothing ever called it, so the Dexie hook in database.ts recorded _pendingChanges rows that the sync engine never heard about. Live writes only ever drained on the next page reload (via drainLeftoverPending). Observed live as fresh calendar/timeblocks writes piling up in _pendingChanges with zero POST traffic to sync.mana.how. Add a listener bridge: database.ts exposes setPendingChangeListener, trackPendingChange invokes it after each successful _pendingChanges insert, and sync.ts registers schedulePush (gated on a known channel) inside startAll. stopAll clears the listener so a torn-down sync engine can't get re-triggered by a stale callback. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6cbb1f64d0
commit
7fa3afcdc7
2 changed files with 41 additions and 11 deletions
|
|
@ -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<string, unknown>): 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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<str
|
|||
// Lazy apps: no pull until ensureAppSynced() is called
|
||||
}
|
||||
|
||||
// Wire fresh pending writes back to schedulePush. Without this, the
|
||||
// Dexie hook in database.ts records the row but the sync engine
|
||||
// never hears about it — pending accumulates indefinitely until the
|
||||
// next reload triggers drainLeftoverPending below. Register before
|
||||
// the drain so any change that lands during the drain itself is
|
||||
// also caught.
|
||||
setPendingChangeListener((appId) => {
|
||||
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<str
|
|||
}
|
||||
|
||||
function stopAll(): void {
|
||||
setPendingChangeListener(null);
|
||||
for (const [, channel] of channels) {
|
||||
if (channel.pushTimer) clearTimeout(channel.pushTimer);
|
||||
if (channel.pullTimer) clearInterval(channel.pullTimer);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue