@gobing-ai/ts-db 0.4.8 → 0.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -286,6 +286,44 @@ const history = await inbox.inbox('coder', 20);
286
286
  const queued = await inbox.countPending('coder');
287
287
  ```
288
288
 
289
+
290
+ #### Structural event sink
291
+
292
+ `InboxMessageDao` accepts an optional `InboxMessageEventSink` via the second constructor
293
+ argument. The DAO depends only on the structural port (`InboxMessageEventSink` is a plain
294
+ interface); it does **not** import `@gobing-ai/ts-infra` or `EventBus`. This follows the
295
+ `ProcessEventSink` pattern from ADR-013.
296
+
297
+ ```ts
298
+ import { type BusLifecycleEvents, EventBus } from '@gobing-ai/ts-infra';
299
+ import { InboxMessageDao, type InboxMessageEvents } from '@gobing-ai/ts-db/inbox';
300
+
301
+ // Pass an EventBus directly as the structural sink — no adapter class needed.
302
+ const lifecycleBus = new EventBus<BusLifecycleEvents>();
303
+ const events = new EventBus<InboxMessageEvents>({ lifecycleBus });
304
+ const inbox = new InboxMessageDao(adapter, { events });
305
+ events.on('message.enqueued', (detail) => {
306
+ console.log(`message ${detail.id} from ${detail.fromId ?? 'operator'} to ${detail.toId}`);
307
+ });
308
+ ```
309
+
310
+ **Events emitted** (after the corresponding database mutation succeeds):
311
+
312
+ |Event|Trigger|Key payload fields|
313
+ |---|---|---|
314
+ |`message.enqueued`|`enqueue` succeeds|`id`, `fromId`, `toId`, `inReplyTo?`|
315
+ |`message.injected`|`drainPending` returns rows|`id`, `toId`, `inReplyTo?`, `injectAttempts`|
316
+ |`message.delivered`|`markDelivered` succeeds|`id`, `toId`, `deliveredAt`|
317
+ |`message.failed`|`markFailed` succeeds|`id`, `toId`, `error`|
318
+
319
+ Omitting the sink preserves the existing contract — no no-op object is allocated and no
320
+ event infrastructure is required.
321
+
322
+ Event delivery is best-effort and never changes the result of an already-committed mutation:
323
+ the DAO does not await observers and contains synchronous throws or rejected thenables. Lifecycle
324
+ observers can persist event payloads, so message bodies are deliberately excluded. The required
325
+ `message.failed.error` field may be persisted; callers must pass a pre-redacted error string.
326
+
289
327
  The `inbox_messages` table is additive and included in embedded migrations. It stores `from_id`,
290
328
  `to_id`, `body`, `status`, optional reply linkage, delivery timestamp, injection attempts, and the
291
329
  last injection error. The indexed access path is `(to_id, status)` for efficient pending drains.
@@ -3,11 +3,65 @@ import { EntityDao } from './entity-dao';
3
3
  import { inboxMessages } from './schema/inbox-messages';
4
4
  /** Row type inferred from the inbox_messages Drizzle schema. */
5
5
  export type InboxMessage = typeof inboxMessages.$inferSelect;
6
+ /** Payload for a `message.enqueued` event. */
7
+ export interface EnqueuedMessageDetail {
8
+ id: string;
9
+ fromId: string | null;
10
+ toId: string;
11
+ inReplyTo?: string;
12
+ timestamp: number;
13
+ }
14
+ /** Payload for a `message.injected` event (one per row returned by `drainPending`). */
15
+ export interface InjectedMessageDetail {
16
+ id: string;
17
+ fromId: string | null;
18
+ toId: string;
19
+ inReplyTo?: string | null;
20
+ injectAttempts: number;
21
+ timestamp: number;
22
+ }
23
+ /** Payload for a `message.delivered` event. */
24
+ export interface DeliveredMessageDetail {
25
+ id: string;
26
+ fromId: string | null;
27
+ toId: string;
28
+ deliveredAt: number;
29
+ timestamp: number;
30
+ }
31
+ /** Payload for a `message.failed` event. */
32
+ export interface FailedMessageDetail {
33
+ id: string;
34
+ fromId: string | null;
35
+ toId: string;
36
+ error: string;
37
+ timestamp: number;
38
+ }
39
+ /** Typed event map for inbox message lifecycle transitions. */
40
+ export type InboxMessageEvents = {
41
+ 'message.enqueued': (detail: EnqueuedMessageDetail) => void;
42
+ 'message.injected': (detail: InjectedMessageDetail) => void;
43
+ 'message.delivered': (detail: DeliveredMessageDetail) => void;
44
+ 'message.failed': (detail: FailedMessageDetail) => void;
45
+ };
46
+ /**
47
+ * Zero-dependency structural event sink.
48
+ * `EventBus<InboxMessageEvents>` is structurally compatible — pass it as
49
+ * `{ events: eventBus }` in `InboxMessageDaoOptions`.
50
+ */
51
+ export interface InboxMessageEventSink {
52
+ emit<K extends keyof InboxMessageEvents>(event: K, ...args: Parameters<InboxMessageEvents[K]>): void;
53
+ }
54
+ /** Options bag for `InboxMessageDao`. */
55
+ export interface InboxMessageDaoOptions {
56
+ /** Optional structural event sink for message lifecycle observability. */
57
+ events?: InboxMessageEventSink;
58
+ }
6
59
  /**
7
60
  * DAO for durable inter-agent inbox messages.
8
61
  */
9
62
  export declare class InboxMessageDao extends EntityDao<typeof inboxMessages, typeof inboxMessages.id> {
10
- constructor(adapter: DbAdapter);
63
+ private readonly events?;
64
+ constructor(adapter: DbAdapter, options?: InboxMessageDaoOptions);
11
65
  enqueue(fromId: string | null, toId: string, body: string, inReplyTo?: string): Promise<string>;
12
66
  drainPending(toId: string): Promise<InboxMessage[]>;
13
67
  markDelivered(msgId: string): Promise<void>;
@@ -15,5 +69,7 @@ export declare class InboxMessageDao extends EntityDao<typeof inboxMessages, typ
15
69
  inbox(toId: string, limit?: number, offset?: number): Promise<InboxMessage[]>;
16
70
  countPending(toId: string): Promise<number>;
17
71
  getById(id: string): Promise<InboxMessage | undefined>;
72
+ /** Event delivery is observational: sink failures never change a committed DAO result. */
73
+ private emitEvent;
18
74
  }
19
75
  //# sourceMappingURL=inbox-message-dao.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"inbox-message-dao.d.ts","sourceRoot":"","sources":["../src/inbox-message-dao.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAExD,gEAAgE;AAChE,MAAM,MAAM,YAAY,GAAG,OAAO,aAAa,CAAC,YAAY,CAAC;AAE7D;;GAEG;AACH,qBAAa,eAAgB,SAAQ,SAAS,CAAC,OAAO,aAAa,EAAE,OAAO,aAAa,CAAC,EAAE,CAAC;gBAC7E,OAAO,EAAE,SAAS;IAIxB,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAc/F,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAwBnD,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO3C,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOvD,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAS7E,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAS3C,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC;CAG/D"}
1
+ {"version":3,"file":"inbox-message-dao.d.ts","sourceRoot":"","sources":["../src/inbox-message-dao.ts"],"names":[],"mappings":"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"}
@@ -1,15 +1,26 @@
1
1
  import { sql } from 'drizzle-orm';
2
2
  import { EntityDao } from './entity-dao.js';
3
3
  import { inboxMessages } from './schema/inbox-messages.js';
4
+ function isPromiseLike(value) {
5
+ return (value !== null &&
6
+ (typeof value === 'object' || typeof value === 'function') &&
7
+ typeof value.then === 'function');
8
+ }
9
+ // ---------------------------------------------------------------------------
10
+ // DAO
11
+ // ---------------------------------------------------------------------------
4
12
  /**
5
13
  * DAO for durable inter-agent inbox messages.
6
14
  */
7
15
  export class InboxMessageDao extends EntityDao {
8
- constructor(adapter) {
16
+ events;
17
+ constructor(adapter, options = {}) {
9
18
  super(adapter, inboxMessages, [inboxMessages.id], 'inbox_messages');
19
+ this.events = options.events;
10
20
  }
11
21
  async enqueue(fromId, toId, body, inReplyTo) {
12
22
  const id = crypto.randomUUID();
23
+ const now = this.now();
13
24
  await this.create({
14
25
  id,
15
26
  fromId,
@@ -17,13 +28,22 @@ export class InboxMessageDao extends EntityDao {
17
28
  body,
18
29
  status: 'queued',
19
30
  injectAttempts: 0,
31
+ createdAt: now,
32
+ updatedAt: now,
33
+ ...(inReplyTo !== undefined ? { inReplyTo } : {}),
34
+ });
35
+ this.emitEvent('message.enqueued', {
36
+ id,
37
+ fromId,
38
+ toId,
20
39
  ...(inReplyTo !== undefined ? { inReplyTo } : {}),
40
+ timestamp: now,
21
41
  });
22
42
  return id;
23
43
  }
24
44
  async drainPending(toId) {
25
45
  const now = this.now();
26
- const rows = await this.db
46
+ const rows = (await this.db
27
47
  .update(inboxMessages)
28
48
  .set({
29
49
  status: 'injected',
@@ -38,20 +58,49 @@ export class InboxMessageDao extends EntityDao {
38
58
  ORDER BY created_at
39
59
  )
40
60
  AND ${inboxMessages.status} = 'queued'`)
41
- .returning();
61
+ .returning());
62
+ for (const row of rows) {
63
+ this.emitEvent('message.injected', {
64
+ id: row.id,
65
+ fromId: row.fromId,
66
+ toId: row.toId,
67
+ inReplyTo: row.inReplyTo,
68
+ injectAttempts: row.injectAttempts,
69
+ timestamp: now,
70
+ });
71
+ }
42
72
  return rows;
43
73
  }
44
74
  async markDelivered(msgId) {
45
- await this.update(msgId, {
75
+ const now = this.now();
76
+ const updated = await this.update(msgId, {
46
77
  status: 'delivered',
47
- deliveredAt: this.now(),
78
+ deliveredAt: now,
48
79
  });
80
+ if (updated) {
81
+ this.emitEvent('message.delivered', {
82
+ id: updated.id,
83
+ fromId: updated.fromId,
84
+ toId: updated.toId,
85
+ deliveredAt: now,
86
+ timestamp: now,
87
+ });
88
+ }
49
89
  }
50
90
  async markFailed(msgId, error) {
51
- await this.update(msgId, {
91
+ const updated = await this.update(msgId, {
52
92
  status: 'failed',
53
93
  injectError: error,
54
94
  });
95
+ if (updated) {
96
+ this.emitEvent('message.failed', {
97
+ id: updated.id,
98
+ fromId: updated.fromId,
99
+ toId: updated.toId,
100
+ error,
101
+ timestamp: updated.updatedAt,
102
+ });
103
+ }
55
104
  }
56
105
  async inbox(toId, limit, offset) {
57
106
  return this.list({
@@ -72,4 +121,20 @@ export class InboxMessageDao extends EntityDao {
72
121
  async getById(id) {
73
122
  return this.findBy(inboxMessages.id, id);
74
123
  }
124
+ /** Event delivery is observational: sink failures never change a committed DAO result. */
125
+ emitEvent(event, ...args) {
126
+ if (!this.events)
127
+ return;
128
+ try {
129
+ const pending = this.events.emit(event, ...args);
130
+ if (isPromiseLike(pending)) {
131
+ void Promise.resolve(pending).catch(() => {
132
+ // A rejected observer must not turn a committed mutation into an apparent failure.
133
+ });
134
+ }
135
+ }
136
+ catch {
137
+ // A throwing observer must not turn a committed mutation into an apparent failure.
138
+ }
139
+ }
75
140
  }
package/dist/inbox.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { type InboxMessage, InboxMessageDao } from './inbox-message-dao';
1
+ export { type DeliveredMessageDetail, type EnqueuedMessageDetail, type FailedMessageDetail, type InboxMessage, InboxMessageDao, type InboxMessageDaoOptions, type InboxMessageEventSink, type InboxMessageEvents, type InjectedMessageDetail, } from './inbox-message-dao';
2
2
  export { inboxMessages } from './schema/inbox-messages';
3
3
  //# sourceMappingURL=inbox.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"inbox.d.ts","sourceRoot":"","sources":["../src/inbox.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"inbox.d.ts","sourceRoot":"","sources":["../src/inbox.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,YAAY,EACjB,eAAe,EACf,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,GAC7B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC"}
package/dist/inbox.js CHANGED
@@ -1,2 +1,2 @@
1
- export { InboxMessageDao } from './inbox-message-dao.js';
1
+ export { InboxMessageDao, } from './inbox-message-dao.js';
2
2
  export { inboxMessages } from './schema/inbox-messages.js';
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export { D1Adapter } from './adapters/d1';
3
3
  export { BaseDao, type TxHandle } from './base-dao';
4
4
  export { type EmbeddedMigration, embeddedMigrations } from './embedded-migrations';
5
5
  export { type CursorListSpec, type DaoValidator, EntityDao, type EntityDaoOptions, type EntityListSpec, type EntityTable, type PKColumn, type PKValue, type SoftDeletableTable, } from './entity-dao';
6
- export { type InboxMessage, InboxMessageDao } from './inbox-message-dao';
6
+ export { type DeliveredMessageDetail, type EnqueuedMessageDetail, type FailedMessageDetail, type InboxMessage, InboxMessageDao, type InboxMessageDaoOptions, type InboxMessageEventSink, type InboxMessageEvents, type InjectedMessageDetail, } from './inbox-message-dao';
7
7
  export { applyMigrations, type MigrationLogger, type MigrationOptions } from './migrate';
8
8
  export { type ColRef, type ComparisonOp, compileOrderBy, compilePredicate, type ListSpec, type OrderTerm, type Predicate, } from './query-spec';
9
9
  export { QueueJobDao, type QueueJobRecord, type QueueStats } from './queue-job-dao';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AACnH,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEpD,OAAO,EAAE,KAAK,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AACnF,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,SAAS,EACT,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,KAAK,OAAO,EACZ,KAAK,kBAAkB,GAC1B,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,KAAK,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACzE,OAAO,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAC;AACzF,OAAO,EACH,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,cAAc,EACd,gBAAgB,EAChB,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,SAAS,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,KAAK,cAAc,EAAE,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACpF,YAAY,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AACnH,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEpD,OAAO,EAAE,KAAK,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AACnF,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,SAAS,EACT,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,KAAK,OAAO,EACZ,KAAK,kBAAkB,GAC1B,MAAM,cAAc,CAAC;AACtB,OAAO,EACH,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,YAAY,EACjB,eAAe,EACf,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,GAC7B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAC;AACzF,OAAO,EACH,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,cAAc,EACd,gBAAgB,EAChB,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,SAAS,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,KAAK,cAAc,EAAE,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACpF,YAAY,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ export { D1Adapter } from './adapters/d1.js';
3
3
  export { BaseDao } from './base-dao.js';
4
4
  export { embeddedMigrations } from './embedded-migrations.js';
5
5
  export { EntityDao, } from './entity-dao.js';
6
- export { InboxMessageDao } from './inbox-message-dao.js';
6
+ export { InboxMessageDao, } from './inbox-message-dao.js';
7
7
  export { applyMigrations } from './migrate.js';
8
8
  export { compileOrderBy, compilePredicate, } from './query-spec.js';
9
9
  export { QueueJobDao } from './queue-job-dao.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/ts-db",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "@gobing-ai/ts-db — a drizzle-free database facade: typed DAOs over Bun SQLite / Cloudflare D1, a small predicate query spec, single-source-of-truth tables, and migrations. Drizzle stays an internal detail.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -66,8 +66,8 @@
66
66
  "release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-db-v<version> && git push --tags' && exit 1"
67
67
  },
68
68
  "dependencies": {
69
- "@gobing-ai/ts-runtime": "^0.4.8",
70
- "@gobing-ai/ts-utils": "^0.4.8"
69
+ "@gobing-ai/ts-runtime": "^0.4.10",
70
+ "@gobing-ai/ts-utils": "^0.4.10"
71
71
  },
72
72
  "peerDependencies": {
73
73
  "drizzle-orm": ">=0.38.0",
@@ -7,16 +7,96 @@ import { inboxMessages } from './schema/inbox-messages';
7
7
  /** Row type inferred from the inbox_messages Drizzle schema. */
8
8
  export type InboxMessage = typeof inboxMessages.$inferSelect;
9
9
 
10
+ // ---------------------------------------------------------------------------
11
+ // Event types (structural port — no ts-infra dependency, per ADR-013 pattern)
12
+ // ---------------------------------------------------------------------------
13
+
14
+ /** Payload for a `message.enqueued` event. */
15
+ export interface EnqueuedMessageDetail {
16
+ id: string;
17
+ fromId: string | null;
18
+ toId: string;
19
+ inReplyTo?: string;
20
+ timestamp: number;
21
+ }
22
+
23
+ /** Payload for a `message.injected` event (one per row returned by `drainPending`). */
24
+ export interface InjectedMessageDetail {
25
+ id: string;
26
+ fromId: string | null;
27
+ toId: string;
28
+ inReplyTo?: string | null;
29
+ injectAttempts: number;
30
+ timestamp: number;
31
+ }
32
+
33
+ /** Payload for a `message.delivered` event. */
34
+ export interface DeliveredMessageDetail {
35
+ id: string;
36
+ fromId: string | null;
37
+ toId: string;
38
+ deliveredAt: number;
39
+ timestamp: number;
40
+ }
41
+
42
+ /** Payload for a `message.failed` event. */
43
+ export interface FailedMessageDetail {
44
+ id: string;
45
+ fromId: string | null;
46
+ toId: string;
47
+ error: string;
48
+ timestamp: number;
49
+ }
50
+
51
+ /** Typed event map for inbox message lifecycle transitions. */
52
+ export type InboxMessageEvents = {
53
+ 'message.enqueued': (detail: EnqueuedMessageDetail) => void;
54
+ 'message.injected': (detail: InjectedMessageDetail) => void;
55
+ 'message.delivered': (detail: DeliveredMessageDetail) => void;
56
+ 'message.failed': (detail: FailedMessageDetail) => void;
57
+ };
58
+
59
+ /**
60
+ * Zero-dependency structural event sink.
61
+ * `EventBus<InboxMessageEvents>` is structurally compatible — pass it as
62
+ * `{ events: eventBus }` in `InboxMessageDaoOptions`.
63
+ */
64
+ export interface InboxMessageEventSink {
65
+ emit<K extends keyof InboxMessageEvents>(event: K, ...args: Parameters<InboxMessageEvents[K]>): void;
66
+ }
67
+
68
+ /** Options bag for `InboxMessageDao`. */
69
+ export interface InboxMessageDaoOptions {
70
+ /** Optional structural event sink for message lifecycle observability. */
71
+ events?: InboxMessageEventSink;
72
+ }
73
+
74
+ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
75
+ return (
76
+ value !== null &&
77
+ (typeof value === 'object' || typeof value === 'function') &&
78
+ typeof (value as { then?: unknown }).then === 'function'
79
+ );
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // DAO
84
+ // ---------------------------------------------------------------------------
85
+
10
86
  /**
11
87
  * DAO for durable inter-agent inbox messages.
12
88
  */
13
89
  export class InboxMessageDao extends EntityDao<typeof inboxMessages, typeof inboxMessages.id> {
14
- constructor(adapter: DbAdapter) {
90
+ private readonly events?: InboxMessageEventSink;
91
+
92
+ constructor(adapter: DbAdapter, options: InboxMessageDaoOptions = {}) {
15
93
  super(adapter, inboxMessages, [inboxMessages.id], 'inbox_messages');
94
+ this.events = options.events;
16
95
  }
17
96
 
18
97
  async enqueue(fromId: string | null, toId: string, body: string, inReplyTo?: string): Promise<string> {
19
98
  const id = crypto.randomUUID();
99
+ const now = this.now();
20
100
  await this.create({
21
101
  id,
22
102
  fromId,
@@ -24,14 +104,23 @@ export class InboxMessageDao extends EntityDao<typeof inboxMessages, typeof inbo
24
104
  body,
25
105
  status: 'queued',
26
106
  injectAttempts: 0,
107
+ createdAt: now,
108
+ updatedAt: now,
27
109
  ...(inReplyTo !== undefined ? { inReplyTo } : {}),
28
110
  });
111
+ this.emitEvent('message.enqueued', {
112
+ id,
113
+ fromId,
114
+ toId,
115
+ ...(inReplyTo !== undefined ? { inReplyTo } : {}),
116
+ timestamp: now,
117
+ });
29
118
  return id;
30
119
  }
31
120
 
32
121
  async drainPending(toId: string): Promise<InboxMessage[]> {
33
122
  const now = this.now();
34
- const rows = await (this.db as UpdateReturningDb)
123
+ const rows = (await (this.db as UpdateReturningDb)
35
124
  .update(inboxMessages)
36
125
  .set({
37
126
  status: 'injected',
@@ -48,23 +137,53 @@ export class InboxMessageDao extends EntityDao<typeof inboxMessages, typeof inbo
48
137
  )
49
138
  AND ${inboxMessages.status} = 'queued'`,
50
139
  )
51
- .returning();
140
+ .returning()) as InboxMessage[];
141
+
142
+ for (const row of rows) {
143
+ this.emitEvent('message.injected', {
144
+ id: row.id,
145
+ fromId: row.fromId,
146
+ toId: row.toId,
147
+ inReplyTo: row.inReplyTo,
148
+ injectAttempts: row.injectAttempts,
149
+ timestamp: now,
150
+ });
151
+ }
52
152
 
53
153
  return rows as InboxMessage[];
54
154
  }
55
155
 
56
156
  async markDelivered(msgId: string): Promise<void> {
57
- await this.update(msgId, {
157
+ const now = this.now();
158
+ const updated = await this.update(msgId, {
58
159
  status: 'delivered',
59
- deliveredAt: this.now(),
160
+ deliveredAt: now,
60
161
  });
162
+ if (updated) {
163
+ this.emitEvent('message.delivered', {
164
+ id: updated.id,
165
+ fromId: updated.fromId,
166
+ toId: updated.toId,
167
+ deliveredAt: now,
168
+ timestamp: now,
169
+ });
170
+ }
61
171
  }
62
172
 
63
173
  async markFailed(msgId: string, error: string): Promise<void> {
64
- await this.update(msgId, {
174
+ const updated = await this.update(msgId, {
65
175
  status: 'failed',
66
176
  injectError: error,
67
177
  });
178
+ if (updated) {
179
+ this.emitEvent('message.failed', {
180
+ id: updated.id,
181
+ fromId: updated.fromId,
182
+ toId: updated.toId,
183
+ error,
184
+ timestamp: updated.updatedAt,
185
+ });
186
+ }
68
187
  }
69
188
 
70
189
  async inbox(toId: string, limit?: number, offset?: number): Promise<InboxMessage[]> {
@@ -88,4 +207,19 @@ export class InboxMessageDao extends EntityDao<typeof inboxMessages, typeof inbo
88
207
  async getById(id: string): Promise<InboxMessage | undefined> {
89
208
  return this.findBy(inboxMessages.id, id);
90
209
  }
210
+
211
+ /** Event delivery is observational: sink failures never change a committed DAO result. */
212
+ private emitEvent<K extends keyof InboxMessageEvents>(event: K, ...args: Parameters<InboxMessageEvents[K]>): void {
213
+ if (!this.events) return;
214
+ try {
215
+ const pending = this.events.emit(event, ...args) as unknown;
216
+ if (isPromiseLike(pending)) {
217
+ void Promise.resolve(pending).catch(() => {
218
+ // A rejected observer must not turn a committed mutation into an apparent failure.
219
+ });
220
+ }
221
+ } catch {
222
+ // A throwing observer must not turn a committed mutation into an apparent failure.
223
+ }
224
+ }
91
225
  }
package/src/inbox.ts CHANGED
@@ -1,2 +1,12 @@
1
- export { type InboxMessage, InboxMessageDao } from './inbox-message-dao';
1
+ export {
2
+ type DeliveredMessageDetail,
3
+ type EnqueuedMessageDetail,
4
+ type FailedMessageDetail,
5
+ type InboxMessage,
6
+ InboxMessageDao,
7
+ type InboxMessageDaoOptions,
8
+ type InboxMessageEventSink,
9
+ type InboxMessageEvents,
10
+ type InjectedMessageDetail,
11
+ } from './inbox-message-dao';
2
12
  export { inboxMessages } from './schema/inbox-messages';
package/src/index.ts CHANGED
@@ -14,7 +14,17 @@ export {
14
14
  type PKValue,
15
15
  type SoftDeletableTable,
16
16
  } from './entity-dao';
17
- export { type InboxMessage, InboxMessageDao } from './inbox-message-dao';
17
+ export {
18
+ type DeliveredMessageDetail,
19
+ type EnqueuedMessageDetail,
20
+ type FailedMessageDetail,
21
+ type InboxMessage,
22
+ InboxMessageDao,
23
+ type InboxMessageDaoOptions,
24
+ type InboxMessageEventSink,
25
+ type InboxMessageEvents,
26
+ type InjectedMessageDetail,
27
+ } from './inbox-message-dao';
18
28
  export { applyMigrations, type MigrationLogger, type MigrationOptions } from './migrate';
19
29
  export {
20
30
  type ColRef,