Skip to content

ACT-101: Store.notify hook for cross-process drain wakeup#693

Merged
Rotorsoft merged 3 commits into
masterfrom
ACT-101
May 10, 2026
Merged

ACT-101: Store.notify hook for cross-process drain wakeup#693
Rotorsoft merged 3 commits into
masterfrom
ACT-101

Conversation

@Rotorsoft
Copy link
Copy Markdown
Owner

@Rotorsoft Rotorsoft commented May 10, 2026

Summary

Closes #672. Adds an optional Store.notify(handler) subscription that the orchestrator auto-wires at build time, with the cost gated behind a per-adapter opt-in.

  • PostgresStore: implements notify via LISTEN/NOTIFY on a per-(schema, table) channel (act_commit_<schema>_<table>). Opt-in via notify: true (default false) — when off, commit() skips pg_notify and the notify method is undefined on the instance, so the orchestrator's auto-wire if (store.notify) short-circuits and no LISTEN client is allocated. Existing callers see zero behavior change after upgrading.
  • InMemoryStore, SqliteStore: notify left undefined — single-process / single-node by design.
  • Act orchestrator: subscribes once at construction when the store exposes notify AND reactions are registered. New notified lifecycle event passes the payload through for SSE fan-out, dashboards, audit. Per-instance _by UUID self-filter keeps the channel cross-process — local commits already arm drain via do().
  • Build-time contract: inject store(adapter) before act()...build() — wiring binds at construction.

Performance

Benchmark in libs/act-pg/PERFORMANCE.md. Two PostgresStore instances on docker PG, 30 commits each:

mode p50 p95 p99
notify 8 ms 12 ms 25 ms
polling 31 ms 54 ms 69 ms

At the default start_correlations 10s interval the gap blows out to ~1000×.

Run: pnpm -F @rotorsoft/act-pg exec vitest run --config vitest.bench.config.ts

Coverage

100% statements / branches / functions / lines workspace-wide. 1024 tests passing.

Docs

  • CLAUDE.md — new "Cross-Process Reactions" section, Store contract amendment, troubleshooting bullet
  • libs/act/README.md, libs/act-pg/README.md, libs/act-sqlite/README.md updated with opt-in story
  • docs/docs/architecture/cross-process-reactions.md (new Docusaurus page)
  • .claude/skills/scaffold-act-app/server.mdnotified lifecycle event
  • TSDoc on Store.notify, StoreNotification, NotifyDisposer, ActLifecycleEvents.notified, Config.notify

Test plan

  • Workspace tests pass (pnpm test) — 1024/1024
  • Lint clean (pnpm lint)
  • Workspace build clean (pnpm build)
  • Coverage 100% across the board
  • Cross-process integration tests against docker PG (libs/act-pg/test/notify.spec.ts)
  • Default-off proof: writer with notify: false and listener with notify: true deliver no notifications
  • Auto-wiring unit tests with fake store (libs/act/test/notify.spec.ts)
  • Mock-based error-path tests (libs/act-pg/test/store.error.spec.ts)
  • Benchmark run: pnpm -F @rotorsoft/act-pg exec vitest run --config vitest.bench.config.ts

🤖 Generated with Claude Code

rotorsoft and others added 3 commits May 9, 2026 22:11
…101)

Closes #672. Adds an optional `Store.notify(handler)` subscription that
the orchestrator auto-wires at build time. When a different process
commits to the same backing store, the listener wakes `settle()`
immediately instead of waiting for the polling/debounce floor — at the
default `start_correlations` interval, that's a ~1000× p99 reduction.

- `PostgresStore`: implemented via `LISTEN`/`NOTIFY` on a per-(schema,
  table) channel; `commit()` issues one NOTIFY per transaction with the
  full event batch as JSON. Self-filter via per-instance `_by` UUID so
  the `notified` lifecycle event surfaces only cross-process activity.
- `InMemoryStore`, `SqliteStore`: leave `notify` undefined — single
  process / single node, no remote writers possible.
- `Act` constructor wires the subscription when the store implements
  `notify` AND there are reactions to wake. New `notified` lifecycle
  event passes the payload through for SSE fan-out, dashboards, audit.
- Build-time contract: inject the store via `store(adapter)` *before*
  `act()...build()` — wiring binds at construction.

Coverage: 100% statements/branches/functions/lines workspace-wide.
Benchmark in libs/act-pg/PERFORMANCE.md (notify p99 ≈25ms vs polling
p99 ≈77ms at 50ms intervals).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The auto-wire of cross-process notifications added overhead even on
single-instance deployments — one `pg_notify` SQL per commit and a
dedicated LISTEN client per process for the lifetime of the
orchestrator. Make the cost explicit and zero by default.

- `PostgresStore` adds optional `notify?: boolean` to its config
  (default `false`).
- When `false`: `commit()` skips the `pg_notify` SQL entirely, and
  `notify` is left undefined on the store — the orchestrator's
  `if (store.notify)` auto-wire short-circuits, so no LISTEN client
  is ever allocated. Existing callers see zero behavior change.
- When `true`: same behavior as the previous always-on default.
- `vitest.bench.config.ts` added so the perf bench can be invoked
  cleanly without polluting the default `pnpm test` run.
- Docs updated everywhere (CLAUDE.md, READMEs, Docusaurus page,
  PERFORMANCE.md, scaffold-act-app skill, TSDoc).

Coverage: 100% across the board (1024 tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The store-side `notify` implementation in PostgresStore was carrying
two kinds of code: adapter-specific (channel filter, JSON parse,
self-filter, payload validation) and generic (lifecycle emit, drain
wakeup, listener-error containment). The generic part now lives in
`Act._wireNotify` so every adapter inherits it without re-implementing.

- `Act._wireNotify` wraps `emit("notified")` + drain arm/settle in
  try/catch so a buggy user `notified` listener can't tear down the
  wiring.
- The PG adapter still wraps the framework-supplied handler in its
  own try/catch — defense in depth, since direct callers of
  `store.notify(handler)` (custom integrations, isolated tests)
  also need their LISTEN client kept alive when their handler throws.
- New unit test confirms the orchestrator-level wrap.

Coverage: 100% across the board (1025 tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Rotorsoft Rotorsoft merged commit fb03458 into master May 10, 2026
7 checks passed
@github-actions
Copy link
Copy Markdown

🎉 This PR is included in version @rotorsoft/act-v0.34.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@github-actions
Copy link
Copy Markdown

🎉 This PR is included in version @rotorsoft/act-pg-v0.19.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@github-actions
Copy link
Copy Markdown

🎉 This PR is included in version @rotorsoft/act-sqlite-v0.4.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ACT-101: add Store.notify(stream) hook for low-latency drain wakeup

1 participant