11import { devtools , DevtoolsOptions , PersistOptions } from 'zustand/middleware' ;
22import { MapStore } from './mapStore' ;
3- import { MIN_DIFF_MS } from '@constants/document/temporal' ;
43import { ZundoOptions } from 'zundo' ;
54import { AssignmentsStore } from './assignmentsStore' ;
65import { CoiAssignmentsStore } from './coiAssignmentsStore' ;
@@ -35,9 +34,14 @@ export const devToolsConfig: DevtoolsOptions = {
3534 } ,
3635} ;
3736
38- // Shared diff function for all temporal stores — only fires when clientLastUpdated changes
39- // and enough time has passed since the last snapshot. Generic over the store type so the
40- // same function can drive both district and COI zundo configurations.
37+ // Shared diff function for all temporal stores — fires once per real edit (every set that
38+ // bumps clientLastUpdated AND replaces a tracked collection). Generic over the store type so
39+ // the same function can drive both district and COI zundo configurations.
40+ //
41+ // Every mutation path replaces tracked Maps/Sets wholesale, so reference equality across all
42+ // partialized keys is a reliable O(keys) "did anything actually change" check. Timestamp-only
43+ // bumps (comment/metadata edits, save syncs) keep identical refs and are skipped — otherwise
44+ // they'd create dead undo steps that appear to do nothing.
4145interface TemporalDiffSnapshot {
4246 clientLastUpdated ?: string ;
4347 pendingShatterUndoState ?: AssignmentsStore [ 'pendingShatterUndoState' ] ;
@@ -51,16 +55,18 @@ export const temporalDiff = <T extends TemporalDiffSnapshot>(
5155 if ( ! past . clientLastUpdated || ! curr . clientLastUpdated ) return null ;
5256 // If the client timestamp is the same, don't store
5357 if ( past . clientLastUpdated === curr . clientLastUpdated ) return null ;
54- // If not yet ingested, don't store
55- if ( past . clientLastUpdated === '' || curr . clientLastUpdated === '' ) return null ;
56- // If the difference is less than the minimum diff time, don't store
57- if (
58- new Date ( curr . clientLastUpdated ) . getTime ( ) - new Date ( past . clientLastUpdated ) . getTime ( ) <
59- MIN_DIFF_MS
60- )
61- return null ;
58+ // Timestamp-only bump: no tracked collection was replaced, so nothing to undo
59+ const contentChanged = ( Object . keys ( past ) as Array < keyof T > ) . some (
60+ key => key !== 'clientLastUpdated' && past [ key ] !== curr [ key ]
61+ ) ;
62+ if ( ! contentChanged ) return null ;
6263 if ( past . pendingShatterUndoState && ! curr . pendingShatterUndoState ) {
63- return cloneTemporalSnapshot ( past . pendingShatterUndoState ) as unknown as Partial < T > ;
64+ // pendingShatterUndoState: null so restoring this entry can't leave a stale
65+ // pending snapshot in the store (entries are applied as partial merges).
66+ return {
67+ ...cloneTemporalSnapshot ( past . pendingShatterUndoState ) ,
68+ pendingShatterUndoState : null ,
69+ } as unknown as Partial < T > ;
6470 }
6571 return past ;
6672} ;
0 commit comments