@agimon-ai/doompi-session 0.0.1-alpha.1
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/LICENSE +21 -0
- package/README.md +44 -0
- package/dist/_virtual/_rolldown/runtime.cjs +23 -0
- package/dist/api-contracts.cjs +7 -0
- package/dist/api-contracts.d.cts +2 -0
- package/dist/api-contracts.d.mts +2 -0
- package/dist/api-contracts.mjs +2 -0
- package/dist/extensions/pi.cjs +20 -0
- package/dist/extensions/pi.cjs.map +1 -0
- package/dist/extensions/pi.d.cts +5 -0
- package/dist/extensions/pi.d.cts.map +1 -0
- package/dist/extensions/pi.d.mts +5 -0
- package/dist/extensions/pi.d.mts.map +1 -0
- package/dist/extensions/pi.mjs +15 -0
- package/dist/extensions/pi.mjs.map +1 -0
- package/dist/extensions/server.cjs +37 -0
- package/dist/extensions/server.cjs.map +1 -0
- package/dist/extensions/server.d.cts +5 -0
- package/dist/extensions/server.d.cts.map +1 -0
- package/dist/extensions/server.d.mts +5 -0
- package/dist/extensions/server.d.mts.map +1 -0
- package/dist/extensions/server.mjs +32 -0
- package/dist/extensions/server.mjs.map +1 -0
- package/dist/index.cjs +32 -0
- package/dist/index.d.cts +11 -0
- package/dist/index.d.mts +11 -0
- package/dist/index.mjs +10 -0
- package/dist/src/extensions/(backend)/api/session-peer/route.server.cjs +7 -0
- package/dist/src/extensions/(backend)/api/session-peer/route.server.cjs.map +1 -0
- package/dist/src/extensions/(backend)/api/session-peer/route.server.mjs +8 -0
- package/dist/src/extensions/(backend)/api/session-peer/route.server.mjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/root.cli.cjs +10 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/root.cli.cjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/root.cli.mjs +11 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/root.cli.mjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/root.server.cjs +101 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/root.server.cjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/root.server.mjs +99 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/root.server.mjs.map +1 -0
- package/dist/src/schemas/apiContracts.cjs +33 -0
- package/dist/src/schemas/apiContracts.cjs.map +1 -0
- package/dist/src/schemas/apiContracts.d.cts +52 -0
- package/dist/src/schemas/apiContracts.d.cts.map +1 -0
- package/dist/src/schemas/apiContracts.d.mts +52 -0
- package/dist/src/schemas/apiContracts.d.mts.map +1 -0
- package/dist/src/schemas/apiContracts.mjs +32 -0
- package/dist/src/schemas/apiContracts.mjs.map +1 -0
- package/dist/src/services/backgroundWorkService/index.cjs +79 -0
- package/dist/src/services/backgroundWorkService/index.cjs.map +1 -0
- package/dist/src/services/backgroundWorkService/index.d.cts +9 -0
- package/dist/src/services/backgroundWorkService/index.d.cts.map +1 -0
- package/dist/src/services/backgroundWorkService/index.d.mts +9 -0
- package/dist/src/services/backgroundWorkService/index.d.mts.map +1 -0
- package/dist/src/services/backgroundWorkService/index.mjs +78 -0
- package/dist/src/services/backgroundWorkService/index.mjs.map +1 -0
- package/dist/src/services/peerInbox/index.cjs +18 -0
- package/dist/src/services/peerInbox/index.cjs.map +1 -0
- package/dist/src/services/peerInbox/index.mjs +17 -0
- package/dist/src/services/peerInbox/index.mjs.map +1 -0
- package/dist/src/services/peerInboxApi/index.cjs +48 -0
- package/dist/src/services/peerInboxApi/index.cjs.map +1 -0
- package/dist/src/services/peerInboxApi/index.mjs +48 -0
- package/dist/src/services/peerInboxApi/index.mjs.map +1 -0
- package/dist/src/services/peerTransport/index.cjs +206 -0
- package/dist/src/services/peerTransport/index.cjs.map +1 -0
- package/dist/src/services/peerTransport/index.d.cts +49 -0
- package/dist/src/services/peerTransport/index.d.cts.map +1 -0
- package/dist/src/services/peerTransport/index.d.mts +49 -0
- package/dist/src/services/peerTransport/index.d.mts.map +1 -0
- package/dist/src/services/peerTransport/index.mjs +195 -0
- package/dist/src/services/peerTransport/index.mjs.map +1 -0
- package/dist/src/services/sessionDelivery/index.cjs +277 -0
- package/dist/src/services/sessionDelivery/index.cjs.map +1 -0
- package/dist/src/services/sessionDelivery/index.d.cts +10 -0
- package/dist/src/services/sessionDelivery/index.d.cts.map +1 -0
- package/dist/src/services/sessionDelivery/index.d.mts +10 -0
- package/dist/src/services/sessionDelivery/index.d.mts.map +1 -0
- package/dist/src/services/sessionDelivery/index.mjs +274 -0
- package/dist/src/services/sessionDelivery/index.mjs.map +1 -0
- package/dist/src/services/sessionDelivery/type.cjs +11 -0
- package/dist/src/services/sessionDelivery/type.cjs.map +1 -0
- package/dist/src/services/sessionDelivery/type.d.cts +73 -0
- package/dist/src/services/sessionDelivery/type.d.cts.map +1 -0
- package/dist/src/services/sessionDelivery/type.d.mts +73 -0
- package/dist/src/services/sessionDelivery/type.d.mts.map +1 -0
- package/dist/src/services/sessionDelivery/type.mjs +10 -0
- package/dist/src/services/sessionDelivery/type.mjs.map +1 -0
- package/dist/src/services/sessionDirectory/index.cjs +120 -0
- package/dist/src/services/sessionDirectory/index.cjs.map +1 -0
- package/dist/src/services/sessionDirectory/index.d.cts +7 -0
- package/dist/src/services/sessionDirectory/index.d.cts.map +1 -0
- package/dist/src/services/sessionDirectory/index.d.mts +7 -0
- package/dist/src/services/sessionDirectory/index.d.mts.map +1 -0
- package/dist/src/services/sessionDirectory/index.mjs +120 -0
- package/dist/src/services/sessionDirectory/index.mjs.map +1 -0
- package/dist/src/services/sessionDirectory/type.d.cts +54 -0
- package/dist/src/services/sessionDirectory/type.d.cts.map +1 -0
- package/dist/src/services/sessionDirectory/type.d.mts +54 -0
- package/dist/src/services/sessionDirectory/type.d.mts.map +1 -0
- package/dist/src/services/sessionGroups/index.cjs +105 -0
- package/dist/src/services/sessionGroups/index.cjs.map +1 -0
- package/dist/src/services/sessionGroups/index.d.cts +7 -0
- package/dist/src/services/sessionGroups/index.d.cts.map +1 -0
- package/dist/src/services/sessionGroups/index.d.mts +7 -0
- package/dist/src/services/sessionGroups/index.d.mts.map +1 -0
- package/dist/src/services/sessionGroups/index.mjs +104 -0
- package/dist/src/services/sessionGroups/index.mjs.map +1 -0
- package/dist/src/services/sessionGroups/type.cjs +11 -0
- package/dist/src/services/sessionGroups/type.cjs.map +1 -0
- package/dist/src/services/sessionGroups/type.d.cts +32 -0
- package/dist/src/services/sessionGroups/type.d.cts.map +1 -0
- package/dist/src/services/sessionGroups/type.d.mts +32 -0
- package/dist/src/services/sessionGroups/type.d.mts.map +1 -0
- package/dist/src/services/sessionGroups/type.mjs +11 -0
- package/dist/src/services/sessionGroups/type.mjs.map +1 -0
- package/dist/src/services/sessionIntercom/index.cjs +89 -0
- package/dist/src/services/sessionIntercom/index.cjs.map +1 -0
- package/dist/src/services/sessionIntercom/index.d.cts +11 -0
- package/dist/src/services/sessionIntercom/index.d.cts.map +1 -0
- package/dist/src/services/sessionIntercom/index.d.mts +11 -0
- package/dist/src/services/sessionIntercom/index.d.mts.map +1 -0
- package/dist/src/services/sessionIntercom/index.mjs +85 -0
- package/dist/src/services/sessionIntercom/index.mjs.map +1 -0
- package/dist/src/services/sessionIntercom/type.cjs +16 -0
- package/dist/src/services/sessionIntercom/type.cjs.map +1 -0
- package/dist/src/services/sessionIntercom/type.d.cts +60 -0
- package/dist/src/services/sessionIntercom/type.d.cts.map +1 -0
- package/dist/src/services/sessionIntercom/type.d.mts +60 -0
- package/dist/src/services/sessionIntercom/type.d.mts.map +1 -0
- package/dist/src/services/sessionIntercom/type.mjs +14 -0
- package/dist/src/services/sessionIntercom/type.mjs.map +1 -0
- package/llms.txt +3 -0
- package/package.json +115 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
const require_type = require("./type.cjs");
|
|
2
|
+
let node_sqlite = require("node:sqlite");
|
|
3
|
+
//#region src/services/sessionDelivery/index.ts
|
|
4
|
+
const DELIVERY_EVENT = "doom/session-delivery/envelope";
|
|
5
|
+
const ACK_EVENT = "doom/session-delivery/ack";
|
|
6
|
+
const DEFAULT_DELIVERY = "prompt";
|
|
7
|
+
const INBOX_STATES = /* @__PURE__ */ new Set([
|
|
8
|
+
"accepted",
|
|
9
|
+
"admitting",
|
|
10
|
+
"admitted",
|
|
11
|
+
"recovery_required"
|
|
12
|
+
]);
|
|
13
|
+
function isRecord(value) {
|
|
14
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
function isMode(value) {
|
|
17
|
+
return value === "prompt" || value === "steer" || value === "followUp";
|
|
18
|
+
}
|
|
19
|
+
function metadataOf(value) {
|
|
20
|
+
if (!isRecord(value)) return void 0;
|
|
21
|
+
const metadata = {};
|
|
22
|
+
for (const [key, item] of Object.entries(value)) {
|
|
23
|
+
if (typeof item !== "string") return void 0;
|
|
24
|
+
metadata[key] = item;
|
|
25
|
+
}
|
|
26
|
+
return metadata;
|
|
27
|
+
}
|
|
28
|
+
function envelopeOf(value) {
|
|
29
|
+
if (!isRecord(value)) return void 0;
|
|
30
|
+
const metadata = metadataOf(value.metadata);
|
|
31
|
+
if (typeof value.deliveryId !== "string" || !value.deliveryId || typeof value.kind !== "string" || !value.kind || typeof value.prompt !== "string" || !isMode(value.delivery) || metadata === void 0) return;
|
|
32
|
+
return {
|
|
33
|
+
deliveryId: value.deliveryId,
|
|
34
|
+
kind: value.kind,
|
|
35
|
+
prompt: value.prompt,
|
|
36
|
+
delivery: value.delivery,
|
|
37
|
+
metadata
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function ackOf(value) {
|
|
41
|
+
if (!isRecord(value)) return void 0;
|
|
42
|
+
if (typeof value.deliveryId !== "string" || typeof value.state !== "string" || !INBOX_STATES.has(value.state)) return;
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function normalizedMetadata(metadata) {
|
|
46
|
+
const normalized = {};
|
|
47
|
+
for (const [key, value] of Object.entries(metadata ?? {})) if (value !== void 0) normalized[key] = value;
|
|
48
|
+
return normalized;
|
|
49
|
+
}
|
|
50
|
+
function matchesMetadata(entry, query) {
|
|
51
|
+
return Object.entries(query ?? {}).every(([key, value]) => value === void 0 || entry[key] === value);
|
|
52
|
+
}
|
|
53
|
+
function sameMetadata(left, right) {
|
|
54
|
+
return matchesMetadata(left, right) && matchesMetadata(right, left);
|
|
55
|
+
}
|
|
56
|
+
/** Opens and starts one durable delivery endpoint. */
|
|
57
|
+
function createSessionDeliveryService(options) {
|
|
58
|
+
if (!options.recipientKey.trim() || options.recipientKey !== options.recipientKey.trim()) throw new TypeError("Session delivery requires a trimmed, non-empty recipient key.");
|
|
59
|
+
const database = new node_sqlite.DatabaseSync(options.databasePath);
|
|
60
|
+
database.exec(`
|
|
61
|
+
PRAGMA journal_mode = WAL;
|
|
62
|
+
CREATE TABLE IF NOT EXISTS session_delivery_outbox (
|
|
63
|
+
delivery_id TEXT PRIMARY KEY,
|
|
64
|
+
recipient_key TEXT NOT NULL,
|
|
65
|
+
kind TEXT NOT NULL,
|
|
66
|
+
prompt TEXT NOT NULL,
|
|
67
|
+
delivery_mode TEXT NOT NULL,
|
|
68
|
+
metadata_json TEXT NOT NULL,
|
|
69
|
+
state TEXT NOT NULL,
|
|
70
|
+
recipient_state TEXT,
|
|
71
|
+
created_at INTEGER NOT NULL,
|
|
72
|
+
acknowledged_at INTEGER
|
|
73
|
+
);
|
|
74
|
+
CREATE TABLE IF NOT EXISTS session_delivery_inbox (
|
|
75
|
+
delivery_id TEXT PRIMARY KEY,
|
|
76
|
+
sender_key TEXT NOT NULL,
|
|
77
|
+
recipient_key TEXT NOT NULL,
|
|
78
|
+
kind TEXT NOT NULL,
|
|
79
|
+
prompt TEXT NOT NULL,
|
|
80
|
+
delivery_mode TEXT NOT NULL,
|
|
81
|
+
metadata_json TEXT NOT NULL,
|
|
82
|
+
state TEXT NOT NULL,
|
|
83
|
+
created_at INTEGER NOT NULL,
|
|
84
|
+
updated_at INTEGER NOT NULL,
|
|
85
|
+
consumed_at INTEGER
|
|
86
|
+
);
|
|
87
|
+
`);
|
|
88
|
+
database.prepare("UPDATE session_delivery_inbox SET state = 'recovery_required', updated_at = ? WHERE state = 'admitting'").run(Date.now());
|
|
89
|
+
let closed = false;
|
|
90
|
+
const scheduled = /* @__PURE__ */ new Set();
|
|
91
|
+
let admissionQueue = Promise.resolve();
|
|
92
|
+
const publishAck = (deliveryId, senderKey, state) => {
|
|
93
|
+
options.communication.publish(senderKey, ACK_EVENT, {
|
|
94
|
+
deliveryId,
|
|
95
|
+
state
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
const readInboxRow = (deliveryId) => database.prepare("SELECT * FROM session_delivery_inbox WHERE delivery_id = ?").get(deliveryId);
|
|
99
|
+
const scheduleAdmission = (deliveryId) => {
|
|
100
|
+
if (closed || scheduled.has(deliveryId)) return;
|
|
101
|
+
scheduled.add(deliveryId);
|
|
102
|
+
admissionQueue = admissionQueue.then(async () => {
|
|
103
|
+
try {
|
|
104
|
+
if (closed) return;
|
|
105
|
+
const row = readInboxRow(deliveryId);
|
|
106
|
+
if (!row || row.state !== "accepted") return;
|
|
107
|
+
database.prepare("UPDATE session_delivery_inbox SET state = 'admitting', updated_at = ? WHERE delivery_id = ?").run(Date.now(), deliveryId);
|
|
108
|
+
publishAck(deliveryId, row.sender_key, "admitting");
|
|
109
|
+
try {
|
|
110
|
+
await options.admitPrompt(row.prompt, row.delivery_mode);
|
|
111
|
+
} catch {
|
|
112
|
+
database.prepare("UPDATE session_delivery_inbox SET state = 'recovery_required', updated_at = ? WHERE delivery_id = ?").run(Date.now(), deliveryId);
|
|
113
|
+
publishAck(deliveryId, row.sender_key, "recovery_required");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
database.prepare("UPDATE session_delivery_inbox SET state = 'admitted', updated_at = ? WHERE delivery_id = ?").run(Date.now(), deliveryId);
|
|
117
|
+
publishAck(deliveryId, row.sender_key, "admitted");
|
|
118
|
+
} finally {
|
|
119
|
+
scheduled.delete(deliveryId);
|
|
120
|
+
}
|
|
121
|
+
}).catch(() => {
|
|
122
|
+
scheduled.delete(deliveryId);
|
|
123
|
+
try {
|
|
124
|
+
const row = readInboxRow(deliveryId);
|
|
125
|
+
if (row?.state !== "admitting") return;
|
|
126
|
+
database.prepare("UPDATE session_delivery_inbox SET state = 'recovery_required', updated_at = ? WHERE delivery_id = ?").run(Date.now(), deliveryId);
|
|
127
|
+
try {
|
|
128
|
+
publishAck(deliveryId, row.sender_key, "recovery_required");
|
|
129
|
+
} catch {}
|
|
130
|
+
} catch {}
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
const receiveEnvelope = (senderKey, value) => {
|
|
134
|
+
if (closed) return void 0;
|
|
135
|
+
const envelope = envelopeOf(value);
|
|
136
|
+
if (!envelope || !options.authorizePeer(senderKey, envelope.metadata)) return void 0;
|
|
137
|
+
database.prepare(`INSERT OR IGNORE INTO session_delivery_inbox
|
|
138
|
+
(delivery_id, sender_key, recipient_key, kind, prompt, delivery_mode, metadata_json, state, created_at, updated_at)
|
|
139
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'accepted', ?, ?)`).run(envelope.deliveryId, senderKey, options.recipientKey, envelope.kind, envelope.prompt, envelope.delivery, JSON.stringify(envelope.metadata), Date.now(), Date.now());
|
|
140
|
+
const current = readInboxRow(envelope.deliveryId);
|
|
141
|
+
if (!current) return void 0;
|
|
142
|
+
if (!(current.sender_key === senderKey && current.recipient_key === options.recipientKey && current.kind === envelope.kind && current.prompt === envelope.prompt && current.delivery_mode === envelope.delivery && sameMetadata(JSON.parse(current.metadata_json), envelope.metadata))) return void 0;
|
|
143
|
+
publishAck(envelope.deliveryId, current.sender_key, current.state);
|
|
144
|
+
if (current.state === "accepted") scheduleAdmission(envelope.deliveryId);
|
|
145
|
+
return current.state;
|
|
146
|
+
};
|
|
147
|
+
const receiveAck = (recipientKey, value) => {
|
|
148
|
+
const ack = ackOf(value);
|
|
149
|
+
if (closed || !ack) return;
|
|
150
|
+
database.prepare(`UPDATE session_delivery_outbox
|
|
151
|
+
SET state = 'acknowledged', recipient_state = ?, acknowledged_at = ?
|
|
152
|
+
WHERE delivery_id = ? AND recipient_key = ?`).run(ack.state, Date.now(), ack.deliveryId, recipientKey);
|
|
153
|
+
};
|
|
154
|
+
const publishOutbox = (deliveryId) => {
|
|
155
|
+
const row = database.prepare("SELECT * FROM session_delivery_outbox WHERE delivery_id = ? AND state = 'queued'").get(deliveryId);
|
|
156
|
+
if (!row) return;
|
|
157
|
+
const metadata = JSON.parse(row.metadata_json);
|
|
158
|
+
if (!options.authorizePeer(row.recipient_key, metadata)) return;
|
|
159
|
+
options.communication.publish(row.recipient_key, DELIVERY_EVENT, {
|
|
160
|
+
deliveryId: row.delivery_id,
|
|
161
|
+
kind: row.kind,
|
|
162
|
+
prompt: row.prompt,
|
|
163
|
+
delivery: row.delivery_mode,
|
|
164
|
+
metadata
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
const publishQueued = (recipientKey) => {
|
|
168
|
+
for (const row of database.prepare("SELECT delivery_id FROM session_delivery_outbox WHERE recipient_key = ? AND state = 'queued'").all(recipientKey)) publishOutbox(row.delivery_id);
|
|
169
|
+
};
|
|
170
|
+
const publishAllQueued = () => {
|
|
171
|
+
for (const row of database.prepare("SELECT delivery_id FROM session_delivery_outbox WHERE state = 'queued'").all()) publishOutbox(row.delivery_id);
|
|
172
|
+
};
|
|
173
|
+
const releaseEnvelope = options.communication.subscribe(DELIVERY_EVENT, receiveEnvelope);
|
|
174
|
+
const releaseAck = options.communication.subscribe(ACK_EVENT, receiveAck);
|
|
175
|
+
const releaseReady = options.communication.onPeerReady(publishQueued);
|
|
176
|
+
for (const row of database.prepare("SELECT delivery_id, sender_key FROM session_delivery_inbox WHERE state = 'recovery_required'").all()) publishAck(row.delivery_id, row.sender_key, "recovery_required");
|
|
177
|
+
for (const row of database.prepare("SELECT delivery_id FROM session_delivery_inbox WHERE state = 'accepted'").all()) scheduleAdmission(row.delivery_id);
|
|
178
|
+
publishAllQueued();
|
|
179
|
+
const retryTimer = setInterval(publishAllQueued, 1e3);
|
|
180
|
+
retryTimer.unref();
|
|
181
|
+
return Object.freeze({
|
|
182
|
+
recipientKey: options.recipientKey,
|
|
183
|
+
async deliver(request) {
|
|
184
|
+
if (closed) throw new Error("Session delivery service is closed.");
|
|
185
|
+
const deliveryId = request.deliveryId ?? crypto.randomUUID();
|
|
186
|
+
const mode = request.delivery ?? DEFAULT_DELIVERY;
|
|
187
|
+
if (!deliveryId.trim() || !request.recipientKey.trim() || !request.kind.trim() || !isMode(mode)) throw new TypeError("Delivery id, recipient key, kind, and mode must be valid.");
|
|
188
|
+
if (!options.authorizePeer(request.recipientKey, request.metadata)) throw new Error(`Session '${request.recipientKey}' is not an authorized communication peer.`);
|
|
189
|
+
const metadata = normalizedMetadata(request.metadata);
|
|
190
|
+
database.prepare(`INSERT OR IGNORE INTO session_delivery_outbox
|
|
191
|
+
(delivery_id, recipient_key, kind, prompt, delivery_mode, metadata_json, state, created_at)
|
|
192
|
+
VALUES (?, ?, ?, ?, ?, ?, 'queued', ?)`).run(deliveryId, request.recipientKey, request.kind, request.prompt, mode, JSON.stringify(metadata), Date.now());
|
|
193
|
+
const current = database.prepare("SELECT * FROM session_delivery_outbox WHERE delivery_id = ?").get(deliveryId);
|
|
194
|
+
if (!current || current.recipient_key !== request.recipientKey || current.kind !== request.kind || current.prompt !== request.prompt || current.delivery_mode !== mode || !sameMetadata(JSON.parse(current.metadata_json), metadata)) throw new TypeError(`Delivery id '${deliveryId}' is already bound to another envelope.`);
|
|
195
|
+
publishOutbox(deliveryId);
|
|
196
|
+
return { deliveryId };
|
|
197
|
+
},
|
|
198
|
+
async waitForAdmission(deliveryId, timeoutMs = 5e3) {
|
|
199
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
200
|
+
let state;
|
|
201
|
+
do {
|
|
202
|
+
state = database.prepare("SELECT recipient_state FROM session_delivery_outbox WHERE delivery_id = ?").get(deliveryId)?.recipient_state ?? void 0;
|
|
203
|
+
if (state === "admitted" || state === "recovery_required") return state;
|
|
204
|
+
if (Date.now() >= deadline) return state;
|
|
205
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(10, deadline - Date.now())));
|
|
206
|
+
} while (!closed);
|
|
207
|
+
return state;
|
|
208
|
+
},
|
|
209
|
+
receive(senderKey, kind, payload) {
|
|
210
|
+
if (kind === DELIVERY_EVENT) return receiveEnvelope(senderKey, payload);
|
|
211
|
+
if (kind === ACK_EVENT) {
|
|
212
|
+
receiveAck(senderKey, payload);
|
|
213
|
+
return "accepted";
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
inbox(query = {}) {
|
|
217
|
+
return database.prepare("SELECT * FROM session_delivery_inbox ORDER BY created_at, delivery_id").all().map((row) => {
|
|
218
|
+
const metadata = JSON.parse(row.metadata_json);
|
|
219
|
+
return {
|
|
220
|
+
deliveryId: row.delivery_id,
|
|
221
|
+
senderKey: row.sender_key,
|
|
222
|
+
recipientKey: row.recipient_key,
|
|
223
|
+
kind: row.kind,
|
|
224
|
+
prompt: row.prompt,
|
|
225
|
+
delivery: row.delivery_mode,
|
|
226
|
+
metadata,
|
|
227
|
+
state: row.state,
|
|
228
|
+
consumed: row.consumed_at !== null
|
|
229
|
+
};
|
|
230
|
+
}).filter((entry) => (query.kind === void 0 || entry.kind === query.kind) && (query.state === void 0 || entry.state === query.state) && (query.consumed === void 0 || entry.consumed === query.consumed) && matchesMetadata(entry.metadata, query.metadata));
|
|
231
|
+
},
|
|
232
|
+
consume(deliveryId) {
|
|
233
|
+
return database.prepare("UPDATE session_delivery_inbox SET consumed_at = ? WHERE delivery_id = ? AND state IN ('admitted', 'recovery_required') AND consumed_at IS NULL").run(Date.now(), deliveryId).changes === 1;
|
|
234
|
+
},
|
|
235
|
+
outbox(deliveryId) {
|
|
236
|
+
const row = database.prepare("SELECT delivery_id, recipient_key, kind, metadata_json, state, recipient_state FROM session_delivery_outbox WHERE delivery_id = ?").get(deliveryId);
|
|
237
|
+
if (!row) return void 0;
|
|
238
|
+
return {
|
|
239
|
+
deliveryId: row.delivery_id,
|
|
240
|
+
recipientKey: row.recipient_key,
|
|
241
|
+
kind: row.kind,
|
|
242
|
+
metadata: JSON.parse(row.metadata_json),
|
|
243
|
+
state: row.state,
|
|
244
|
+
...row.recipient_state === null ? {} : { recipientState: row.recipient_state }
|
|
245
|
+
};
|
|
246
|
+
},
|
|
247
|
+
async close() {
|
|
248
|
+
if (closed) return;
|
|
249
|
+
closed = true;
|
|
250
|
+
clearInterval(retryTimer);
|
|
251
|
+
releaseEnvelope();
|
|
252
|
+
releaseAck();
|
|
253
|
+
releaseReady();
|
|
254
|
+
try {
|
|
255
|
+
await admissionQueue;
|
|
256
|
+
} finally {
|
|
257
|
+
database.close();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
/** Installs a provider-owned endpoint in the current Cordis context. */
|
|
263
|
+
function provideSessionDeliveryService(context, options) {
|
|
264
|
+
const service = createSessionDeliveryService(options);
|
|
265
|
+
const unprovide = context.provide(require_type.DOOM_SESSION_DELIVERY_SERVICE, service);
|
|
266
|
+
return async () => {
|
|
267
|
+
unprovide();
|
|
268
|
+
await service.close();
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
//#endregion
|
|
272
|
+
exports.DOOM_SESSION_DELIVERY_SERVICE = require_type.DOOM_SESSION_DELIVERY_SERVICE;
|
|
273
|
+
exports.createSessionDeliveryService = createSessionDeliveryService;
|
|
274
|
+
exports.provideSessionDeliveryService = provideSessionDeliveryService;
|
|
275
|
+
exports.readDoomSessionDelivery = require_type.readDoomSessionDelivery;
|
|
276
|
+
|
|
277
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["DatabaseSync","DOOM_SESSION_DELIVERY_SERVICE"],"sources":["../../../../src/services/sessionDelivery/index.ts"],"sourcesContent":["import { DatabaseSync } from 'node:sqlite';\n\nimport type { Context } from '@deepseek-ai/cordis';\n\nimport {\n DOOM_SESSION_DELIVERY_SERVICE,\n type DoomSessionDeliveryInboxState,\n type DoomSessionDeliveryMetadata,\n type DoomSessionDeliveryMode,\n type DoomSessionDeliveryRequest,\n type DoomSessionDeliveryService,\n type DoomSessionInboxEntry,\n type DoomSessionInboxQuery,\n type DoomSessionOutboxEntry,\n type SessionDeliveryServiceOptions,\n} from './type';\n\nconst DELIVERY_EVENT = 'doom/session-delivery/envelope';\nconst ACK_EVENT = 'doom/session-delivery/ack';\nconst DEFAULT_DELIVERY: DoomSessionDeliveryMode = 'prompt';\nconst INBOX_STATES = new Set<DoomSessionDeliveryInboxState>(['accepted', 'admitting', 'admitted', 'recovery_required']);\n\ninterface DeliveryEnvelope {\n readonly deliveryId: string;\n readonly kind: string;\n readonly prompt: string;\n readonly delivery: DoomSessionDeliveryMode;\n readonly metadata: DoomSessionDeliveryMetadata;\n}\n\ninterface DeliveryAck {\n readonly deliveryId: string;\n readonly state: DoomSessionDeliveryInboxState;\n}\n\ninterface InboxRow {\n delivery_id: string;\n sender_key: string;\n recipient_key: string;\n kind: string;\n prompt: string;\n delivery_mode: DoomSessionDeliveryMode;\n metadata_json: string;\n state: DoomSessionDeliveryInboxState;\n consumed_at: number | null;\n}\n\ninterface OutboxRow {\n delivery_id: string;\n recipient_key: string;\n kind: string;\n prompt: string;\n delivery_mode: DoomSessionDeliveryMode;\n metadata_json: string;\n state: 'queued' | 'acknowledged';\n recipient_state: DoomSessionDeliveryInboxState | null;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isMode(value: unknown): value is DoomSessionDeliveryMode {\n return value === 'prompt' || value === 'steer' || value === 'followUp';\n}\n\nfunction metadataOf(value: unknown): DoomSessionDeliveryMetadata | undefined {\n if (!isRecord(value)) return undefined;\n const metadata: Record<string, string> = {};\n for (const [key, item] of Object.entries(value)) {\n if (typeof item !== 'string') return undefined;\n metadata[key] = item;\n }\n return metadata;\n}\n\nfunction envelopeOf(value: unknown): DeliveryEnvelope | undefined {\n if (!isRecord(value)) return undefined;\n const metadata = metadataOf(value.metadata);\n if (\n typeof value.deliveryId !== 'string' ||\n !value.deliveryId ||\n typeof value.kind !== 'string' ||\n !value.kind ||\n typeof value.prompt !== 'string' ||\n !isMode(value.delivery) ||\n metadata === undefined\n ) {\n return undefined;\n }\n return {\n deliveryId: value.deliveryId,\n kind: value.kind,\n prompt: value.prompt,\n delivery: value.delivery,\n metadata,\n };\n}\n\nfunction ackOf(value: unknown): DeliveryAck | undefined {\n if (!isRecord(value)) return undefined;\n if (\n typeof value.deliveryId !== 'string' ||\n typeof value.state !== 'string' ||\n !INBOX_STATES.has(value.state as DoomSessionDeliveryInboxState)\n ) {\n return undefined;\n }\n return value as unknown as DeliveryAck;\n}\n\nfunction normalizedMetadata(metadata: DoomSessionDeliveryMetadata | undefined): DoomSessionDeliveryMetadata {\n const normalized: Record<string, string> = {};\n for (const [key, value] of Object.entries(metadata ?? {})) {\n if (value !== undefined) normalized[key] = value;\n }\n return normalized;\n}\n\nfunction matchesMetadata(entry: DoomSessionDeliveryMetadata, query: DoomSessionDeliveryMetadata | undefined): boolean {\n return Object.entries(query ?? {}).every(([key, value]) => value === undefined || entry[key] === value);\n}\n\nfunction sameMetadata(left: DoomSessionDeliveryMetadata, right: DoomSessionDeliveryMetadata): boolean {\n return matchesMetadata(left, right) && matchesMetadata(right, left);\n}\n\n/** Opens and starts one durable delivery endpoint. */\nexport function createSessionDeliveryService(options: SessionDeliveryServiceOptions): DoomSessionDeliveryService {\n if (!options.recipientKey.trim() || options.recipientKey !== options.recipientKey.trim()) {\n throw new TypeError('Session delivery requires a trimmed, non-empty recipient key.');\n }\n const database = new DatabaseSync(options.databasePath);\n database.exec(`\n PRAGMA journal_mode = WAL;\n CREATE TABLE IF NOT EXISTS session_delivery_outbox (\n delivery_id TEXT PRIMARY KEY,\n recipient_key TEXT NOT NULL,\n kind TEXT NOT NULL,\n prompt TEXT NOT NULL,\n delivery_mode TEXT NOT NULL,\n metadata_json TEXT NOT NULL,\n state TEXT NOT NULL,\n recipient_state TEXT,\n created_at INTEGER NOT NULL,\n acknowledged_at INTEGER\n );\n CREATE TABLE IF NOT EXISTS session_delivery_inbox (\n delivery_id TEXT PRIMARY KEY,\n sender_key TEXT NOT NULL,\n recipient_key TEXT NOT NULL,\n kind TEXT NOT NULL,\n prompt TEXT NOT NULL,\n delivery_mode TEXT NOT NULL,\n metadata_json TEXT NOT NULL,\n state TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n consumed_at INTEGER\n );\n `);\n database\n .prepare(\"UPDATE session_delivery_inbox SET state = 'recovery_required', updated_at = ? WHERE state = 'admitting'\")\n .run(Date.now());\n\n let closed = false;\n const scheduled = new Set<string>();\n let admissionQueue = Promise.resolve();\n\n const publishAck = (deliveryId: string, senderKey: string, state: DoomSessionDeliveryInboxState): void => {\n options.communication.publish(senderKey, ACK_EVENT, { deliveryId, state } satisfies DeliveryAck);\n };\n\n const readInboxRow = (deliveryId: string): InboxRow | undefined =>\n database.prepare('SELECT * FROM session_delivery_inbox WHERE delivery_id = ?').get(deliveryId) as\n | InboxRow\n | undefined;\n\n const scheduleAdmission = (deliveryId: string): void => {\n if (closed || scheduled.has(deliveryId)) return;\n scheduled.add(deliveryId);\n admissionQueue = admissionQueue\n .then(async () => {\n try {\n if (closed) return;\n const row = readInboxRow(deliveryId);\n if (!row || row.state !== 'accepted') return;\n database\n .prepare(\"UPDATE session_delivery_inbox SET state = 'admitting', updated_at = ? WHERE delivery_id = ?\")\n .run(Date.now(), deliveryId);\n publishAck(deliveryId, row.sender_key, 'admitting');\n try {\n await options.admitPrompt(row.prompt, row.delivery_mode);\n } catch {\n database\n .prepare(\n \"UPDATE session_delivery_inbox SET state = 'recovery_required', updated_at = ? WHERE delivery_id = ?\",\n )\n .run(Date.now(), deliveryId);\n publishAck(deliveryId, row.sender_key, 'recovery_required');\n return;\n }\n database\n .prepare(\"UPDATE session_delivery_inbox SET state = 'admitted', updated_at = ? WHERE delivery_id = ?\")\n .run(Date.now(), deliveryId);\n publishAck(deliveryId, row.sender_key, 'admitted');\n } finally {\n scheduled.delete(deliveryId);\n }\n })\n .catch(() => {\n scheduled.delete(deliveryId);\n try {\n const row = readInboxRow(deliveryId);\n if (row?.state !== 'admitting') return;\n database\n .prepare(\n \"UPDATE session_delivery_inbox SET state = 'recovery_required', updated_at = ? WHERE delivery_id = ?\",\n )\n .run(Date.now(), deliveryId);\n try {\n publishAck(deliveryId, row.sender_key, 'recovery_required');\n } catch {\n // The durable state is authoritative; readiness replay can request another acknowledgement.\n }\n } catch {\n // A database failure remains recoverable on restart when the schema can be opened again.\n }\n });\n };\n\n const receiveEnvelope = (senderKey: string, value: unknown): DoomSessionDeliveryInboxState | undefined => {\n if (closed) return undefined;\n const envelope = envelopeOf(value);\n if (!envelope || !options.authorizePeer(senderKey, envelope.metadata)) return undefined;\n database\n .prepare(\n `INSERT OR IGNORE INTO session_delivery_inbox\n (delivery_id, sender_key, recipient_key, kind, prompt, delivery_mode, metadata_json, state, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, 'accepted', ?, ?)`,\n )\n .run(\n envelope.deliveryId,\n senderKey,\n options.recipientKey,\n envelope.kind,\n envelope.prompt,\n envelope.delivery,\n JSON.stringify(envelope.metadata),\n Date.now(),\n Date.now(),\n );\n const current = readInboxRow(envelope.deliveryId);\n if (!current) return undefined;\n const sameEnvelope =\n current.sender_key === senderKey &&\n current.recipient_key === options.recipientKey &&\n current.kind === envelope.kind &&\n current.prompt === envelope.prompt &&\n current.delivery_mode === envelope.delivery &&\n sameMetadata(JSON.parse(current.metadata_json) as DoomSessionDeliveryMetadata, envelope.metadata);\n if (!sameEnvelope) return undefined;\n publishAck(envelope.deliveryId, current.sender_key, current.state);\n if (current.state === 'accepted') scheduleAdmission(envelope.deliveryId);\n return current.state;\n };\n\n const receiveAck = (recipientKey: string, value: unknown): void => {\n const ack = ackOf(value);\n if (closed || !ack) return;\n database\n .prepare(\n `UPDATE session_delivery_outbox\n SET state = 'acknowledged', recipient_state = ?, acknowledged_at = ?\n WHERE delivery_id = ? AND recipient_key = ?`,\n )\n .run(ack.state, Date.now(), ack.deliveryId, recipientKey);\n };\n\n const publishOutbox = (deliveryId: string): void => {\n const row = database\n .prepare(\"SELECT * FROM session_delivery_outbox WHERE delivery_id = ? AND state = 'queued'\")\n .get(deliveryId) as OutboxRow | undefined;\n if (!row) return;\n const metadata = JSON.parse(row.metadata_json) as DoomSessionDeliveryMetadata;\n if (!options.authorizePeer(row.recipient_key, metadata)) return;\n options.communication.publish(row.recipient_key, DELIVERY_EVENT, {\n deliveryId: row.delivery_id,\n kind: row.kind,\n prompt: row.prompt,\n delivery: row.delivery_mode,\n metadata,\n } satisfies DeliveryEnvelope);\n };\n\n const publishQueued = (recipientKey: string): void => {\n for (const row of database\n .prepare(\"SELECT delivery_id FROM session_delivery_outbox WHERE recipient_key = ? AND state = 'queued'\")\n .all(recipientKey) as { delivery_id: string }[]) {\n publishOutbox(row.delivery_id);\n }\n };\n const publishAllQueued = (): void => {\n for (const row of database\n .prepare(\"SELECT delivery_id FROM session_delivery_outbox WHERE state = 'queued'\")\n .all() as { delivery_id: string }[]) {\n publishOutbox(row.delivery_id);\n }\n };\n const releaseEnvelope = options.communication.subscribe(DELIVERY_EVENT, receiveEnvelope);\n const releaseAck = options.communication.subscribe(ACK_EVENT, receiveAck);\n const releaseReady = options.communication.onPeerReady(publishQueued);\n\n for (const row of database\n .prepare(\"SELECT delivery_id, sender_key FROM session_delivery_inbox WHERE state = 'recovery_required'\")\n .all() as { delivery_id: string; sender_key: string }[]) {\n publishAck(row.delivery_id, row.sender_key, 'recovery_required');\n }\n for (const row of database\n .prepare(\"SELECT delivery_id FROM session_delivery_inbox WHERE state = 'accepted'\")\n .all() as {\n delivery_id: string;\n }[]) {\n scheduleAdmission(row.delivery_id);\n }\n publishAllQueued();\n // A publish only means the transport accepted the envelope. Repeat stable delivery\n // IDs until the durable acknowledgement arrives, including after a tunnel restart.\n const retryTimer = setInterval(publishAllQueued, 1_000);\n retryTimer.unref();\n\n return Object.freeze({\n recipientKey: options.recipientKey,\n async deliver(request: DoomSessionDeliveryRequest): Promise<{ deliveryId: string }> {\n if (closed) throw new Error('Session delivery service is closed.');\n const deliveryId = request.deliveryId ?? crypto.randomUUID();\n const mode = request.delivery ?? DEFAULT_DELIVERY;\n if (!deliveryId.trim() || !request.recipientKey.trim() || !request.kind.trim() || !isMode(mode)) {\n throw new TypeError('Delivery id, recipient key, kind, and mode must be valid.');\n }\n if (!options.authorizePeer(request.recipientKey, request.metadata)) {\n throw new Error(`Session '${request.recipientKey}' is not an authorized communication peer.`);\n }\n const metadata = normalizedMetadata(request.metadata);\n database\n .prepare(\n `INSERT OR IGNORE INTO session_delivery_outbox\n (delivery_id, recipient_key, kind, prompt, delivery_mode, metadata_json, state, created_at)\n VALUES (?, ?, ?, ?, ?, ?, 'queued', ?)`,\n )\n .run(\n deliveryId,\n request.recipientKey,\n request.kind,\n request.prompt,\n mode,\n JSON.stringify(metadata),\n Date.now(),\n );\n const current = database\n .prepare('SELECT * FROM session_delivery_outbox WHERE delivery_id = ?')\n .get(deliveryId) as OutboxRow | undefined;\n if (\n !current ||\n current.recipient_key !== request.recipientKey ||\n current.kind !== request.kind ||\n current.prompt !== request.prompt ||\n current.delivery_mode !== mode ||\n !sameMetadata(JSON.parse(current.metadata_json) as DoomSessionDeliveryMetadata, metadata)\n ) {\n throw new TypeError(`Delivery id '${deliveryId}' is already bound to another envelope.`);\n }\n publishOutbox(deliveryId);\n return { deliveryId };\n },\n async waitForAdmission(deliveryId: string, timeoutMs = 5_000): Promise<DoomSessionDeliveryInboxState | undefined> {\n const deadline = Date.now() + Math.max(0, timeoutMs);\n let state: DoomSessionDeliveryInboxState | undefined;\n do {\n const row = database\n .prepare('SELECT recipient_state FROM session_delivery_outbox WHERE delivery_id = ?')\n .get(deliveryId) as { recipient_state: DoomSessionDeliveryInboxState | null } | undefined;\n state = row?.recipient_state ?? undefined;\n if (state === 'admitted' || state === 'recovery_required') return state;\n if (Date.now() >= deadline) return state;\n await new Promise((resolve) => setTimeout(resolve, Math.min(10, deadline - Date.now())));\n } while (!closed);\n return state;\n },\n receive(senderKey: string, kind: string, payload: unknown) {\n if (kind === DELIVERY_EVENT) return receiveEnvelope(senderKey, payload);\n if (kind === ACK_EVENT) {\n receiveAck(senderKey, payload);\n return 'accepted';\n }\n return undefined;\n },\n inbox(query: DoomSessionInboxQuery = {}): readonly DoomSessionInboxEntry[] {\n const rows = database\n .prepare('SELECT * FROM session_delivery_inbox ORDER BY created_at, delivery_id')\n .all() as unknown as InboxRow[];\n return rows\n .map((row): DoomSessionInboxEntry => {\n const metadata = JSON.parse(row.metadata_json) as DoomSessionDeliveryMetadata;\n return {\n deliveryId: row.delivery_id,\n senderKey: row.sender_key,\n recipientKey: row.recipient_key,\n kind: row.kind,\n prompt: row.prompt,\n delivery: row.delivery_mode,\n metadata,\n state: row.state,\n consumed: row.consumed_at !== null,\n };\n })\n .filter(\n (entry) =>\n (query.kind === undefined || entry.kind === query.kind) &&\n (query.state === undefined || entry.state === query.state) &&\n (query.consumed === undefined || entry.consumed === query.consumed) &&\n matchesMetadata(entry.metadata, query.metadata),\n );\n },\n consume(deliveryId: string): boolean {\n const result = database\n .prepare(\n \"UPDATE session_delivery_inbox SET consumed_at = ? WHERE delivery_id = ? AND state IN ('admitted', 'recovery_required') AND consumed_at IS NULL\",\n )\n .run(Date.now(), deliveryId);\n return result.changes === 1;\n },\n outbox(deliveryId: string): DoomSessionOutboxEntry | undefined {\n const row = database\n .prepare(\n 'SELECT delivery_id, recipient_key, kind, metadata_json, state, recipient_state FROM session_delivery_outbox WHERE delivery_id = ?',\n )\n .get(deliveryId) as OutboxRow | undefined;\n if (!row) return undefined;\n return {\n deliveryId: row.delivery_id,\n recipientKey: row.recipient_key,\n kind: row.kind,\n metadata: JSON.parse(row.metadata_json) as DoomSessionDeliveryMetadata,\n state: row.state,\n ...(row.recipient_state === null ? {} : { recipientState: row.recipient_state }),\n };\n },\n async close(): Promise<void> {\n if (closed) return;\n closed = true;\n clearInterval(retryTimer);\n releaseEnvelope();\n releaseAck();\n releaseReady();\n try {\n await admissionQueue;\n } finally {\n database.close();\n }\n },\n });\n}\n\n/** Installs a provider-owned endpoint in the current Cordis context. */\nexport function provideSessionDeliveryService(\n context: Context,\n options: SessionDeliveryServiceOptions,\n): () => Promise<void> {\n const service = createSessionDeliveryService(options);\n const unprovide = context.provide(DOOM_SESSION_DELIVERY_SERVICE, service);\n return async () => {\n unprovide();\n await service.close();\n };\n}\n\nexport type {\n DoomSessionDeliveryInboxState,\n DoomSessionDeliveryMetadata,\n DoomSessionDeliveryMode,\n DoomSessionDeliveryRequest,\n DoomSessionDeliveryService,\n DoomSessionInboxEntry,\n DoomSessionInboxQuery,\n DoomSessionOutboxEntry,\n SessionDeliveryServiceOptions,\n} from './type';\nexport { DOOM_SESSION_DELIVERY_SERVICE, readDoomSessionDelivery } from './type';\n"],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { DOOM_SESSION_DELIVERY_SERVICE, DoomSessionDeliveryInboxState, DoomSessionDeliveryMetadata, DoomSessionDeliveryMode, DoomSessionDeliveryRequest, DoomSessionDeliveryService, DoomSessionInboxEntry, DoomSessionInboxQuery, DoomSessionOutboxEntry, SessionDeliveryServiceOptions, readDoomSessionDelivery } from "./type.cjs";
|
|
2
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
3
|
+
//#region src/services/sessionDelivery/index.d.ts
|
|
4
|
+
/** Opens and starts one durable delivery endpoint. */
|
|
5
|
+
export declare function createSessionDeliveryService(options: SessionDeliveryServiceOptions): DoomSessionDeliveryService;
|
|
6
|
+
/** Installs a provider-owned endpoint in the current Cordis context. */
|
|
7
|
+
export declare function provideSessionDeliveryService(context: Context, options: SessionDeliveryServiceOptions): () => Promise<void>;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { DOOM_SESSION_DELIVERY_SERVICE, type DoomSessionDeliveryInboxState, type DoomSessionDeliveryMetadata, type DoomSessionDeliveryMode, type DoomSessionDeliveryRequest, type DoomSessionDeliveryService, type DoomSessionInboxEntry, type DoomSessionInboxQuery, type DoomSessionOutboxEntry, type SessionDeliveryServiceOptions, readDoomSessionDelivery };
|
|
10
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../../../src/services/sessionDelivery/index.ts"],"mappings":";;;;wBAgIgB,6BAA6B,SAAS,gCAAgC;;wBAiVtE,8BACd,SAAS,SACT,SAAS,sCACF"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { DOOM_SESSION_DELIVERY_SERVICE, DoomSessionDeliveryInboxState, DoomSessionDeliveryMetadata, DoomSessionDeliveryMode, DoomSessionDeliveryRequest, DoomSessionDeliveryService, DoomSessionInboxEntry, DoomSessionInboxQuery, DoomSessionOutboxEntry, SessionDeliveryServiceOptions, readDoomSessionDelivery } from "./type.mjs";
|
|
2
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
3
|
+
//#region src/services/sessionDelivery/index.d.ts
|
|
4
|
+
/** Opens and starts one durable delivery endpoint. */
|
|
5
|
+
export declare function createSessionDeliveryService(options: SessionDeliveryServiceOptions): DoomSessionDeliveryService;
|
|
6
|
+
/** Installs a provider-owned endpoint in the current Cordis context. */
|
|
7
|
+
export declare function provideSessionDeliveryService(context: Context, options: SessionDeliveryServiceOptions): () => Promise<void>;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { DOOM_SESSION_DELIVERY_SERVICE, type DoomSessionDeliveryInboxState, type DoomSessionDeliveryMetadata, type DoomSessionDeliveryMode, type DoomSessionDeliveryRequest, type DoomSessionDeliveryService, type DoomSessionInboxEntry, type DoomSessionInboxQuery, type DoomSessionOutboxEntry, type SessionDeliveryServiceOptions, readDoomSessionDelivery };
|
|
10
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../../src/services/sessionDelivery/index.ts"],"mappings":";;;;wBAgIgB,6BAA6B,SAAS,gCAAgC;;wBAiVtE,8BACd,SAAS,SACT,SAAS,sCACF"}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { DOOM_SESSION_DELIVERY_SERVICE, readDoomSessionDelivery } from "./type.mjs";
|
|
2
|
+
import { DatabaseSync } from "node:sqlite";
|
|
3
|
+
//#region src/services/sessionDelivery/index.ts
|
|
4
|
+
const DELIVERY_EVENT = "doom/session-delivery/envelope";
|
|
5
|
+
const ACK_EVENT = "doom/session-delivery/ack";
|
|
6
|
+
const DEFAULT_DELIVERY = "prompt";
|
|
7
|
+
const INBOX_STATES = /* @__PURE__ */ new Set([
|
|
8
|
+
"accepted",
|
|
9
|
+
"admitting",
|
|
10
|
+
"admitted",
|
|
11
|
+
"recovery_required"
|
|
12
|
+
]);
|
|
13
|
+
function isRecord(value) {
|
|
14
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
function isMode(value) {
|
|
17
|
+
return value === "prompt" || value === "steer" || value === "followUp";
|
|
18
|
+
}
|
|
19
|
+
function metadataOf(value) {
|
|
20
|
+
if (!isRecord(value)) return void 0;
|
|
21
|
+
const metadata = {};
|
|
22
|
+
for (const [key, item] of Object.entries(value)) {
|
|
23
|
+
if (typeof item !== "string") return void 0;
|
|
24
|
+
metadata[key] = item;
|
|
25
|
+
}
|
|
26
|
+
return metadata;
|
|
27
|
+
}
|
|
28
|
+
function envelopeOf(value) {
|
|
29
|
+
if (!isRecord(value)) return void 0;
|
|
30
|
+
const metadata = metadataOf(value.metadata);
|
|
31
|
+
if (typeof value.deliveryId !== "string" || !value.deliveryId || typeof value.kind !== "string" || !value.kind || typeof value.prompt !== "string" || !isMode(value.delivery) || metadata === void 0) return;
|
|
32
|
+
return {
|
|
33
|
+
deliveryId: value.deliveryId,
|
|
34
|
+
kind: value.kind,
|
|
35
|
+
prompt: value.prompt,
|
|
36
|
+
delivery: value.delivery,
|
|
37
|
+
metadata
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function ackOf(value) {
|
|
41
|
+
if (!isRecord(value)) return void 0;
|
|
42
|
+
if (typeof value.deliveryId !== "string" || typeof value.state !== "string" || !INBOX_STATES.has(value.state)) return;
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function normalizedMetadata(metadata) {
|
|
46
|
+
const normalized = {};
|
|
47
|
+
for (const [key, value] of Object.entries(metadata ?? {})) if (value !== void 0) normalized[key] = value;
|
|
48
|
+
return normalized;
|
|
49
|
+
}
|
|
50
|
+
function matchesMetadata(entry, query) {
|
|
51
|
+
return Object.entries(query ?? {}).every(([key, value]) => value === void 0 || entry[key] === value);
|
|
52
|
+
}
|
|
53
|
+
function sameMetadata(left, right) {
|
|
54
|
+
return matchesMetadata(left, right) && matchesMetadata(right, left);
|
|
55
|
+
}
|
|
56
|
+
/** Opens and starts one durable delivery endpoint. */
|
|
57
|
+
function createSessionDeliveryService(options) {
|
|
58
|
+
if (!options.recipientKey.trim() || options.recipientKey !== options.recipientKey.trim()) throw new TypeError("Session delivery requires a trimmed, non-empty recipient key.");
|
|
59
|
+
const database = new DatabaseSync(options.databasePath);
|
|
60
|
+
database.exec(`
|
|
61
|
+
PRAGMA journal_mode = WAL;
|
|
62
|
+
CREATE TABLE IF NOT EXISTS session_delivery_outbox (
|
|
63
|
+
delivery_id TEXT PRIMARY KEY,
|
|
64
|
+
recipient_key TEXT NOT NULL,
|
|
65
|
+
kind TEXT NOT NULL,
|
|
66
|
+
prompt TEXT NOT NULL,
|
|
67
|
+
delivery_mode TEXT NOT NULL,
|
|
68
|
+
metadata_json TEXT NOT NULL,
|
|
69
|
+
state TEXT NOT NULL,
|
|
70
|
+
recipient_state TEXT,
|
|
71
|
+
created_at INTEGER NOT NULL,
|
|
72
|
+
acknowledged_at INTEGER
|
|
73
|
+
);
|
|
74
|
+
CREATE TABLE IF NOT EXISTS session_delivery_inbox (
|
|
75
|
+
delivery_id TEXT PRIMARY KEY,
|
|
76
|
+
sender_key TEXT NOT NULL,
|
|
77
|
+
recipient_key TEXT NOT NULL,
|
|
78
|
+
kind TEXT NOT NULL,
|
|
79
|
+
prompt TEXT NOT NULL,
|
|
80
|
+
delivery_mode TEXT NOT NULL,
|
|
81
|
+
metadata_json TEXT NOT NULL,
|
|
82
|
+
state TEXT NOT NULL,
|
|
83
|
+
created_at INTEGER NOT NULL,
|
|
84
|
+
updated_at INTEGER NOT NULL,
|
|
85
|
+
consumed_at INTEGER
|
|
86
|
+
);
|
|
87
|
+
`);
|
|
88
|
+
database.prepare("UPDATE session_delivery_inbox SET state = 'recovery_required', updated_at = ? WHERE state = 'admitting'").run(Date.now());
|
|
89
|
+
let closed = false;
|
|
90
|
+
const scheduled = /* @__PURE__ */ new Set();
|
|
91
|
+
let admissionQueue = Promise.resolve();
|
|
92
|
+
const publishAck = (deliveryId, senderKey, state) => {
|
|
93
|
+
options.communication.publish(senderKey, ACK_EVENT, {
|
|
94
|
+
deliveryId,
|
|
95
|
+
state
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
const readInboxRow = (deliveryId) => database.prepare("SELECT * FROM session_delivery_inbox WHERE delivery_id = ?").get(deliveryId);
|
|
99
|
+
const scheduleAdmission = (deliveryId) => {
|
|
100
|
+
if (closed || scheduled.has(deliveryId)) return;
|
|
101
|
+
scheduled.add(deliveryId);
|
|
102
|
+
admissionQueue = admissionQueue.then(async () => {
|
|
103
|
+
try {
|
|
104
|
+
if (closed) return;
|
|
105
|
+
const row = readInboxRow(deliveryId);
|
|
106
|
+
if (!row || row.state !== "accepted") return;
|
|
107
|
+
database.prepare("UPDATE session_delivery_inbox SET state = 'admitting', updated_at = ? WHERE delivery_id = ?").run(Date.now(), deliveryId);
|
|
108
|
+
publishAck(deliveryId, row.sender_key, "admitting");
|
|
109
|
+
try {
|
|
110
|
+
await options.admitPrompt(row.prompt, row.delivery_mode);
|
|
111
|
+
} catch {
|
|
112
|
+
database.prepare("UPDATE session_delivery_inbox SET state = 'recovery_required', updated_at = ? WHERE delivery_id = ?").run(Date.now(), deliveryId);
|
|
113
|
+
publishAck(deliveryId, row.sender_key, "recovery_required");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
database.prepare("UPDATE session_delivery_inbox SET state = 'admitted', updated_at = ? WHERE delivery_id = ?").run(Date.now(), deliveryId);
|
|
117
|
+
publishAck(deliveryId, row.sender_key, "admitted");
|
|
118
|
+
} finally {
|
|
119
|
+
scheduled.delete(deliveryId);
|
|
120
|
+
}
|
|
121
|
+
}).catch(() => {
|
|
122
|
+
scheduled.delete(deliveryId);
|
|
123
|
+
try {
|
|
124
|
+
const row = readInboxRow(deliveryId);
|
|
125
|
+
if (row?.state !== "admitting") return;
|
|
126
|
+
database.prepare("UPDATE session_delivery_inbox SET state = 'recovery_required', updated_at = ? WHERE delivery_id = ?").run(Date.now(), deliveryId);
|
|
127
|
+
try {
|
|
128
|
+
publishAck(deliveryId, row.sender_key, "recovery_required");
|
|
129
|
+
} catch {}
|
|
130
|
+
} catch {}
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
const receiveEnvelope = (senderKey, value) => {
|
|
134
|
+
if (closed) return void 0;
|
|
135
|
+
const envelope = envelopeOf(value);
|
|
136
|
+
if (!envelope || !options.authorizePeer(senderKey, envelope.metadata)) return void 0;
|
|
137
|
+
database.prepare(`INSERT OR IGNORE INTO session_delivery_inbox
|
|
138
|
+
(delivery_id, sender_key, recipient_key, kind, prompt, delivery_mode, metadata_json, state, created_at, updated_at)
|
|
139
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'accepted', ?, ?)`).run(envelope.deliveryId, senderKey, options.recipientKey, envelope.kind, envelope.prompt, envelope.delivery, JSON.stringify(envelope.metadata), Date.now(), Date.now());
|
|
140
|
+
const current = readInboxRow(envelope.deliveryId);
|
|
141
|
+
if (!current) return void 0;
|
|
142
|
+
if (!(current.sender_key === senderKey && current.recipient_key === options.recipientKey && current.kind === envelope.kind && current.prompt === envelope.prompt && current.delivery_mode === envelope.delivery && sameMetadata(JSON.parse(current.metadata_json), envelope.metadata))) return void 0;
|
|
143
|
+
publishAck(envelope.deliveryId, current.sender_key, current.state);
|
|
144
|
+
if (current.state === "accepted") scheduleAdmission(envelope.deliveryId);
|
|
145
|
+
return current.state;
|
|
146
|
+
};
|
|
147
|
+
const receiveAck = (recipientKey, value) => {
|
|
148
|
+
const ack = ackOf(value);
|
|
149
|
+
if (closed || !ack) return;
|
|
150
|
+
database.prepare(`UPDATE session_delivery_outbox
|
|
151
|
+
SET state = 'acknowledged', recipient_state = ?, acknowledged_at = ?
|
|
152
|
+
WHERE delivery_id = ? AND recipient_key = ?`).run(ack.state, Date.now(), ack.deliveryId, recipientKey);
|
|
153
|
+
};
|
|
154
|
+
const publishOutbox = (deliveryId) => {
|
|
155
|
+
const row = database.prepare("SELECT * FROM session_delivery_outbox WHERE delivery_id = ? AND state = 'queued'").get(deliveryId);
|
|
156
|
+
if (!row) return;
|
|
157
|
+
const metadata = JSON.parse(row.metadata_json);
|
|
158
|
+
if (!options.authorizePeer(row.recipient_key, metadata)) return;
|
|
159
|
+
options.communication.publish(row.recipient_key, DELIVERY_EVENT, {
|
|
160
|
+
deliveryId: row.delivery_id,
|
|
161
|
+
kind: row.kind,
|
|
162
|
+
prompt: row.prompt,
|
|
163
|
+
delivery: row.delivery_mode,
|
|
164
|
+
metadata
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
const publishQueued = (recipientKey) => {
|
|
168
|
+
for (const row of database.prepare("SELECT delivery_id FROM session_delivery_outbox WHERE recipient_key = ? AND state = 'queued'").all(recipientKey)) publishOutbox(row.delivery_id);
|
|
169
|
+
};
|
|
170
|
+
const publishAllQueued = () => {
|
|
171
|
+
for (const row of database.prepare("SELECT delivery_id FROM session_delivery_outbox WHERE state = 'queued'").all()) publishOutbox(row.delivery_id);
|
|
172
|
+
};
|
|
173
|
+
const releaseEnvelope = options.communication.subscribe(DELIVERY_EVENT, receiveEnvelope);
|
|
174
|
+
const releaseAck = options.communication.subscribe(ACK_EVENT, receiveAck);
|
|
175
|
+
const releaseReady = options.communication.onPeerReady(publishQueued);
|
|
176
|
+
for (const row of database.prepare("SELECT delivery_id, sender_key FROM session_delivery_inbox WHERE state = 'recovery_required'").all()) publishAck(row.delivery_id, row.sender_key, "recovery_required");
|
|
177
|
+
for (const row of database.prepare("SELECT delivery_id FROM session_delivery_inbox WHERE state = 'accepted'").all()) scheduleAdmission(row.delivery_id);
|
|
178
|
+
publishAllQueued();
|
|
179
|
+
const retryTimer = setInterval(publishAllQueued, 1e3);
|
|
180
|
+
retryTimer.unref();
|
|
181
|
+
return Object.freeze({
|
|
182
|
+
recipientKey: options.recipientKey,
|
|
183
|
+
async deliver(request) {
|
|
184
|
+
if (closed) throw new Error("Session delivery service is closed.");
|
|
185
|
+
const deliveryId = request.deliveryId ?? crypto.randomUUID();
|
|
186
|
+
const mode = request.delivery ?? DEFAULT_DELIVERY;
|
|
187
|
+
if (!deliveryId.trim() || !request.recipientKey.trim() || !request.kind.trim() || !isMode(mode)) throw new TypeError("Delivery id, recipient key, kind, and mode must be valid.");
|
|
188
|
+
if (!options.authorizePeer(request.recipientKey, request.metadata)) throw new Error(`Session '${request.recipientKey}' is not an authorized communication peer.`);
|
|
189
|
+
const metadata = normalizedMetadata(request.metadata);
|
|
190
|
+
database.prepare(`INSERT OR IGNORE INTO session_delivery_outbox
|
|
191
|
+
(delivery_id, recipient_key, kind, prompt, delivery_mode, metadata_json, state, created_at)
|
|
192
|
+
VALUES (?, ?, ?, ?, ?, ?, 'queued', ?)`).run(deliveryId, request.recipientKey, request.kind, request.prompt, mode, JSON.stringify(metadata), Date.now());
|
|
193
|
+
const current = database.prepare("SELECT * FROM session_delivery_outbox WHERE delivery_id = ?").get(deliveryId);
|
|
194
|
+
if (!current || current.recipient_key !== request.recipientKey || current.kind !== request.kind || current.prompt !== request.prompt || current.delivery_mode !== mode || !sameMetadata(JSON.parse(current.metadata_json), metadata)) throw new TypeError(`Delivery id '${deliveryId}' is already bound to another envelope.`);
|
|
195
|
+
publishOutbox(deliveryId);
|
|
196
|
+
return { deliveryId };
|
|
197
|
+
},
|
|
198
|
+
async waitForAdmission(deliveryId, timeoutMs = 5e3) {
|
|
199
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
200
|
+
let state;
|
|
201
|
+
do {
|
|
202
|
+
state = database.prepare("SELECT recipient_state FROM session_delivery_outbox WHERE delivery_id = ?").get(deliveryId)?.recipient_state ?? void 0;
|
|
203
|
+
if (state === "admitted" || state === "recovery_required") return state;
|
|
204
|
+
if (Date.now() >= deadline) return state;
|
|
205
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(10, deadline - Date.now())));
|
|
206
|
+
} while (!closed);
|
|
207
|
+
return state;
|
|
208
|
+
},
|
|
209
|
+
receive(senderKey, kind, payload) {
|
|
210
|
+
if (kind === DELIVERY_EVENT) return receiveEnvelope(senderKey, payload);
|
|
211
|
+
if (kind === ACK_EVENT) {
|
|
212
|
+
receiveAck(senderKey, payload);
|
|
213
|
+
return "accepted";
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
inbox(query = {}) {
|
|
217
|
+
return database.prepare("SELECT * FROM session_delivery_inbox ORDER BY created_at, delivery_id").all().map((row) => {
|
|
218
|
+
const metadata = JSON.parse(row.metadata_json);
|
|
219
|
+
return {
|
|
220
|
+
deliveryId: row.delivery_id,
|
|
221
|
+
senderKey: row.sender_key,
|
|
222
|
+
recipientKey: row.recipient_key,
|
|
223
|
+
kind: row.kind,
|
|
224
|
+
prompt: row.prompt,
|
|
225
|
+
delivery: row.delivery_mode,
|
|
226
|
+
metadata,
|
|
227
|
+
state: row.state,
|
|
228
|
+
consumed: row.consumed_at !== null
|
|
229
|
+
};
|
|
230
|
+
}).filter((entry) => (query.kind === void 0 || entry.kind === query.kind) && (query.state === void 0 || entry.state === query.state) && (query.consumed === void 0 || entry.consumed === query.consumed) && matchesMetadata(entry.metadata, query.metadata));
|
|
231
|
+
},
|
|
232
|
+
consume(deliveryId) {
|
|
233
|
+
return database.prepare("UPDATE session_delivery_inbox SET consumed_at = ? WHERE delivery_id = ? AND state IN ('admitted', 'recovery_required') AND consumed_at IS NULL").run(Date.now(), deliveryId).changes === 1;
|
|
234
|
+
},
|
|
235
|
+
outbox(deliveryId) {
|
|
236
|
+
const row = database.prepare("SELECT delivery_id, recipient_key, kind, metadata_json, state, recipient_state FROM session_delivery_outbox WHERE delivery_id = ?").get(deliveryId);
|
|
237
|
+
if (!row) return void 0;
|
|
238
|
+
return {
|
|
239
|
+
deliveryId: row.delivery_id,
|
|
240
|
+
recipientKey: row.recipient_key,
|
|
241
|
+
kind: row.kind,
|
|
242
|
+
metadata: JSON.parse(row.metadata_json),
|
|
243
|
+
state: row.state,
|
|
244
|
+
...row.recipient_state === null ? {} : { recipientState: row.recipient_state }
|
|
245
|
+
};
|
|
246
|
+
},
|
|
247
|
+
async close() {
|
|
248
|
+
if (closed) return;
|
|
249
|
+
closed = true;
|
|
250
|
+
clearInterval(retryTimer);
|
|
251
|
+
releaseEnvelope();
|
|
252
|
+
releaseAck();
|
|
253
|
+
releaseReady();
|
|
254
|
+
try {
|
|
255
|
+
await admissionQueue;
|
|
256
|
+
} finally {
|
|
257
|
+
database.close();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
/** Installs a provider-owned endpoint in the current Cordis context. */
|
|
263
|
+
function provideSessionDeliveryService(context, options) {
|
|
264
|
+
const service = createSessionDeliveryService(options);
|
|
265
|
+
const unprovide = context.provide(DOOM_SESSION_DELIVERY_SERVICE, service);
|
|
266
|
+
return async () => {
|
|
267
|
+
unprovide();
|
|
268
|
+
await service.close();
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
//#endregion
|
|
272
|
+
export { DOOM_SESSION_DELIVERY_SERVICE, createSessionDeliveryService, provideSessionDeliveryService, readDoomSessionDelivery };
|
|
273
|
+
|
|
274
|
+
//# sourceMappingURL=index.mjs.map
|