diff --git a/packages/devtools/src/__tests__/ngrx-collector.test.ts b/packages/devtools/src/__tests__/ngrx-collector.test.ts index 3ee51e91..baaaaae8 100644 --- a/packages/devtools/src/__tests__/ngrx-collector.test.ts +++ b/packages/devtools/src/__tests__/ngrx-collector.test.ts @@ -171,6 +171,21 @@ describe('ngrx collector', () => { expect(entry.durationMs).toBeUndefined(); }); + it('keeps a method change out of a patchState entry from earlier in the same tick', async () => { + const { store, collector } = setup(); + collector.collect(); + patchState(store, { saved: ['x'] }); + store.setQuery('rome'); + await Promise.resolve(); + expect(collector.logSince(0).map((e) => [e.type, e.diff])).toEqual([ + ['patchState', [{ path: 'saved[0]', op: 'add', after: 'x' }]], + ['setQuery', [{ path: 'query', op: 'change', before: '', after: 'rome' }]], + ]); + expect(collector.run({ type: 'restore', seq: 1 }).ok).toBe(true); + expect((store as any)[STATE_SOURCE].query()).toBe(''); + expect((store as any)[STATE_SOURCE].saved()).toEqual(['x']); + }); + it('measures a method call duration and updates rolling stats per method', () => { const { store, collector } = setup(); collector.collect(); diff --git a/packages/devtools/src/ngrx-collector.ts b/packages/devtools/src/ngrx-collector.ts index 8a552089..ee2dd307 100644 --- a/packages/devtools/src/ngrx-collector.ts +++ b/packages/devtools/src/ngrx-collector.ts @@ -353,6 +353,18 @@ export function createNgrxCollector( }); }; + /** + * Logs writes still waiting for the microtask above right away, so a method + * call or restore that follows in the same tick gets its own entry and its + * change does not also land in that `patchState` entry. + */ + const flushPending = (t: Tracked) => { + const before = t.pendingBefore; + if (!before) return; + t.pendingBefore = null; + finish(t, 'patchState', undefined, before); + }; + /** * Appends one `signal-store` entry per patchState call, picked up through * `watchState()`. Dedicated to that path (not reused through `finish()`) so @@ -463,6 +475,7 @@ export function createNgrxCollector( apply(target, thisArg, args) { info.calls++; const outer = t.depth === 0; + if (outer) flushPending(t); const before = outer ? snapshot(t) : null; const frame = { name, args, startedAt: now(), fired: 0 }; t.methodStack.push(frame); @@ -1249,6 +1262,7 @@ export function createNgrxCollector( const t = entry.tracked; if (!tracked.has(t.instance)) return { error: 'This store no longer exists on the page.' }; if (!t.writable) return { error: 'This store state is read-only.' }; + flushPending(t); const before = snapshot(t); const patch: Record = {}; for (const key of t.stateKeys) {