@gobing-ai/ts-db 0.4.7 → 0.4.9
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 +38 -0
- package/dist/inbox-message-dao.d.ts +57 -1
- package/dist/inbox-message-dao.d.ts.map +1 -1
- package/dist/inbox-message-dao.js +71 -6
- package/dist/inbox.d.ts +1 -1
- package/dist/inbox.d.ts.map +1 -1
- package/dist/inbox.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/src/inbox-message-dao.ts +140 -6
- package/src/inbox.ts +11 -1
- package/src/index.ts +11 -1
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
|
-
|
|
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;
|
|
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;AAM7D,8CAA8C;AAC9C,MAAM,WAAW,qBAAqB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,uFAAuF;AACvF,MAAM,WAAW,qBAAqB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,+CAA+C;AAC/C,MAAM,WAAW,sBAAsB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,4CAA4C;AAC5C,MAAM,WAAW,mBAAmB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,+DAA+D;AAC/D,MAAM,MAAM,kBAAkB,GAAG;IAC7B,kBAAkB,EAAE,CAAC,MAAM,EAAE,qBAAqB,KAAK,IAAI,CAAC;IAC5D,kBAAkB,EAAE,CAAC,MAAM,EAAE,qBAAqB,KAAK,IAAI,CAAC;IAC5D,mBAAmB,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,IAAI,CAAC;IAC9D,gBAAgB,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;CAC3D,CAAC;AAEF;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IAClC,IAAI,CAAC,CAAC,SAAS,MAAM,kBAAkB,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI,EAAE,UAAU,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;CACxG;AAED,yCAAyC;AACzC,MAAM,WAAW,sBAAsB;IACnC,0EAA0E;IAC1E,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAClC;AAcD;;GAEG;AACH,qBAAa,eAAgB,SAAQ,SAAS,CAAC,OAAO,aAAa,EAAE,OAAO,aAAa,CAAC,EAAE,CAAC;IACzF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAwB;gBAEpC,OAAO,EAAE,SAAS,EAAE,OAAO,GAAE,sBAA2B;IAK9D,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;IAwB/F,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAmCnD,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiB3C,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBvD,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;IAI5D,0FAA0F;IAC1F,OAAO,CAAC,SAAS;CAapB"}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
75
|
+
const now = this.now();
|
|
76
|
+
const updated = await this.update(msgId, {
|
|
46
77
|
status: 'delivered',
|
|
47
|
-
deliveredAt:
|
|
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
|
package/dist/inbox.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inbox.d.ts","sourceRoot":"","sources":["../src/inbox.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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.
|
|
3
|
+
"version": "0.4.9",
|
|
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.
|
|
70
|
-
"@gobing-ai/ts-utils": "^0.4.
|
|
69
|
+
"@gobing-ai/ts-runtime": "^0.4.9",
|
|
70
|
+
"@gobing-ai/ts-utils": "^0.4.9"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
73
|
"drizzle-orm": ">=0.38.0",
|
package/src/inbox-message-dao.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
157
|
+
const now = this.now();
|
|
158
|
+
const updated = await this.update(msgId, {
|
|
58
159
|
status: 'delivered',
|
|
59
|
-
deliveredAt:
|
|
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 {
|
|
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 {
|
|
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,
|