Skip to content

server: onMutationCommitted, a post-commit hook with net row changes — 0.2.1 - #1

Merged
InfinityBowman merged 1 commit into
mainfrom
feat/on-mutation-committed
Sep 7, 2026
Merged

InfinityBowman merged 1 commit into
mainfrom
feat/on-mutation-committed

Conversation

@InfinityBowman

@InfinityBowman InfinityBowman commented Sep 7, 2026

Copy link
Copy Markdown
Owner

What

WorkspaceEngineConfig gains onMutationCommitted(event, env), invoked once per mutation that wrote rows, after its transaction committed, with { workspaceId, name, args, principal, clientId, version, changes }. Each change is { tbl, id, before, after }: the row's stored image at the mutation's first touch against what it left.

  • Observer, never participant: never awaited, a returned promise goes to waitUntil, throw or rejection lands on the engine logger with the mutation name and version.
  • Never fires for migrations, admin import or reset, rejected mutations, or writes that net to nothing. At-most-once delivery.
  • Before-image capture is opt-in on the WriteSet; the DO enables it only when a hook is configured. A tx.get that reached storage doubles as the image, so read-modify-write mutators add no read.
  • createTestEngine().mutate() returns the same changes list for node-level testing of hook logic.

Why

Three CoRATES issues want the same seam: assignment notifications (#628), D1 projections (#645), and workspace activity logging (#660). Built once to the #645 shape so none needs a further engine change.

Verification

  • 6 workerd drills in commit-hook.test.ts, 5 node tests in write-changes.test.ts.
  • pnpm typecheck, pnpm test (361 passing), pnpm check:packages all green.

Additive API, hence 0.2.1.

https://claude.ai/code/session_012Xd62wDxf41DjJK1T3Wh3Y

Summary by CodeRabbit

  • New Features

    • Added a post-commit hook for observing successful mutations and their row-level before-and-after changes.
    • Exposed mutation change details through the server and test APIs, including inserts, updates, and deletes.
    • Added metadata such as mutator details, principal, client ID, and committed version to commit events.
  • Documentation

    • Documented hook behavior, event data, delivery guarantees, exclusions, and testing support.
  • Bug Fixes

    • Hook errors are logged without blocking mutation confirmation.

…— 0.2.1

WorkspaceEngineConfig gains onMutationCommitted(event, env), invoked once per
mutation that wrote rows, after its transaction committed, with { workspaceId,
name, args (parsed), principal, clientId, version, changes }. Each change is the
row's stored image at the mutation's first touch against what it left, so a row
put twice reports once and a create-then-delete of a new row reports nothing.
The hook observes and never participates: it is never awaited (the push handler
stays synchronous), a returned promise goes to waitUntil, and a throw or
rejection lands on the engine logger with the mutation name and version.
Migrations, admin import and reset bypass the mutation path and never fire it;
rejected mutations and no-op writes are silent too. Delivery is at-most-once.

Before-image capture is opt-in on the WriteSet and the DO enables it only when
a hook is configured, so apps without one pay nothing. A tx.get that reached
storage doubles as the image, so read-modify-write mutators add no read; blind
puts pay one point lookup. Migrations never track.

createTestEngine().mutate() returns the same changes list, so the logic behind a
hook is unit-testable in node. Additive API, hence a patch.

Claude-Session: https://claude.ai/code/session_012Xd62wDxf41DjJK1T3Wh3Y
@InfinityBowman
InfinityBowman merged commit de610bf into main Sep 7, 2026
0 of 2 checks passed
@InfinityBowman
InfinityBowman deleted the feat/on-mutation-committed branch September 7, 2026 20:33
@coderabbitai

coderabbitai Bot commented Sep 7, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: db30b6ae-cd6a-41e0-aa07-fef9abccf844

📥 Commits

Reviewing files that changed from the base of the PR and between 93e40c0 and 607b6a3.

📒 Files selected for processing (14)
  • ARCHITECTURE.md
  • docs/guide/mutations.md
  • docs/guide/testing.md
  • docs/reference/server.md
  • docs/reference/testing.md
  • packages/server/package.json
  • packages/server/src/config.ts
  • packages/server/src/do.ts
  • packages/server/src/engine-core.ts
  • packages/server/src/index.ts
  • packages/server/src/testing.ts
  • packages/server/test/commit-hook.test.ts
  • packages/server/test/fixture/worker.ts
  • packages/server/test/node/write-changes.test.ts

📝 Walkthrough

Walkthrough

Adds net row-change tracking to WriteSet, exposes changes through the test engine, and introduces the post-commit onMutationCommitted hook with typed event data, asynchronous error handling, exclusions, tests, and documentation.

Changes

Post-commit mutation notifications

Layer / File(s) Summary
Public event contract and documentation
packages/server/src/config.ts, packages/server/src/index.ts, packages/server/package.json, docs/..., ARCHITECTURE.md
Defines RowChange, MutationCommitted, and onMutationCommitted. Exports the new types and documents event fields, delivery rules, exclusions, and test results.
Net row-change capture and test-engine results
packages/server/src/engine-core.ts, packages/server/src/testing.ts, packages/server/test/node/write-changes.test.ts
WriteSet records first-touch before-images and returns ordered net changes. TestEngine returns those changes and tests inserts, updates, deletes, repeated touches, no-ops, and failed mutations.
Durable Object hook delivery and integration coverage
packages/server/src/do.ts, packages/server/test/commit-hook.test.ts, packages/server/test/fixture/worker.ts
The Durable Object emits committed events after successful writes, holds promise results with waitUntil, logs hook failures, and excludes migrations, admin operations, rejected mutations, and no-op writes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant DurableObject
  participant WriteSet
  participant onMutationCommitted
  participant waitUntil
  participant logger
  Client->>DurableObject: Submit mutation
  DurableObject->>WriteSet: Flush candidate writes
  WriteSet-->>DurableObject: Return written count and row changes
  DurableObject->>onMutationCommitted: Invoke committed event
  onMutationCommitted-->>DurableObject: Return synchronously or return promise
  DurableObject->>waitUntil: Hold returned promise
  onMutationCommitted-->>logger: Report throw or rejection
  DurableObject-->>Client: Send mutation confirmation
Loading
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/on-mutation-committed

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

InfinityBowman added a commit that referenced this pull request Sep 7, 2026
The convergence test polls mid-stream with 50ms waits and treats a timeout
as "nothing pending". But a timed-out nextFrame left its waiter queued, so
the next frame to arrive resolved a promise nobody awaited and never reached
the queue — one relayed edit vanished per stale waiter, and the two docs
diverged. It surfaced on a contended CI runner (PR #1's run, alongside the
push run for the same commit), where relays arrive late enough to trip it.

The harness now withdraws a waiter when its timer fires, for frames, JSON
and close alike. The test also counts what each side is owed — every update
one client sends is relayed to the other exactly once — and the final drain
waits for that count with the normal timeout instead of reading silence as
done, so a slow relay waits rather than fails.

Claude-Session: https://claude.ai/code/session_012Xd62wDxf41DjJK1T3Wh3Y
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