Skip to content

Commit 1da2f7b

Browse files
committed
fix(hub): don't mutate frozen dock entries in register().update
Projecting dock entries into the `devframe:docks` shared state runs them through Immer's `produce`, which deep-freezes the very objects the docks registry holds. The `register().update(patch)` handler then merged with an in-place `Object.assign`, throwing "Cannot assign to read only property" the first time a launcher (or any entry) was patched after the initial sync. Merge into a fresh object and swap it into the registry instead.
1 parent 56dc946 commit 1da2f7b

2 files changed

Lines changed: 33 additions & 1 deletion

File tree

packages/hub/src/node/__tests__/host-docks.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { DevframeViewLauncher } from '../../types/docks'
12
import type { DevframeHubContext } from '../context'
23
import { mkdtempSync } from 'node:fs'
34
import { tmpdir } from 'node:os'
@@ -305,4 +306,30 @@ describe('devframeDockHost ~builtin category', () => {
305306
expect(updatedLauncher?.digest).toBe('done')
306307
expect(updatedLauncher?.status).toBe('success')
307308
})
309+
310+
it('updates a registered entry whose stored view was frozen (Immer-projected shared state)', () => {
311+
const host = new DevframeDocksHost(createContext())
312+
const handle = host.register<DevframeViewLauncher>({
313+
type: 'launcher',
314+
id: 'app:build',
315+
title: 'Build',
316+
icon: 'ph:hammer-duotone',
317+
launcher: { title: 'Run build', status: 'idle', command: 'app:run-build' },
318+
})
319+
320+
// Projecting the entry into `devframe:docks` shared state runs it through
321+
// Immer's `produce`, which deep-freezes the very object the registry holds.
322+
Object.freeze(host.values()[0])
323+
324+
// The launch handler patches the launcher as the process boots; the update
325+
// must not throw "Cannot assign to read only property" on the frozen entry.
326+
expect(() => handle.update({
327+
launcher: { title: 'Run build', status: 'loading', command: 'app:run-build', terminalSessionId: 'build-session', digest: 'compiling…' },
328+
})).not.toThrow()
329+
330+
const entry = host.values()[0]
331+
const launcher = entry.type === 'launcher' ? entry.launcher : undefined
332+
expect(launcher?.status).toBe('loading')
333+
expect(launcher?.digest).toBe('compiling…')
334+
})
308335
})

packages/hub/src/node/host-docks.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,12 @@ export class DevframeDocksHost implements DevframeDocksHostType {
148148
if (patch.id && patch.id !== view.id) {
149149
throw diagnostics.DF8101({ id: view.id, attempted: patch.id })
150150
}
151-
this.update(Object.assign(this.views.get(view.id)!, patch))
151+
// Merge into a fresh object rather than mutating the stored entry in
152+
// place: once projected into the `devframe:docks` shared state, the
153+
// stored entry is deep-frozen by Immer, so an in-place `Object.assign`
154+
// would throw "Cannot assign to read only property". A new object is
155+
// always writable, and `update()` swaps it into the registry.
156+
this.update({ ...this.views.get(view.id)!, ...patch } as DevframeDockUserEntry)
152157
},
153158
}
154159
}

0 commit comments

Comments
 (0)