5.5 KiB
Enterprise migration: @suaveplan/imessage-adapter
- Change ID:
enterprise-imessage-adapter - Status: PROPOSED
- Capability:
imessage-adapter - Target package:
@suaveplan/imessage-adapterat0.1.0 - Target path:
packages/communication/messaging/imessage-adapter/ - Design: ./design.md
- Spec delta: ./specs/imessage-adapter/spec.md
- Source on main:
/mnt/s/web/suaveplan/packages/universal/imessage-adapter(~718 LOC) - Enterprise plan:
/mnt/s/web/suaveplan/enterprise/imessage-adapter.md
Why
@suaveplan/imessage-adapter is the macOS-side ingress for any genesis
messaging surface that wants to read from Apple's Messages app. It
polls the ~/Library/Messages/chat.db SQLite database, decodes the
Apple-epoch / nanosecond timestamps, resolves handle + chat + attachment
joins, and emits ParsedIMessage payloads — and a downstream
InboundMessage-shaped projection — that any inbound-message router
can consume without coupling to Apple's schema.
The upstream package (v1.0.0) is functional but not enterprise-grade:
require("bun:sqlite")is reached through anany-cast cast at every call site, defeating type-checking and blocking testability.- No structured error codes — every failure is a swallowed
console-lesserrorevent with a bareError. - No Zod boundary on config; no schema-registry hook.
- No OpenTelemetry instrumentation — no counters for messages emitted, no histogram for poll duration, no error counter.
- No
AbortSignalpropagation onstart()—stop()only. - The internal listener registry duplicates
@suaveplan/events'sEventBus; the package should ride the shared bus. - Tests don't exercise the watcher loop end-to-end against a real SQLite chat.db fixture — they only construct the adapter and assert on the un-initialised error path.
This change rewrites the package on genesis at enterprise quality
without removing a single export from the upstream surface, preserving
the contract that any future consumer (e.g. an inbound-message router,
the existing @suaveplan/api-plugin-messaging plugin family, or a
standalone agent) can rely on.
What
A net-new package at
packages/communication/messaging/imessage-adapter/ at 0.1.0,
preserving the upstream public surface:
- Classes:
IMessageAdapter,IMessageWatcher,IMessageParser - Types:
IMessageRow,HandleRow,ChatRow,AttachmentRow,ParsedIMessage,ParsedAttachment,IMessageAdapterConfig,IMessageWatcherConfig,InboundMessage,MessageSourceAdapter,IMessageWatcherEvents
Enterprise additions on top:
- Structured errors via
defineCodes("IMESSAGE", [...])— 10 stable codes (DB_OPEN_FAILED,DB_QUERY_FAILED,WATCHER_NOT_RUNNING,WATCHER_ALREADY_RUNNING,ADAPTER_NOT_INITIALIZED,INVALID_CONFIG,POLL_ABORTED,POLL_FAILED,ATTACHMENT_PARSE_FAILED,APPLE_DATE_PARSE_FAILED), each reachable from a real call site and asserted in tests. - Zod-validated config boundary at
IMessageWatcherConfigSchemaandIMessageAdapterConfigSchema, used by the constructor; invalid config throwsIMessageInvalidConfigError. - Injectable
SqliteReaderseam (structural type) replaces therequire("bun:sqlite") as anycast. Production wiring usesbun:sqlitelazily; tests use a real in-processbun:sqlitedatabase populated from a chat.db-shaped fixture (no mocks; rule 2). - Injectable
Clock+Timers+IdGeneratorfor deterministic polling, last-rowid pointer management, and envelope IDs. - OTel instrumentation — counters
imessage.messages_emitted_total,imessage.poll_errors_total,imessage.attachments_emitted_total; histogramimessage.poll_duration_ms. - AbortSignal propagation —
start({ signal }),poll({ signal }), and the internal scheduler all honour cancellation; aborted polls throwIMessagePollAbortedError. - Bus bridge —
IMessageWatcherexposes anEventBus-shaped surface in parallel with its legacyon/offAPI so downstream subscribers can compose with@suaveplan/eventsdirectly. The legacyon/offAPI is preserved for back-compat. - Idempotent rowid pointer —
lastRowIdis settable + readable, and the watcher refuses to re-emit messages withROWID <= lastRowIdso persisted-pointer consumers can resume safely. - 100/100/100/100 coverage with real SQLite fixtures (no mocks),
sibling
.md≥200 words per implementation file, rootREADME.md.
Impact
Affected dependents
| Package | Impact | Action |
|---|---|---|
@suaveplan/events |
New consumer | None — additive |
@suaveplan/error |
New consumer | None — additive |
@suaveplan/opentelemetry (via @opentelemetry/api) |
New consumer | None — additive |
@suaveplan/types |
New consumer of Clock, IdGenerator, LoggerLike |
None — additive |
(future) @suaveplan/api-plugin-messaging / inbound-message router |
Will consume IMessageAdapter as a MessageSourceAdapter |
No live consumer today; future package can depend on ^0.1.0 |
No genesis package currently imports @suaveplan/imessage-adapter, so
this change has zero blast radius beyond adding the package to the
manifest + flipping the STATUS row.
Out of scope
Nothing. Per feedback_no_deferrals_on_migration.md and AGENTS.md §2b, no "phase 2" is permitted. The whole upstream surface plus the
enterprise additions land in this change, feature-complete, with
100/100/100/100 coverage.