Skip to content

RFC: entity.update(trait, draft => { ... }) — safe mutation that always signals changes #166

@lamaster

Description

@lamaster

Problem

Mutating arrays/objects inside a trait “in place” can fail to trigger updates; developers must remember to reassign or call entity.changed(trait) manually. This is easy to miss and leads to confusing “UI did not update” bugs. There is an open issue reporting cases where useTraitEffect updates but useTrait does not, highlighting change‑detection pain in real projects.
References:


Proposal

Provide a small, ergonomic mutation primitive that guarantees change signaling:

// Always marks the trait as changed if anything inside is modified.
entity.update(Inventory, inv => {
  // Nested object + array mutations should be safe:
  const stack = inv.stacks.find(s => s.itemId === newItemId)
  if (stack) stack.count += 1
  else inv.stacks.push({ itemId: newItemId, count: 1 })
})

Principles

  • The updater receives a draft (or equivalent safe handle) to mutate.
  • After the updater returns, the engine calls changed(trait) exactly once if anything meaningful changed.
  • Works for nested objects/arrays; trivial scalar writes continue to behave as they do today.

Prior art (authoritative references)


Performance considerations

  • No heavy deep proxies required: the contract should not mandate Immer‑style deep proxies; implementations can fast‑path scalars and use lightweight bookkeeping for arrays/objects.
  • One signal per call: even if the updater makes many writes, emit at most one changed(trait) to avoid redundant downstream work.
  • SoA / typed arrays: for traits backed by typed arrays, direct index writes must remain fast; update should not add overhead in these cases.

Edge cases to define

  • Exceptions inside the updater: decide whether partial mutations should mark changed; safest is to mark changed only if the updater completes successfully.
  • Re‑entrancy: prevent calling update on the same trait recursively.
  • Interop with future Changed<T> filters: ensure this path triggers those reliably when richer query filters land.

Related Koota discussion

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