Skip to content

feat(operation): add AtomicOperation::supports_hooks() probe - #155

Merged
bodymindarts merged 1 commit into
mainfrom
feat/atomic-op-supports-hooks
Jul 22, 2026
Merged

feat(operation): add AtomicOperation::supports_hooks() probe#155
bodymindarts merged 1 commit into
mainfrom
feat/atomic-op-supports-hooks

Conversation

@bodymindarts

@bodymindarts bodymindarts commented Jul 21, 2026

Copy link
Copy Markdown
Member

What

Adds a read-only capability probe to AtomicOperation:

fn supports_hooks(&self) -> bool { false } // default
  • DbOptrue
  • DbOpWithTime / OpWithTime → delegate to their inner op
  • default (bare sqlx::Transaction, HookOperation) → false

Why

commit_hook::<H>(&self) -> Option<&H> is the only &self window into hook state, but its None is ambiguous: it means either "this op doesn't support hooks" or "it does, but nothing of type H is registered yet." The only unambiguous support signal today is add_commit_hook, which returns Err iff unsupported — but that requires &mut and consumes a hook value.

supports_hooks() reports capability directly, with &self and no registration side effect.

Consumer

obix's new OpCursor marks a position in an op's publish buffer and reads events back before commit (for atomic cross-outbox reposts). It must fail loudly when handed an op that can't buffer (a bare sqlx::Transaction) rather than silently yield nothing. Without this probe, cursor() had to take &mut op and register an empty hook just to detect support; with it, cursor(&op) -> Result<…> stays borrow-light:

if !op.supports_hooks() {
    return Err(CursorError::HooksUnsupported);
}

Tests

tests/hooks.rs::supports_hooks_reflects_op_capability — asserts true for DbOp and its time wrappers, false for a bare sqlx::Transaction. Full hooks suite: 11 passed. cargo fmt --check clean; no new clippy warnings.

Additive and backwards-compatible (defaulted trait method). Unblocks the es-entity bump + OpCursor fail-loud behavior in obix (GaloyMoney/obix#79).

🤖 Generated with Claude Code


Note

Low Risk
Additive defaulted trait method with narrow capability reporting; no change to commit or hook execution paths.

Overview
Adds AtomicOperation::supports_hooks(&self) -> bool so callers can tell whether an op can register commit hooks without using &mut or registering a dummy hook.

commit_hook’s None could mean “no hooks” or “nothing registered yet”; supports_hooks is unambiguous: DbOp (and time wrappers via delegation) return true, while the trait default and bare sqlx::Transaction return false.

A new integration test in tests/hooks.rs asserts that behavior for DbOp, DbOpWithTime, OpWithTime, and a raw transaction.

Reviewed by Cursor Bugbot for commit acde1d9. Bugbot is set up for automated code reviews on this repo. Configure here.

A read-only (`&self`) way to detect whether an operation supports commit
hooks. Defaults to false; DbOp reports true and the time wrappers
(DbOpWithTime, OpWithTime) delegate to their inner op.

This disambiguates a gap in commit_hook(): its None conflates "op does
not support hooks" with "op supports hooks but none registered yet", so
callers could not detect hook support without a &mut add_commit_hook
registration attempt. supports_hooks() reports capability directly.

Consumed by obix's OpCursor to fail loudly on hookless ops (e.g. a bare
sqlx::Transaction) via a borrow-light cursor(&op) instead of needing
&mut op.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@bodymindarts
bodymindarts merged commit e6a2dc2 into main Jul 22, 2026
7 checks passed
bodymindarts added a commit to GaloyMoney/obix that referenced this pull request Jul 22, 2026
Previously cursor() silently returned a cursor that yielded nothing on
an op without commit-hook support (a bare sqlx::Transaction, which
persists publishes immediately with no op-local buffer). In the
cross-outbox repost use case that meant the source events persisted
while the mapped repost silently produced zero events.

cursor() now returns Result and errors with CursorError::HooksUnsupported
when the op does not support hooks, detected via es-entity 0.11.3's new
&self AtomicOperation::supports_hooks() probe. The signature stays
borrow-light (&op, no &mut) and the reads stay infallible: a valid
OpCursor implies hook support, and commit_hook()==None still covers the
legitimate supported-but-nothing-published-yet case.

Bumps es-entity 0.11.1 -> 0.11.3 for supports_hooks() (GaloyMoney/es-entity#155).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bodymindarts added a commit to GaloyMoney/obix that referenced this pull request Jul 22, 2026
For AtomicOperation::supports_hooks() (GaloyMoney/es-entity#155), used by
OpCursor to detect commit-hook support. Keeps job at 0.6.30.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bodymindarts added a commit to GaloyMoney/obix that referenced this pull request Jul 22, 2026
…#79)

* chore(deps): bump es-entity to 0.11.3

For AtomicOperation::supports_hooks() (GaloyMoney/es-entity#155), used by
OpCursor to detect commit-hook support. Keeps job at 0.6.30.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(outbox): add OpCursor for pre-commit reads of op-buffered events

Publishing to an outbox within an es_entity op buffers events on a
(merged) commit hook; nothing is written until op.commit(). OpCursor
marks a position in that buffer so events published after it can be read
back before commit, via reads on the Outbox that mirror how
EventSequence is passed to listen_persisted:

- Outbox::cursor(&op)                  -> Result<OpCursor<P, Tables>, CursorError>
- Outbox::new_events(&op, &mut cursor) -> &[P]   (advancing)
- Outbox::map_new(&op, &mut cursor, f) -> Vec<T> (advancing, filter_map)
- Outbox::peek_new(&op, &cursor)       -> &[P]   (non-advancing)

The cursor is a passive token (index + PhantomData); reads borrow the op,
so the buffer can't change while a slice is held. The type parameters pin
a cursor to its outbox's payload/table types.

cursor() fails loudly with CursorError::HooksUnsupported on an op that
does not support commit hooks (a bare sqlx::Transaction, which persists
publishes immediately with no op-local buffer) rather than returning a
cursor that silently yields nothing. Support is detected via es-entity's
&self AtomicOperation::supports_hooks(), so the signature stays
borrow-light (&op) and the reads stay infallible.

Motivating use case: atomically republish a mapped projection of one
outbox's events onto another outbox in the same transaction (e.g.
cala -> lana). PersistEvents exposes a crate-private pending() accessor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

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