Pivot: fold drizzlebox as utils, HonkerEventTarget, OperationSpecs as repo surface
- 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
This commit is contained in:
53
docs/architecture/decisions/046-fold-drizzlebox-as-utils.md
Normal file
53
docs/architecture/decisions/046-fold-drizzlebox-as-utils.md
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
status: accepted
|
||||
date: 2026-06-01
|
||||
supersedes: ADR-005, ADR-018
|
||||
---
|
||||
|
||||
# ADR-046: Fold @alkdev/drizzlebox as src/sqlite/utils/
|
||||
|
||||
## Context
|
||||
|
||||
`@alkdev/drizzlebox` (a fork of `drizzle-typebox` adapted for
|
||||
`@alkdev/typebox`) provides `createSelectSchema` and `createInsertSchema`
|
||||
functions that derive TypeBox validation schemas from Drizzle table
|
||||
definitions. It is consumed as an external npm dependency by all SQLite
|
||||
table files in `@alkdev/storage`.
|
||||
|
||||
The parent project `@alkdev/dbtype` also planned a Phase 1 (UJSX→HostConfig
|
||||
→Drizzle pipeline) that was never implemented. With SQLite as the sole
|
||||
database target (ADR-038), the multi-dialect column mappings in dbtype
|
||||
(PG, MySQL, SingleStore) are dead weight for storage.
|
||||
|
||||
ADR-018 deferred dbtype integration to post-v1. But the fold of just the
|
||||
Phase 0 subset (column→TypeBox mappings + schema generation) is a
|
||||
straightforward import path change with no behavioral difference.
|
||||
|
||||
## Decision
|
||||
|
||||
Fold the SQLite-only subset of `@alkdev/dbtype` Phase 0 into
|
||||
`src/sqlite/utils/`:
|
||||
|
||||
| Source (dbtype) | Target (storage) | Changes |
|
||||
|-----------------|-------------------|---------|
|
||||
| `schema.ts` | `utils/schema.ts` | Remove PgEnum handling. Keep createSelectSchema, createInsertSchema, createUpdateSchema. |
|
||||
| `column.ts` | `utils/column.ts` | Strip PG, MySQL, SingleStore branches. Keep SQLiteInteger, SQLiteReal, SQLiteText + generic dataType dispatch. |
|
||||
| `schema.types.ts` + `schema.types.internal.ts` + `column.types.ts` | `utils/types.ts` | Merged. Remove PgEnum overloads. |
|
||||
| `constants.ts` | `utils/constants.ts` | Keep as-is. |
|
||||
| `utils.ts` | `utils/utils.ts` | Remove PgEnum alias. Keep isColumnType, isWithEnum, JsonSchema, BufferSchema. |
|
||||
|
||||
All table files change their import from `@alkdev/drizzlebox` to `../utils/schema.ts`.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **No external drizzlebox dependency** — one fewer npm package in the
|
||||
dependency graph.
|
||||
- **SQLite-only column mappings** — dead multi-dialect code removed. If a
|
||||
new database host is added later, mappings would need to be re-added.
|
||||
- **Same API surface** — `createSelectSchema`, `createInsertSchema`,
|
||||
`createUpdateSchema` produce the same TypeBox schemas.
|
||||
- **Co-located with tables** — utility code lives next to the tables it
|
||||
derives schemas from. Easier to maintain.
|
||||
- **dbtype Phase 1 (UJSX→HostConfig) not affected** — that remains a
|
||||
separate architectural concern, could live in storage or as its own
|
||||
package when built.
|
||||
58
docs/architecture/decisions/047-honker-event-target.md
Normal file
58
docs/architecture/decisions/047-honker-event-target.md
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
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).
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
status: accepted
|
||||
date: 2026-06-01
|
||||
supersedes: ADR-033 (partially — OQ-17, OQ-18, OQ-19 updated)
|
||||
---
|
||||
|
||||
# ADR-048: OperationSpecs as Repository Surface
|
||||
|
||||
## Context
|
||||
|
||||
ADR-033 established JSON path queries with hand-written CRUD for v1. The
|
||||
question was whether the repository layer would be hand-written query
|
||||
functions, auto-generated from table definitions, or something else.
|
||||
|
||||
The `@alkdev/operations` package provides `OperationSpec` — a serializable
|
||||
descriptor for a query, mutation, or subscription operation. Hubs and spokes
|
||||
register specs in an `OperationRegistry` along with handlers. The operations
|
||||
runtime handles execution, call protocol routing, subscriptions, access
|
||||
control, and validation.
|
||||
|
||||
This presents a natural alternative to hand-written CRUD: storage outputs
|
||||
`OperationSpec[]` describing CRUD operations per table, and the consumer
|
||||
wires them into the operations registry.
|
||||
|
||||
## Decision
|
||||
|
||||
Storage does not ship a "repository layer" of hand-written query functions.
|
||||
Instead, it outputs `OperationSpec[]` per table — flat arrays describing
|
||||
CRUD operations. The consumer (hub/spoke) imports these specs, registers
|
||||
handlers, and the operations runtime handles execution.
|
||||
|
||||
```ts
|
||||
// Storage defines table + operation contracts
|
||||
export const graphTypes = sqliteTable("graph_types", { ... });
|
||||
export const graphTypeSpecs: OperationSpec[] = [
|
||||
{ name: "create", namespace: "graph_types", type: "mutation", ... },
|
||||
{ name: "find", namespace: "graph_types", type: "query", ... },
|
||||
{ name: "list", namespace: "graph_types", type: "query", ... },
|
||||
{ name: "update", namespace: "graph_types", type: "mutation", ... },
|
||||
{ name: "delete", namespace: "graph_types", type: "mutation", ... },
|
||||
];
|
||||
|
||||
// Hub registers specs + handlers
|
||||
for (const spec of graphTypeSpecs) {
|
||||
registry.registerSpec(spec);
|
||||
registry.registerHandler(`${spec.namespace}.${spec.name}`, handler);
|
||||
}
|
||||
```
|
||||
|
||||
The handler is consumer-provided. Storage defines the contract (input/output
|
||||
schemas, operation type); the hub provides the execution (Drizzle queries,
|
||||
Honker transactions, notifications).
|
||||
|
||||
`@alkdev/operations` is a type-only peer dependency of storage. No circular
|
||||
dependency.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **No hand-written repository functions** — storage avoids the maintenance
|
||||
burden of typed CRUD code. The contract is the OperationSpec.
|
||||
- **Consumer owns execution** — the hub decides how to handle each operation
|
||||
(raw Drizzle query, with Honker notification, with access control, etc.).
|
||||
Storage doesn't execute queries.
|
||||
- **Subscriptions for free** — OperationSpec supports `type: "subscription"`.
|
||||
Table-level change streams become operation subscriptions rather than
|
||||
custom event handling.
|
||||
- **Clean dependency graph** — storage depends on operations only for the
|
||||
`OperationSpec` type (peer dep). No runtime dependency.
|
||||
- **Attribute queries remain JSON path** — for metagraph tables where
|
||||
attributes are dynamic JSON, `json_extract()` is still the query mechanism.
|
||||
Domain-specific tables with native columns produce simpler specs.
|
||||
- **Supersedes ADR-033 partially** — the "hand-written CRUD" part is replaced.
|
||||
The "JSON path queries for attributes" part still applies to metagraph
|
||||
tables.
|
||||
Reference in New Issue
Block a user