Skip to content

Add GetOrAdd and GetOrAddFunc to the single-component handle - #4

Merged
herve-quiroz merged 2 commits into
mainfrom
claude/issue-3
Jun 20, 2026
Merged

Add GetOrAdd and GetOrAddFunc to the single-component handle#4
herve-quiroz merged 2 commits into
mainfrom
claude/issue-3

Conversation

@herve-quiroz

Copy link
Copy Markdown
Contributor

Summary

Adds a first-class get-or-create operation to the single-component handle (Accessor[A]), as specified in #3:

  • GetOrAdd(id, value) *A — returns the interior pointer to the entity's component, inserting value first if absent (eager).
  • GetOrAddFunc(id, make) *A — same, but builds the value lazily via make, which is called only on the miss path (nothing constructed on a hit).

Both return a non-nil *A, valid until the next structural change to the store (the same contract as Get and iteration).

Design notes

The add honours the existing deferral model exactly as Add does:

  • At depth 0 (outside iteration): immediate. A dead entity is not stored (mirroring Add's no-op), but a non-nil pointer to the unstored value is still returned to keep the "never nil" contract.
  • During an All() loop: deferred. Because the result must remain a usable pointer for the rest of the loop, the value is staged off-store and the returned pointer points at it; the auto-flush inserts that same value, so writes made through the pointer during the loop survive into the store. No mutation touches the dense storage mid-iteration, so previously-yielded iteration pointers stay valid.

Both methods delegate to a shared private insertMissing helper after the get-miss check, so the eager/lazy split is the only difference between them.

Scope is the single-component handle only; the join handles (Accessor2/Accessor3) are out of scope per the issue.

Testing

New tests in ecs_components_test.go cover: insert-on-miss (both variants), return-existing-without-overwrite, make not called on hit, dead-entity returns non-nil without storing, deferred-during-iteration (mutation through the staged pointer lands after flush), and hit-path-during-iteration returning a live pointer.

Verification (all clean):

gofmt -l ecs
go vet ./...
go build ./...
go test ./... -race

Docs

Updated the README's "Core concepts" and "API at a glance" to list the new get-or-create pair.

fixes #3


✨ Content generated by Claude AI.

Introduce a first-class get-or-create operation on Accessor[A]:

- GetOrAdd(id, value) returns the interior pointer to the entity's
  component, adding value first if absent (eager).
- GetOrAddFunc(id, make) does the same but builds the value lazily via
  make, which is called only on the miss path.

Both return a non-nil *A valid until the next structural change, and the
add honours the existing deferral rules: immediate at depth 0, deferred
during an All() loop where the returned pointer is to a staged value the
flush inserts, so writes through it survive into the store.

Update the README's core concepts and "API at a glance" to list the new
get-or-create pair.

fixes #3
@herve-quiroz herve-quiroz added the claude Ticket that should be handled by Claude label Jun 19, 2026
@github-actions github-actions Bot added the input required Derived from whether the last interaction was Claude-authored. Do not set by hand label Jun 19, 2026
GetOrAdd's miss path previously inserted via applyAdd and then re-derived
the interior pointer with a second map lookup. Have applyAdd return the
pointer it just created so the lookup happens once; other callers ignore
the return value.
@herve-quiroz
herve-quiroz merged commit 953afc8 into main Jun 20, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude Ticket that should be handled by Claude input required Derived from whether the last interaction was Claude-authored. Do not set by hand

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add GetOrAdd and GetOrAddFunc to the single-component handle

1 participant