@gobing-ai/ts-db 0.4.62 → 0.4.64
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 +7 -0
- package/dist/inbox-message-dao.d.ts +17 -0
- package/dist/inbox-message-dao.d.ts.map +1 -1
- package/dist/inbox-message-dao.js +28 -1
- package/package.json +3 -3
- package/src/inbox-message-dao.ts +39 -1
package/README.md
CHANGED
|
@@ -265,6 +265,13 @@ const stats = await queue.getStats();
|
|
|
265
265
|
// → { pending: 5, processing: 2, completed: 100, failed: 3 }
|
|
266
266
|
```
|
|
267
267
|
|
|
268
|
+
**Delivery semantics:** the lease path is **at-least-once, never exactly-once**. A
|
|
269
|
+
claimed job whose lease expires (crash, lost renewal, stalled event loop) becomes
|
|
270
|
+
claimable again and will execute a second time; the attempt token only fences the
|
|
271
|
+
*ack* (a stale `markCompleted`/`markFailed` from the lost owner is ignored), it
|
|
272
|
+
does not fence execution. Handlers must therefore be idempotent or carry their own
|
|
273
|
+
dedup key — assume any claimed job can run more than once under failure.
|
|
274
|
+
|
|
268
275
|
### InboxMessageDao — durable inter-agent messages
|
|
269
276
|
|
|
270
277
|
`InboxMessageDao` persists directed messages for team-mode or multi-agent workflows. It lives on the
|
|
@@ -36,12 +36,20 @@ export interface FailedMessageDetail {
|
|
|
36
36
|
error: string;
|
|
37
37
|
timestamp: number;
|
|
38
38
|
}
|
|
39
|
+
/** Payload for a `message.requeued` event (one per row returned by `release`). */
|
|
40
|
+
export interface RequeuedMessageDetail {
|
|
41
|
+
id: string;
|
|
42
|
+
fromId: string | null;
|
|
43
|
+
toId: string;
|
|
44
|
+
timestamp: number;
|
|
45
|
+
}
|
|
39
46
|
/** Typed event map for inbox message lifecycle transitions. */
|
|
40
47
|
export type InboxMessageEvents = {
|
|
41
48
|
'message.enqueued': (detail: EnqueuedMessageDetail) => void;
|
|
42
49
|
'message.injected': (detail: InjectedMessageDetail) => void;
|
|
43
50
|
'message.delivered': (detail: DeliveredMessageDetail) => void;
|
|
44
51
|
'message.failed': (detail: FailedMessageDetail) => void;
|
|
52
|
+
'message.requeued': (detail: RequeuedMessageDetail) => void;
|
|
45
53
|
};
|
|
46
54
|
/**
|
|
47
55
|
* Zero-dependency structural event sink.
|
|
@@ -68,6 +76,15 @@ export declare class InboxMessageDao extends EntityDao<typeof inboxMessages, typ
|
|
|
68
76
|
}): Promise<InboxMessage[]>;
|
|
69
77
|
markDelivered(msgId: string): Promise<void>;
|
|
70
78
|
markFailed(msgId: string, error: string): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Release claimed (`injected`) messages back to `queued` so a drain whose
|
|
81
|
+
* invocation never started stays redeliverable. Only rows currently in
|
|
82
|
+
* `status = 'injected'` are touched — a `queued` or `delivered` row is
|
|
83
|
+
* left alone, so double-release is a no-op. `injectAttempts` and
|
|
84
|
+
* `injectError` are left untouched: the claim counter IS the budget.
|
|
85
|
+
* Emits one `message.requeued` event per released row.
|
|
86
|
+
*/
|
|
87
|
+
release(msgIds: string[]): Promise<number>;
|
|
71
88
|
inbox(toId: string, limit?: number, offset?: number): Promise<InboxMessage[]>;
|
|
72
89
|
countPending(toId: string): Promise<number>;
|
|
73
90
|
getById(id: string): Promise<InboxMessage | undefined>;
|
|
@@ -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;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;
|
|
1
|
+
{"version":3,"file":"inbox-message-dao.d.ts","sourceRoot":"","sources":["../src/inbox-message-dao.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sql } from 'drizzle-orm';
|
|
1
|
+
import { and, eq, inArray, sql } from 'drizzle-orm';
|
|
2
2
|
import { EntityDao } from './entity-dao.js';
|
|
3
3
|
import { inboxMessages } from './schema/inbox-messages.js';
|
|
4
4
|
function isPromiseLike(value) {
|
|
@@ -109,6 +109,33 @@ export class InboxMessageDao extends EntityDao {
|
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Release claimed (`injected`) messages back to `queued` so a drain whose
|
|
114
|
+
* invocation never started stays redeliverable. Only rows currently in
|
|
115
|
+
* `status = 'injected'` are touched — a `queued` or `delivered` row is
|
|
116
|
+
* left alone, so double-release is a no-op. `injectAttempts` and
|
|
117
|
+
* `injectError` are left untouched: the claim counter IS the budget.
|
|
118
|
+
* Emits one `message.requeued` event per released row.
|
|
119
|
+
*/
|
|
120
|
+
async release(msgIds) {
|
|
121
|
+
if (msgIds.length === 0)
|
|
122
|
+
return 0;
|
|
123
|
+
const now = this.now();
|
|
124
|
+
const rows = (await this.db
|
|
125
|
+
.update(inboxMessages)
|
|
126
|
+
.set({ status: 'queued', updatedAt: now })
|
|
127
|
+
.where(and(inArray(inboxMessages.id, msgIds), eq(inboxMessages.status, 'injected')))
|
|
128
|
+
.returning());
|
|
129
|
+
for (const row of rows) {
|
|
130
|
+
this.emitEvent('message.requeued', {
|
|
131
|
+
id: row.id,
|
|
132
|
+
fromId: row.fromId,
|
|
133
|
+
toId: row.toId,
|
|
134
|
+
timestamp: now,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return rows.length;
|
|
138
|
+
}
|
|
112
139
|
async inbox(toId, limit, offset) {
|
|
113
140
|
return this.list({
|
|
114
141
|
where: { col: inboxMessages.toId, op: 'eq', value: toId },
|
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.64",
|
|
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.63",
|
|
70
|
+
"@gobing-ai/ts-utils": "^0.4.63"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
73
|
"drizzle-orm": ">=0.38.0",
|
package/src/inbox-message-dao.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sql } from 'drizzle-orm';
|
|
1
|
+
import { and, eq, inArray, sql } from 'drizzle-orm';
|
|
2
2
|
import type { DbAdapter } from './adapter';
|
|
3
3
|
import type { UpdateReturningDb } from './drizzle-builders';
|
|
4
4
|
import { EntityDao } from './entity-dao';
|
|
@@ -48,12 +48,21 @@ export interface FailedMessageDetail {
|
|
|
48
48
|
timestamp: number;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/** Payload for a `message.requeued` event (one per row returned by `release`). */
|
|
52
|
+
export interface RequeuedMessageDetail {
|
|
53
|
+
id: string;
|
|
54
|
+
fromId: string | null;
|
|
55
|
+
toId: string;
|
|
56
|
+
timestamp: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
/** Typed event map for inbox message lifecycle transitions. */
|
|
52
60
|
export type InboxMessageEvents = {
|
|
53
61
|
'message.enqueued': (detail: EnqueuedMessageDetail) => void;
|
|
54
62
|
'message.injected': (detail: InjectedMessageDetail) => void;
|
|
55
63
|
'message.delivered': (detail: DeliveredMessageDetail) => void;
|
|
56
64
|
'message.failed': (detail: FailedMessageDetail) => void;
|
|
65
|
+
'message.requeued': (detail: RequeuedMessageDetail) => void;
|
|
57
66
|
};
|
|
58
67
|
|
|
59
68
|
/**
|
|
@@ -193,6 +202,35 @@ export class InboxMessageDao extends EntityDao<typeof inboxMessages, typeof inbo
|
|
|
193
202
|
}
|
|
194
203
|
}
|
|
195
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Release claimed (`injected`) messages back to `queued` so a drain whose
|
|
207
|
+
* invocation never started stays redeliverable. Only rows currently in
|
|
208
|
+
* `status = 'injected'` are touched — a `queued` or `delivered` row is
|
|
209
|
+
* left alone, so double-release is a no-op. `injectAttempts` and
|
|
210
|
+
* `injectError` are left untouched: the claim counter IS the budget.
|
|
211
|
+
* Emits one `message.requeued` event per released row.
|
|
212
|
+
*/
|
|
213
|
+
async release(msgIds: string[]): Promise<number> {
|
|
214
|
+
if (msgIds.length === 0) return 0;
|
|
215
|
+
const now = this.now();
|
|
216
|
+
const rows = (await (this.db as UpdateReturningDb)
|
|
217
|
+
.update(inboxMessages)
|
|
218
|
+
.set({ status: 'queued', updatedAt: now })
|
|
219
|
+
.where(and(inArray(inboxMessages.id, msgIds), eq(inboxMessages.status, 'injected')))
|
|
220
|
+
.returning()) as InboxMessage[];
|
|
221
|
+
|
|
222
|
+
for (const row of rows) {
|
|
223
|
+
this.emitEvent('message.requeued', {
|
|
224
|
+
id: row.id,
|
|
225
|
+
fromId: row.fromId,
|
|
226
|
+
toId: row.toId,
|
|
227
|
+
timestamp: now,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return rows.length;
|
|
232
|
+
}
|
|
233
|
+
|
|
196
234
|
async inbox(toId: string, limit?: number, offset?: number): Promise<InboxMessage[]> {
|
|
197
235
|
return this.list({
|
|
198
236
|
where: { col: inboxMessages.toId, op: 'eq', value: toId },
|