Skip to content

Replica doesn't observe in-place array mutations nested inside records (splice/push lost) #6

Description

@RobPruzan

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions