Skip to content

feat: deferred end-of-mutation calculation hook#500

Open
smartive-nicolai[bot] wants to merge 2 commits into
mainfrom
feat/deferred-end-of-mutation-calculations
Open

feat: deferred end-of-mutation calculation hook#500
smartive-nicolai[bot] wants to merge 2 commits into
mainfrom
feat/deferred-end-of-mutation-calculations

Conversation

@smartive-nicolai

Copy link
Copy Markdown
Contributor

What

Adds a request-scoped mutation scratch bag (mutationState) and an afterMutations(ctx) lifecycle callback, so consumers can defer work to a single pass at the end of each top-level mutation instead of running it eagerly inside individual mutation hooks.

Why

In zwei-wealth-platform, entity calculations (Portfolio → Goal → Relation, etc.) currently run eagerly inside each afterMutate* hook. When one request mutates several related entities — via mutation reentrancy, delete-cascade or restore-cascade — the same entity is recalculated many times, in the wrong order, and on inconsistent mid-mutation state. This hook lets the consumer accumulate dirty entity ids across the whole mutation and run each calculation exactly once, in dependency order, on a consistent post-write state.

Changes

  • Context / MutationContext gain two optional fields:
    • mutationState?: Record<string, unknown> — a scratch bag, initialized to {} by mutationResolver at the start of every top-level mutation and shared by reference across all nested withTransaction levels (so a mark made deep inside a cascade is visible at the top).
    • afterMutations?: (ctx) => Promise<void> | void — run once at the end of the mutation.
  • mutationResolver sets mutationState = {} per top-level field and calls afterMutations after the write (including all cascade/reentrant writes) but before the result is read back, still inside the mutation transaction. Skipped for dry-run deletes.

Fully backwards compatible: both fields are optional; when afterMutations is unset the behaviour is unchanged.

Consumer

Pairs with the zwei-wealth-platform MR that moves Portfolio/Goal/Relation/PAR/RelationCost calculations to a single deferred flush (link to follow).

🤖 Generated with Claude Code

nicola-smartive and others added 2 commits June 23, 2026 14:28
Add a request-scoped mutation scratch bag (`mutationState`) and an
`afterMutations(ctx)` lifecycle callback so consumers can defer work to a
single pass at the end of each top-level mutation instead of running it
eagerly inside individual mutation hooks.

- `Context`/`MutationContext` gain `mutationState?` (shared by reference
  across all nested `withTransaction` levels) and `afterMutations?`.
- `mutationResolver` initializes `mutationState` to `{}` per top-level
  mutation field and invokes `afterMutations` after the write (including all
  cascade/reentrant writes) but before the result is read back, still inside
  the mutation transaction. Skipped for dry-run deletes.

This lets a consumer accumulate dirty entity ids across cascades/reentrancy
and run each derived calculation exactly once, in dependency order, on a
consistent post-mutation state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the afterMutations flush from mutationResolver into withTransaction so it
runs at the outermost mutation scope — i.e. whichever withTransaction creates
the mutationState bag flushes it on the way out. This guarantees the flush on
every mutation path, not just graphql mutations: custom resolvers, scripts,
tests and a bare createEntity/updateEntity now all run the deferred work
exactly once, without the consumer wiring anything.

- withTransaction: initialize mutationState and flush afterMutations when it is
  the outermost scope (mutationState was undefined on entry); nested scopes and
  callers that pre-seed mutationState to batch skip it. Errors roll the
  transaction back before the flush.
- mutationResolver: drop the explicit mutationState init (withTransaction owns
  it) but keep an explicit flush before the create/update read-back so the
  mutation response still reflects fresh values; delete/restore have no
  read-back and rely on the outermost flush (a dry-run delete throws and rolls
  back beforehand).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant