Skip to content

Commit e495123

Browse files
committed
fix(shared-state): preserve local undo during initial reads
Track updates while the initial snapshot is pending. Restoring the original object after a local edit must not make a stale snapshot current again. Add an undo regression and retain initial synchronization after unchanged mutations. All required checks and the WebSocket/SSE service-discovery workflows pass.
1 parent 0179d3d commit e495123

2 files changed

Lines changed: 35 additions & 2 deletions

File tree

‎packages/devframe/src/client/rpc-shared-state.test.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,35 @@ describe('initial shared-state snapshots', () => {
9898
expect(setCalls).toHaveLength(1)
9999
})
100100

101+
it('keeps a local undo that restores the initial object while the snapshot is pending', async () => {
102+
const { host, snapshot, setCalls } = setup()
103+
const state = await host.get('k', { initialValue: { count: 0 } })
104+
const initialValue = state.value()
105+
state.mutate((draft) => {
106+
draft.count = 2
107+
})
108+
state.mutate(() => initialValue)
109+
expect(state.value()).toBe(initialValue)
110+
expect(setCalls.map(([, value]) => value)).toEqual([{ count: 2 }, { count: 0 }])
111+
112+
snapshot.resolve({ count: 1 })
113+
await snapshot.promise
114+
expect(state.value()).toBe(initialValue)
115+
expect(setCalls).toHaveLength(2)
116+
})
117+
118+
it('applies the initial snapshot after a mutation that leaves the value unchanged', async () => {
119+
const { host, snapshot, setCalls } = setup()
120+
const state = await host.get('k', { initialValue: { count: 0 } })
121+
state.mutate((draft) => {
122+
draft.count = 0
123+
})
124+
snapshot.resolve({ count: 1 })
125+
await snapshot.promise
126+
expect(state.value()).toEqual({ count: 1 })
127+
expect(setCalls).toEqual([])
128+
})
129+
101130
it('forwards a local write triggered by the initial snapshot', async () => {
102131
const { host, snapshot, setCalls } = setup()
103132
const state = await host.get('k', { initialValue: { count: 0 } })

‎packages/devframe/src/client/rpc-shared-state.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,17 +100,21 @@ export function createRpcSharedStateClientHost(rpc: DevframeRpcClient): RpcShare
100100
sharedState.set(key, state)
101101
for (const fn of keyAddedListeners)
102102
fn(key)
103-
const initialState = state.value()
103+
let stateUpdated = false
104+
const stopTrackingUpdates = state.on('updated', () => {
105+
stateUpdated = true
106+
})
104107
const initialSyncId = nanoid()
105108
rpc.call('devframe:rpc:server-state:get', key)
106109
.then((serverState) => {
107110
// A pending read must not replace a newer update or a deleted state.
108-
if (serverState !== undefined && sharedState.get(key) === state && state.value() === initialState)
111+
if (serverState !== undefined && sharedState.get(key) === state && !stateUpdated)
109112
state.mutate(() => mergeWithInitialValue(key, serverState), initialSyncId)
110113
})
111114
.catch((error) => {
112115
console.error('Error getting server state', error)
113116
})
117+
.finally(stopTrackingUpdates)
114118
stateDisposers.set(key, registerSharedState(key, state, initialSyncId))
115119
return state
116120
}

0 commit comments

Comments
 (0)