- Update architecture docs to reflect pivot from @libsql/client to Honker - Fold @alkdev/drizzlebox Phase 0 into src/sqlite/utils/ (ADR-046) - Add HonkerEventTarget adapter for pubsub TypedEventTarget (ADR-047) - Replace hand-written CRUD with OperationSpec generation (ADR-048) - Resolved OQ-26: Honker replaces Redis for single-node pub/sub (POC validated) - Updated OQ-17, OQ-18, OQ-19 for OperationSpec repository surface - Added OQ-30 (composite event target), OQ-31 (consumer naming), OQ-32 (Drizzle Kit) - POC results: adapter buildable, same-process pub/sub works, transactional outbox semantics confirmed, concurrent listeners/streams work - Research doc at docs/research/pivot-honker-sqlite-adapter.md
58 lines
2.6 KiB
Markdown
58 lines
2.6 KiB
Markdown
---
|
|
status: accepted
|
|
date: 2026-06-01
|
|
resolves: OQ-26
|
|
---
|
|
|
|
# ADR-047: HonkerEventTarget Adapter for pubsub
|
|
|
|
## Context
|
|
|
|
`@alkdev/pubsub` defines a `TypedEventTarget` interface that all transport
|
|
adapters implement: in-process `EventTarget`, Redis, WebSocket client/server,
|
|
and Worker. This provides transport-agnostic pub/sub — consumers call
|
|
`createPubSub({ eventTarget })` without knowing the underlying transport.
|
|
|
|
Honker provides SQLite with built-in pub/sub primitives:
|
|
- `db.notify(channel, payload)` / `db.listen(channel)` — ephemeral, fire-and-forget
|
|
- `db.stream(name).publish(payload)` / `db.stream(name).subscribe(consumer)` — durable, offset-tracked
|
|
|
|
POC 2-4 (2026-06-01) validated:
|
|
- Same-process notify→listen works. ~17ms median latency.
|
|
- Multiple concurrent listeners on different channels work.
|
|
- `tx.notify()` only fires on `tx.commit()`. Rollback suppresses notification.
|
|
- `queue.enqueueTx(tx, payload)` only visible after commit. Rollback suppresses.
|
|
- Stream publish/subscribe works with consumer offset tracking.
|
|
|
|
These results confirm Honker can back the pubsub `TypedEventTarget`
|
|
interface for single-node deployments.
|
|
|
|
## Decision
|
|
|
|
Implement `HonkerEventTarget` in `src/sqlite/event-target.ts`. It adapts
|
|
`@alkdev/pubsub`'s `TypedEventTarget` to Honker primitives with two modes:
|
|
|
|
1. **Ephemeral mode**: `addEventListener` → `db.listen()`, `dispatchEvent` →
|
|
`db.notify()`. Fire-and-forget semantics, no delivery guarantee.
|
|
2. **Durable mode**: `addEventListener` → `db.stream().subscribe()`,
|
|
`dispatchEvent` → `db.stream().publish()`. Per-consumer offset tracking,
|
|
crash recovery replays from last saved offset.
|
|
|
|
`@alkdev/pubsub` is a peer dependency (needed only when using
|
|
HonkerEventTarget). The `graphs/` module remains zero-dep.
|
|
|
|
## Consequences
|
|
|
|
- **Single-node hub can use Honker instead of Redis** — no separate Redis
|
|
deployment needed for pub/sub.
|
|
- **Transactional outbox semantics** — `dispatchEvent` inside a Drizzle
|
|
transaction (via `tx.notify()` or `stream.publishTx()`) commits atomically
|
|
with data writes. No dual-write problem.
|
|
- **Hub-spoke symmetry** — both hub and spoke use the same `createPubSub()`
|
|
call. Different event target instances determine routing.
|
|
- **Multi-node still needs Redis or WebSocket** — Honker events don't cross
|
|
process boundaries. For multi-node, the `WebSocketServerEventTarget` (hub)
|
|
and `WebSocketClientEventTarget` (spoke) handle cross-process routing.
|
|
- **Latency trade-off** — ~17ms Honker round-trip vs sub-ms in-process. For
|
|
hot-path call protocol, pair with in-process `EventTarget`. Design of a
|
|
composite event target is an open question (OQ-30). |