121 lines
7.2 KiB
Markdown
121 lines
7.2 KiB
Markdown
# @suaveplan/imessage-adapter - macOS iMessage SQLite chat.db watcher. Polls Apple's Messages database, decodes Apple-epoch / nanosecond timestamps, resolves handle + chat + attachment joins, and emits ParsedIMessage + InboundMessage envelopes. Structured IMESSAGE.* error registry, Zod-validated config boundary, AbortSignal cancellation, injectable SqliteReader seam (production: bun:sqlite, tests: real bun:sqlite chat.db fixture — no mocks), EventBus bridge atop @suaveplan/events, four OTel instruments.
|
|
|
|
macOS iMessage SQLite chat.db watcher and `MessageSourceAdapter`
|
|
implementation. Polls Apple's `~/Library/Messages/chat.db`, decodes
|
|
Apple-epoch / nanosecond timestamps, resolves handle + chat +
|
|
attachment joins, and emits `ParsedIMessage` and `InboundMessage`
|
|
envelopes. Server-tier (Bun); the `bun:sqlite` driver is loaded
|
|
lazily inside the production reader so the package type-checks +
|
|
lints on every host.
|
|
|
|
<!-- begin:auto-docs -->
|
|
## Documentation tree
|
|
|
|
- [adapter](./src/adapter/imessage-adapter.md) — `MessageSourceAdapter`-shaped wrapper around `IMessageWatcher` that projects each `ParsedIMessage` into the channel-agn…
|
|
- [imessage-adapter](./src/adapter/imessage-adapter.md) — `MessageSourceAdapter`-shaped wrapper around `IMessageWatcher` that projects each `ParsedIMessage` into the channel-agn…
|
|
- [error-codes](./src/error-codes/error-codes.md) — Defines the stable, fully-qualified error-code registry for `@suaveplan/imessage-adapter`. Every error class in this mo…
|
|
- [error-codes](./src/error-codes/error-codes.md) — Defines the stable, fully-qualified error-code registry for `@suaveplan/imessage-adapter`. Every error class in this mo…
|
|
- [fixtures](./src/fixtures/chat-db-fixture.md) — Builds a real `chat.db`-shaped SQLite database on a temp path using the structural `SqliteReader` seam. This is the fou…
|
|
- [chat-db-fixture](./src/fixtures/chat-db-fixture.md) — Builds a real `chat.db`-shaped SQLite database on a temp path using the structural `SqliteReader` seam. This is the fou…
|
|
- [otel](./src/otel/metrics.md) — Defines the four OpenTelemetry instruments emitted by `@suaveplan/imessage-adapter` and the helpers that record onto th…
|
|
- [metrics](./src/otel/metrics.md) — Defines the four OpenTelemetry instruments emitted by `@suaveplan/imessage-adapter` and the helpers that record onto th…
|
|
- [parser](./src/parser/imessage-parser.md) — Pure-function parser that turns Apple's chat.db row shapes into the `ParsedIMessage` projection consumed by every downs…
|
|
- [imessage-parser](./src/parser/imessage-parser.md) — Pure-function parser that turns Apple's chat.db row shapes into the `ParsedIMessage` projection consumed by every downs…
|
|
- [schemas](./src/schemas/schemas.md) — Provides Zod boundary schemas for `IMessageWatcherConfig` and `IMessageAdapterConfig`, plus the `parseWatcherConfig` /…
|
|
- [schemas](./src/schemas/schemas.md) — Provides Zod boundary schemas for `IMessageWatcherConfig` and `IMessageAdapterConfig`, plus the `parseWatcherConfig` /…
|
|
- [sqlite](./src/sqlite/bun-sqlite-reader.md) — Production-grade `SqliteReader` backed by Bun's built-in `bun:sqlite` driver. The reader is the single seam through whi…
|
|
- [bun-sqlite-reader](./src/sqlite/bun-sqlite-reader.md) — Production-grade `SqliteReader` backed by Bun's built-in `bun:sqlite` driver. The reader is the single seam through whi…
|
|
- [watcher](./src/watcher/imessage-watcher.md) — Polls Apple's `~/Library/Messages/chat.db` SQLite database on a configurable interval, projects new rows into `ParsedIM…
|
|
- [imessage-watcher](./src/watcher/imessage-watcher.md) — Polls Apple's `~/Library/Messages/chat.db` SQLite database on a configurable interval, projects new rows into `ParsedIM…
|
|
<!-- end:auto-docs -->
|
|
|
|
## Highlights
|
|
|
|
- **`IMessageWatcher`** — Zod-validated config boundary, AbortSignal
|
|
propagation, injectable `SqliteReader`, typed `EventBus` surface
|
|
alongside the upstream `on/off` listener API, monotonic
|
|
`lastRowId` cursor (settable + readable for resume), filters for
|
|
`includeFromMe` and `chatFilter`.
|
|
- **`IMessageAdapter`** — wraps the watcher and projects each
|
|
`ParsedIMessage` into a channel-agnostic `InboundMessage` with
|
|
normalised phone numbers and chat metadata.
|
|
- **`IMessageParser`** — pure-function parser for chat.db row shapes;
|
|
Apple-epoch decoding, attachment fallbacks, phone normalisation.
|
|
- **`createChatDbFixture`** — real chat.db-shaped SQLite database
|
|
builder used by tests so every test path runs against the same SQL
|
|
the production watcher executes — no mocks.
|
|
- **`bunSqliteReader` / `createBunSqliteReader`** — production
|
|
`SqliteReader` backed by Bun's `bun:sqlite`. Errors are wrapped in
|
|
`IMessageDbOpenFailedError`.
|
|
|
|
## Install
|
|
|
|
```bash
|
|
bun add @suaveplan/imessage-adapter
|
|
```
|
|
|
|
## Usage
|
|
|
|
```ts
|
|
import { IMessageAdapter } from "@suaveplan/imessage-adapter";
|
|
|
|
const adapter = new IMessageAdapter();
|
|
await adapter.initialize({
|
|
dbPath: "~/Library/Messages/chat.db",
|
|
pollIntervalMs: 5000,
|
|
includeFromMe: false,
|
|
});
|
|
|
|
for (const inbound of await adapter.poll()) {
|
|
console.log(inbound.sourceAddress, inbound.content);
|
|
adapter.markProcessed(inbound.id);
|
|
}
|
|
|
|
await adapter.close();
|
|
```
|
|
|
|
## Error registry (`IMESSAGE.*`)
|
|
|
|
| Code | Class | When |
|
|
|---|---|---|
|
|
| `IMESSAGE.DB_OPEN_FAILED` | `IMessageDbOpenFailedError` | `bun:sqlite` cannot open the chat.db path |
|
|
| `IMESSAGE.DB_QUERY_FAILED` | `IMessageDbQueryFailedError` | A poll-time SQL query throws |
|
|
| `IMESSAGE.WATCHER_NOT_RUNNING` | `IMessageWatcherNotRunningError` | `stop()` called while not running |
|
|
| `IMESSAGE.WATCHER_ALREADY_RUNNING` | `IMessageWatcherAlreadyRunningError` | `start()` called twice |
|
|
| `IMESSAGE.ADAPTER_NOT_INITIALIZED` | `IMessageAdapterNotInitializedError` | `poll()` before `initialize()` |
|
|
| `IMESSAGE.INVALID_CONFIG` | `IMessageInvalidConfigError` | Zod boundary rejected config; `context.issues` carries the path + reason |
|
|
| `IMESSAGE.POLL_ABORTED` | `IMessagePollAbortedError` | `AbortSignal` fired |
|
|
| `IMESSAGE.POLL_FAILED` | `IMessagePollFailedError` | Reserved for explicit `poll()` wrapping |
|
|
| `IMESSAGE.ATTACHMENT_PARSE_FAILED` | `IMessageAttachmentParseError` | Attachment row could not be parsed |
|
|
| `IMESSAGE.APPLE_DATE_PARSE_FAILED` | `IMessageAppleDateParseError` | Timestamp is NaN / non-finite |
|
|
|
|
## OpenTelemetry instruments
|
|
|
|
| Instrument | Kind | Attributes |
|
|
|---|---|---|
|
|
| `imessage.messages_emitted_total` | Counter | `chat_type`, `from_me` |
|
|
| `imessage.attachments_emitted_total` | Counter | `mime_family` |
|
|
| `imessage.poll_errors_total` | Counter | `code` |
|
|
| `imessage.poll_duration_ms` | Histogram | `outcome` (`ok`, `error`, `aborted`) |
|
|
|
|
Call `refreshIMessageMeters()` after `metrics.setGlobalMeterProvider`
|
|
to bind subsequent records to the chosen provider.
|
|
|
|
## Subpath exports
|
|
|
|
- `@suaveplan/imessage-adapter/error-codes`
|
|
- `@suaveplan/imessage-adapter/schemas`
|
|
- `@suaveplan/imessage-adapter/parser`
|
|
- `@suaveplan/imessage-adapter/watcher`
|
|
- `@suaveplan/imessage-adapter/adapter`
|
|
- `@suaveplan/imessage-adapter/sqlite/bun-sqlite-reader`
|
|
- `@suaveplan/imessage-adapter/otel`
|
|
- `@suaveplan/imessage-adapter/fixtures`
|
|
|
|
## Tier
|
|
|
|
Server-tier (Bun on macOS). The `bun:sqlite` import is reached via
|
|
`createRequire(import.meta.url)` so the static module graph never
|
|
attempts to resolve the specifier on a non-Bun runner.
|
|
|