diff --git a/packages/devtools/src/__tests__/ngrx-collector.test.ts b/packages/devtools/src/__tests__/ngrx-collector.test.ts index 8f7ef290..5947b55f 100644 --- a/packages/devtools/src/__tests__/ngrx-collector.test.ts +++ b/packages/devtools/src/__tests__/ngrx-collector.test.ts @@ -1175,6 +1175,40 @@ describe('ngrx collector with Store DevTools', () => { } }); + it('resends the held log when the server forgot the page', async () => { + const { store, ng } = setupDevtools(); + const pages: NgrxPages = new Map(); + const my = { + rpc: { + call: async (name: string, report: unknown) => { + if (name === 'forget-ngrx-page') return void pages.delete(report as string); + if (name !== 'push-ngrx-state') return undefined; + return { seq: mergeNgrxReport(pages, report as NgrxPageReport, []) }; + }, + register: () => {}, + }, + }; + const overlay = attachNgrx(my, 'p1', () => ng as any); + const seqs = () => ngrxStateOf(pages).pages[0]?.log.map((e) => e.seq); + try { + await overlay.push(); + store.dispatch({ type: 'inc' }); + store.dispatch({ type: 'inc' }); + await overlay.push(); + expect(seqs()).toEqual([1, 2]); + overlay.leave(); + await Promise.resolve(); + expect(pages.size).toBe(0); + store.dispatch({ type: 'inc' }); + await overlay.push(); + await overlay.push(); + expect(seqs()).toEqual([1, 2, 3]); + expect(ngrxStateOf(pages).pages[0].dropped).toBe(0); + } finally { + overlay.stop(); + } + }); + it('does not call a restore to the newest action paused', () => { const { store, collector } = setupDevtools(); store.dispatch({ type: 'inc' }); diff --git a/packages/devtools/src/ngrx-overlay.ts b/packages/devtools/src/ngrx-overlay.ts index 273e2305..0c099f54 100644 --- a/packages/devtools/src/ngrx-overlay.ts +++ b/packages/devtools/src/ngrx-overlay.ts @@ -80,12 +80,19 @@ export function attachNgrx( stores, classic, log, + since: sentSeq, ...(lost.updates.length ? { unrestorable: lost.updates } : {}), }; const answer = (await my.rpc.call('push-ngrx-state', report)) as { seq?: unknown } | undefined; + const previous = sentSeq; sentSeq = typeof answer?.seq === 'number' ? answer.seq : collector.lastSeq(); sentLost = lost.last; + // A lower seq means the server lost this page: resend the held log. + if (sentSeq < previous) { + sentLost = 0; + schedule(); + } } catch { return; } finally { diff --git a/packages/devtools/src/ngrx-shared.ts b/packages/devtools/src/ngrx-shared.ts index b80c6e38..8ee233e1 100644 --- a/packages/devtools/src/ngrx-shared.ts +++ b/packages/devtools/src/ngrx-shared.ts @@ -118,6 +118,8 @@ export interface NgrxPageReport { stores: NgrxSignalStoreInfo[]; classic: NgrxClassicStoreInfo | null; log: NgrxLogEntry[]; + /** The newest seq the server confirmed before this report; `log` holds only later entries. */ + since?: number; /** Entries sent earlier that Store DevTools can no longer restore. */ unrestorable?: NgrxUnrestorableUpdate[]; } diff --git a/packages/devtools/src/rpc/ngrx-tools.ts b/packages/devtools/src/rpc/ngrx-tools.ts index e1bb8ac5..cc9f697d 100644 --- a/packages/devtools/src/rpc/ngrx-tools.ts +++ b/packages/devtools/src/rpc/ngrx-tools.ts @@ -90,9 +90,14 @@ export function mergeNgrxReport( maxLog = MAX_LOG, ): number { const previous = pages.get(report.pageId); - const keep = previous && previous.session === report.session ? previous.log : []; + const known = previous?.session === report.session; + const keep = known ? previous.log : []; const lastSeq = keep.at(-1)?.seq ?? 0; - const fresh = report.log.filter((entry: NgrxLogEntry) => entry.seq > lastSeq); + // The page only sends entries after `since`; if this server forgot the page + // (forget-ngrx-page, expiry, restart), drop the partial log and answer 0 so + // the page resends everything it still holds. + const resync = !known && typeof report.since === 'number' && report.since > 0; + const fresh = resync ? [] : report.log.filter((entry: NgrxLogEntry) => entry.seq > lastSeq); const lost = new Map(); for (const update of Array.isArray(report.unrestorable) ? report.unrestorable : []) { if (typeof update?.seq === 'number' && UNRESTORABLE.has(update.reason)) {