Claude generated
Summary
When mutating an array nested inside a z.record(...) value via client.update, in-place mutations like arr.splice(...) or arr.push(...) are not picked up by the replica's change tracker. Sibling scalar assignments in the same update are observed, leading to partial / inconsistent snapshots in subscribers (useDb).
Repro
Schema (abridged):
const pane = z.object({
id: z.string(),
tabs: z.array(z.object({ id: z.string(), chatId: z.string().nullable() })),
activeTabId: z.string(),
})
const scopePaneState = z.object({
panes: z.array(pane),
activePaneId: z.string(),
})
const windowState = z.object({
// ...
scopePanes: z.record(z.string(), scopePaneState).default({}),
})
const schema = createSchema({
windowStates: z.record(z.string(), windowState).default({}),
})
Update from the renderer:
await dbClient.update(root => {
const state = root.app.windowStates["main"].scopePanes["scope-1"]
const newPane = { id: "p2", tabs: [{ id: "t2", chatId: "c2" }], activeTabId: "t2" }
state.panes.push(newPane) // <-- lost
state.activePaneId = "p2" // <-- observed
// Inside the same update callback, reading back state.panes shows length === 2,
// i.e. the proxy reports the mutation locally.
console.log(JSON.stringify(state)) // panes has 2 entries here
})
After the update commits, a subscriber:
useDb(root => root.app.windowStates["main"].scopePanes["scope-1"])
receives { panes: [<the original one>], activePaneId: "p2" } — activePaneId flipped, but panes is still length 1.
Replacing the array reference instead works around it:
state.panes = [...state.panes, newPane] // observed correctly
Expected
In-place splice / push / index assignment on arrays inside z.record values should be tracked the same way as on top-level arrays, matching the docs ("Inside update(), you mutate the root object directly, the same way you would with a regular JavaScript object"). At minimum, the inconsistency where sibling scalar assignments commit but the array mutation does not should not be possible.
Workaround
Replace the array reference instead of mutating it in place:
state.panes = state.panes.concat(newPane)
// or
const next = state.panes.slice(); next.splice(i, 0, x); state.panes = next
Environment
@zenbujs/core 0.0.24
- Electron 42
- Renderer (React 19) via
useDbClient().update
Happy to put together a minimal repro repo if useful.
Claude generated
Summary
When mutating an array nested inside a
z.record(...)value viaclient.update, in-place mutations likearr.splice(...)orarr.push(...)are not picked up by the replica's change tracker. Sibling scalar assignments in the same update are observed, leading to partial / inconsistent snapshots in subscribers (useDb).Repro
Schema (abridged):
Update from the renderer:
After the update commits, a subscriber:
receives
{ panes: [<the original one>], activePaneId: "p2" }—activePaneIdflipped, butpanesis still length 1.Replacing the array reference instead works around it:
Expected
In-place
splice/push/ index assignment on arrays insidez.recordvalues should be tracked the same way as on top-level arrays, matching the docs ("Insideupdate(), you mutate the root object directly, the same way you would with a regular JavaScript object"). At minimum, the inconsistency where sibling scalar assignments commit but the array mutation does not should not be possible.Workaround
Replace the array reference instead of mutating it in place:
Environment
@zenbujs/core0.0.24useDbClient().updateHappy to put together a minimal repro repo if useful.