@dispatch-foo/protocol 0.0.0 → 0.2.0
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 +25 -0
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +3 -0
- package/dist/conformance-adapter.d.ts +1 -0
- package/dist/conformance-adapter.js +11 -0
- package/dist/conformance-adapter.js.map +1 -0
- package/dist/conformance.d.ts +243 -0
- package/dist/conformance.d.ts.map +1 -0
- package/dist/conformance.js +5 -0
- package/dist/constants-CRXF4f7g.js +89 -0
- package/dist/constants-CRXF4f7g.js.map +1 -0
- package/dist/constants-CUM7Sm5u.d.ts +36 -0
- package/dist/constants-CUM7Sm5u.d.ts.map +1 -0
- package/dist/federation/index.d.ts +360 -0
- package/dist/federation/index.d.ts.map +1 -0
- package/dist/federation/index.js +628 -0
- package/dist/federation/index.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/serve-CVPZbCpT.js +672 -0
- package/dist/serve-CVPZbCpT.js.map +1 -0
- package/dist/sqliteStore-BtFP1f8O.d.ts +376 -0
- package/dist/sqliteStore-BtFP1f8O.d.ts.map +1 -0
- package/dist/untrusted-DJNiucEX-tUd9XZzz.d.ts +388 -0
- package/dist/untrusted-DJNiucEX-tUd9XZzz.d.ts.map +1 -0
- package/dist/version-Ds8ZFZLh.js +2490 -0
- package/dist/version-Ds8ZFZLh.js.map +1 -0
- package/package.json +54 -1
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
import { d as SYSTEM_ADDRESS, o as GATE_TYPES, r as DELIVERY_STATES } from "./constants-CRXF4f7g.js";
|
|
2
|
+
import { C as parseAddress, c as createUlidFactory, g as gateOf, i as openMessagesDb, l as renderDigestLine, r as SqliteMessageStore, s as DeliveryEngine, t as PROTOCOL_VERSION, u as renderForAgent, v as validateSendInput, w as MessagingError } from "./version-Ds8ZFZLh.js";
|
|
3
|
+
import { queryAll } from "@dispatch-foo/core";
|
|
4
|
+
|
|
5
|
+
//#region src/conformance/errors.ts
|
|
6
|
+
var UnsupportedOp = class extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "UnsupportedOp";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/conformance/fields.ts
|
|
15
|
+
function malformed(where, why) {
|
|
16
|
+
throw new Error(`malformed vector field ${where}: ${why}`);
|
|
17
|
+
}
|
|
18
|
+
function isObject(value) {
|
|
19
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
function asObject(value, where) {
|
|
22
|
+
return isObject(value) ? value : malformed(where, "expected an object");
|
|
23
|
+
}
|
|
24
|
+
function text(obj, key, where) {
|
|
25
|
+
const value = obj[key];
|
|
26
|
+
return typeof value === "string" ? value : malformed(`${where}.${key}`, "expected a string");
|
|
27
|
+
}
|
|
28
|
+
function optionalText(obj, key, where) {
|
|
29
|
+
return obj[key] === void 0 ? void 0 : text(obj, key, where);
|
|
30
|
+
}
|
|
31
|
+
function texts(value, where) {
|
|
32
|
+
if (!Array.isArray(value)) return malformed(where, "expected a list");
|
|
33
|
+
return value.map((v, i) => typeof v === "string" ? v : malformed(`${where}[${i}]`, "expected a string"));
|
|
34
|
+
}
|
|
35
|
+
function bare(session) {
|
|
36
|
+
return session.startsWith("run:") ? session.slice(4) : session;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/conformance/host.ts
|
|
41
|
+
const DEFAULT_CLOCK = "2026-09-23T10:00:00.000Z";
|
|
42
|
+
const RULINGS = [
|
|
43
|
+
"allow",
|
|
44
|
+
"ask",
|
|
45
|
+
"deny"
|
|
46
|
+
];
|
|
47
|
+
function worldFrom(given) {
|
|
48
|
+
const world = {
|
|
49
|
+
clockMs: Date.parse(given.clock ?? DEFAULT_CLOCK),
|
|
50
|
+
liveRuns: /* @__PURE__ */ new Map(),
|
|
51
|
+
runTasks: /* @__PURE__ */ new Map(),
|
|
52
|
+
auxRuns: new Set((given.auxSessions ?? []).map(bare)),
|
|
53
|
+
implicit: new Map(Object.entries(given.implicit ?? {})),
|
|
54
|
+
owner: given.owner ?? "human:owner",
|
|
55
|
+
rulings: new Map(Object.entries(given.rulings ?? {})),
|
|
56
|
+
wakeResults: new Map(Object.entries(given.wakeResults ?? {}).map(([target, r]) => [target, r.ok ? {
|
|
57
|
+
ok: true,
|
|
58
|
+
runId: bare(r.session)
|
|
59
|
+
} : r])),
|
|
60
|
+
failPush: new Set((given.failPush ?? []).map(bare)),
|
|
61
|
+
failOnAnswered: given.failOnAnswered === true,
|
|
62
|
+
external: new Map(Object.entries(given.external ?? {}))
|
|
63
|
+
};
|
|
64
|
+
for (const item of given.workItems ?? []) {
|
|
65
|
+
for (const s of item.sessions ?? []) world.runTasks.set(bare(s), item.id);
|
|
66
|
+
if (item.liveSession !== void 0) {
|
|
67
|
+
world.liveRuns.set(item.id, bare(item.liveSession));
|
|
68
|
+
world.runTasks.set(bare(item.liveSession), item.id);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return world;
|
|
72
|
+
}
|
|
73
|
+
function rulingOf(value, where) {
|
|
74
|
+
return RULINGS.find((r) => r === value) ?? malformed(where, `expected one of ${RULINGS.join(", ")}`);
|
|
75
|
+
}
|
|
76
|
+
function wakeResultOf(value, where) {
|
|
77
|
+
const r = asObject(value, where);
|
|
78
|
+
if (r["ok"] === true) return {
|
|
79
|
+
ok: true,
|
|
80
|
+
runId: bare(text(r, "session", where))
|
|
81
|
+
};
|
|
82
|
+
if (r["ok"] === false) return {
|
|
83
|
+
ok: false,
|
|
84
|
+
reason: text(r, "reason", where)
|
|
85
|
+
};
|
|
86
|
+
return malformed(`${where}.ok`, "expected a boolean");
|
|
87
|
+
}
|
|
88
|
+
function applyWorld(world, change) {
|
|
89
|
+
const keys = Object.keys(change);
|
|
90
|
+
const key = keys[0];
|
|
91
|
+
if (keys.length !== 1 || key === void 0) throw new UnsupportedOp(`a world change names exactly one key, not ${keys.length}`);
|
|
92
|
+
const value = change[key];
|
|
93
|
+
const where = `world.${key}`;
|
|
94
|
+
switch (key) {
|
|
95
|
+
case "startSession": {
|
|
96
|
+
const s = asObject(value, where);
|
|
97
|
+
const item = text(s, "workItem", where);
|
|
98
|
+
const run = bare(text(s, "session", where));
|
|
99
|
+
world.liveRuns.set(item, run);
|
|
100
|
+
world.runTasks.set(run, item);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
case "endSession":
|
|
104
|
+
world.liveRuns.delete(text(asObject(value, where), "workItem", where));
|
|
105
|
+
return;
|
|
106
|
+
case "ruling": {
|
|
107
|
+
const r = asObject(value, where);
|
|
108
|
+
world.rulings.set(text(r, "target", where), rulingOf(r["ruling"], `${where}.ruling`));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
case "wakeResult": {
|
|
112
|
+
const r = asObject(value, where);
|
|
113
|
+
world.wakeResults.set(text(r, "target", where), wakeResultOf(r["result"], `${where}.result`));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
case "failPush":
|
|
117
|
+
world.failPush = new Set(texts(value, where).map(bare));
|
|
118
|
+
return;
|
|
119
|
+
case "failOnAnswered":
|
|
120
|
+
if (typeof value !== "boolean") malformed(where, "expected a boolean");
|
|
121
|
+
world.failOnAnswered = value;
|
|
122
|
+
return;
|
|
123
|
+
case "advanceMs":
|
|
124
|
+
if (typeof value !== "number" || !Number.isFinite(value)) malformed(where, "expected a number");
|
|
125
|
+
world.clockMs += value;
|
|
126
|
+
return;
|
|
127
|
+
default: throw new UnsupportedOp(`world change ${key} is not implemented`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
var ConformanceHost = class {
|
|
131
|
+
calls = [];
|
|
132
|
+
constructor(world) {
|
|
133
|
+
this.world = world;
|
|
134
|
+
}
|
|
135
|
+
liveRunFor(taskId) {
|
|
136
|
+
return this.world.liveRuns.get(taskId) ?? null;
|
|
137
|
+
}
|
|
138
|
+
isLiveRun(runId) {
|
|
139
|
+
return this.world.auxRuns.has(runId) || [...this.world.liveRuns.values()].includes(runId);
|
|
140
|
+
}
|
|
141
|
+
taskOfRun(runId) {
|
|
142
|
+
return this.world.runTasks.get(runId) ?? null;
|
|
143
|
+
}
|
|
144
|
+
push(runId, _rendered, message) {
|
|
145
|
+
this.calls.push({
|
|
146
|
+
hook: "push",
|
|
147
|
+
session: `run:${runId}`,
|
|
148
|
+
message: message.id
|
|
149
|
+
});
|
|
150
|
+
return this.settle(runId, "push failed");
|
|
151
|
+
}
|
|
152
|
+
notify(runId, _digest, message) {
|
|
153
|
+
this.calls.push({
|
|
154
|
+
hook: "notify",
|
|
155
|
+
session: `run:${runId}`,
|
|
156
|
+
message: message.id
|
|
157
|
+
});
|
|
158
|
+
return this.settle(runId, "notify failed");
|
|
159
|
+
}
|
|
160
|
+
notifyHuman(actor, message) {
|
|
161
|
+
this.calls.push({
|
|
162
|
+
hook: "notifyHuman",
|
|
163
|
+
actor,
|
|
164
|
+
message: message.id
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
wake(target, message) {
|
|
168
|
+
this.calls.push({
|
|
169
|
+
hook: "wake",
|
|
170
|
+
target,
|
|
171
|
+
message: message.id
|
|
172
|
+
});
|
|
173
|
+
return Promise.resolve(this.world.wakeResults.get(target) ?? {
|
|
174
|
+
ok: true,
|
|
175
|
+
runId: "r-00000f"
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
decide(request) {
|
|
179
|
+
this.calls.push({
|
|
180
|
+
hook: "decide",
|
|
181
|
+
target: request.target,
|
|
182
|
+
message: request.message.id
|
|
183
|
+
});
|
|
184
|
+
return this.world.rulings.get(request.target) ?? "deny";
|
|
185
|
+
}
|
|
186
|
+
owner() {
|
|
187
|
+
return this.world.owner;
|
|
188
|
+
}
|
|
189
|
+
implicitMembers(channel) {
|
|
190
|
+
return this.world.implicit.get(channel) ?? [];
|
|
191
|
+
}
|
|
192
|
+
onAnswered(question, answer) {
|
|
193
|
+
this.calls.push({
|
|
194
|
+
hook: "onAnswered",
|
|
195
|
+
question: question.id,
|
|
196
|
+
answer: answer.id
|
|
197
|
+
});
|
|
198
|
+
if (this.world.failOnAnswered) return Promise.reject(/* @__PURE__ */ new Error("effect failed"));
|
|
199
|
+
const gate = gateOf(question);
|
|
200
|
+
if (gate?.type === "wake" && answer.choice === "approve") this.calls.push({
|
|
201
|
+
hook: "wake",
|
|
202
|
+
target: gate.target,
|
|
203
|
+
message: gate.message
|
|
204
|
+
});
|
|
205
|
+
return Promise.resolve();
|
|
206
|
+
}
|
|
207
|
+
now() {
|
|
208
|
+
return new Date(this.world.clockMs);
|
|
209
|
+
}
|
|
210
|
+
external(address) {
|
|
211
|
+
return this.world.external.get(address) ?? null;
|
|
212
|
+
}
|
|
213
|
+
admitExternal(target, _sender, _replyTarget, message) {
|
|
214
|
+
this.calls.push({
|
|
215
|
+
hook: "admitExternal",
|
|
216
|
+
recipient: target.recipient,
|
|
217
|
+
message: message.id
|
|
218
|
+
});
|
|
219
|
+
return "deliver";
|
|
220
|
+
}
|
|
221
|
+
settle(runId, why) {
|
|
222
|
+
return this.world.failPush.has(runId) ? Promise.reject(new Error(why)) : Promise.resolve();
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
//#endregion
|
|
227
|
+
//#region src/conformance/seed.ts
|
|
228
|
+
function seededBytes(seed) {
|
|
229
|
+
let state = seed >>> 0;
|
|
230
|
+
return (n) => {
|
|
231
|
+
const out$1 = new Uint8Array(n);
|
|
232
|
+
for (let i = 0; i < n; i++) {
|
|
233
|
+
state = state + 1831565813 >>> 0;
|
|
234
|
+
let t = state;
|
|
235
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
236
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
237
|
+
out$1[i] = (t ^ t >>> 14) & 255;
|
|
238
|
+
}
|
|
239
|
+
return out$1;
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
//#endregion
|
|
244
|
+
//#region src/conformance/seedStore.ts
|
|
245
|
+
const VIAS = ["direct", "channel"];
|
|
246
|
+
function nullableText(obj, key, where) {
|
|
247
|
+
return obj[key] === null ? null : optionalText(obj, key, where) ?? null;
|
|
248
|
+
}
|
|
249
|
+
function refsOf(value, where) {
|
|
250
|
+
if (value === void 0) return [];
|
|
251
|
+
if (!Array.isArray(value)) return malformed(where, "expected a list");
|
|
252
|
+
return value.map((raw, i) => {
|
|
253
|
+
const at = `${where}[${i}]`;
|
|
254
|
+
const r = asObject(raw, at);
|
|
255
|
+
const ref = {
|
|
256
|
+
type: text(r, "type", at),
|
|
257
|
+
id: text(r, "id", at)
|
|
258
|
+
};
|
|
259
|
+
const commit = optionalText(r, "at", at);
|
|
260
|
+
if (commit !== void 0) ref.at = commit;
|
|
261
|
+
return ref;
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
function seededMessage(row, at, where) {
|
|
265
|
+
const id = text(row, "id", where);
|
|
266
|
+
const message = {
|
|
267
|
+
id,
|
|
268
|
+
thread: optionalText(row, "thread", where) ?? id,
|
|
269
|
+
replyTo: nullableText(row, "replyTo", where),
|
|
270
|
+
from: text(row, "from", where),
|
|
271
|
+
to: texts(row["to"], `${where}.to`),
|
|
272
|
+
kind: text(row, "kind", where),
|
|
273
|
+
body: text(row, "body", where),
|
|
274
|
+
refs: refsOf(row["refs"], `${where}.refs`),
|
|
275
|
+
urgent: row["urgent"] === true,
|
|
276
|
+
blocking: row["blocking"] === true,
|
|
277
|
+
wake: row["wake"] === "request" ? "request" : "none",
|
|
278
|
+
createdAt: optionalText(row, "createdAt", where) ?? at
|
|
279
|
+
};
|
|
280
|
+
const session = optionalText(row, "session", where);
|
|
281
|
+
if (session !== void 0) message.session = session;
|
|
282
|
+
const data = row["data"];
|
|
283
|
+
if (data !== void 0) message.data = data;
|
|
284
|
+
if (row["choices"] !== void 0) message.choices = texts(row["choices"], `${where}.choices`);
|
|
285
|
+
const choice = optionalText(row, "choice", where);
|
|
286
|
+
if (choice !== void 0) message.choice = choice;
|
|
287
|
+
return message;
|
|
288
|
+
}
|
|
289
|
+
function seededDelivery(row, at, where) {
|
|
290
|
+
const session = nullableText(row, "session", where);
|
|
291
|
+
const via = row["via"] === void 0 ? "direct" : row["via"];
|
|
292
|
+
return {
|
|
293
|
+
id: text(row, "id", where),
|
|
294
|
+
messageId: text(row, "message", where),
|
|
295
|
+
recipient: text(row, "recipient", where),
|
|
296
|
+
runId: session === null ? null : bare(session),
|
|
297
|
+
via: VIAS.find((v) => v === via) ?? malformed(`${where}.via`, `expected one of ${VIAS.join(", ")}`),
|
|
298
|
+
state: DELIVERY_STATES.find((s) => s === row["state"]) ?? malformed(`${where}.state`, `expected one of ${DELIVERY_STATES.join(", ")}`),
|
|
299
|
+
updatedAt: at
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function seedStore(store, given, world) {
|
|
303
|
+
const at = new Date(world.clockMs).toISOString();
|
|
304
|
+
store.transaction(() => {
|
|
305
|
+
for (const agent of given.agents ?? []) store.putAgent({
|
|
306
|
+
address: agent.address,
|
|
307
|
+
displayName: agent.address,
|
|
308
|
+
client: "conformance",
|
|
309
|
+
tokenHash: `seed:${agent.address}`,
|
|
310
|
+
status: agent.status,
|
|
311
|
+
muted: agent.muted === true,
|
|
312
|
+
approvedBy: world.owner,
|
|
313
|
+
createdAt: at
|
|
314
|
+
});
|
|
315
|
+
for (const channel of given.channels ?? []) {
|
|
316
|
+
store.ensureChannel(channel.name, at, false);
|
|
317
|
+
for (const member of channel.members) store.addMember(channel.name, member, at);
|
|
318
|
+
}
|
|
319
|
+
const rows = given.store ?? {};
|
|
320
|
+
(rows.messages ?? []).forEach((row, i) => store.insertMessage(seededMessage(row, at, `given.store.messages[${i}]`)));
|
|
321
|
+
(rows.deliveries ?? []).forEach((row, i) => store.insertDelivery(seededDelivery(row, at, `given.store.deliveries[${i}]`)));
|
|
322
|
+
for (const id of rows.appliedGates ?? []) store.markGateApplied(id, at);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
//#endregion
|
|
327
|
+
//#region src/conformance/adapter.ts
|
|
328
|
+
const CREATES = new Set([
|
|
329
|
+
"send",
|
|
330
|
+
"reply",
|
|
331
|
+
"close",
|
|
332
|
+
"a2a.inbound"
|
|
333
|
+
]);
|
|
334
|
+
const ROLE = /\$(?:s|gate|notice)[0-9]+\b/g;
|
|
335
|
+
const ORIGINS = ["local", "received"];
|
|
336
|
+
const AGENT_STATUSES = [
|
|
337
|
+
"pending",
|
|
338
|
+
"approved",
|
|
339
|
+
"revoked"
|
|
340
|
+
];
|
|
341
|
+
const REFERENCE_HELLO = {
|
|
342
|
+
dmp: "hello",
|
|
343
|
+
implementation: {
|
|
344
|
+
name: "dispatch-reference",
|
|
345
|
+
version: PROTOCOL_VERSION
|
|
346
|
+
},
|
|
347
|
+
classes: ["envelope", "host-core"],
|
|
348
|
+
profiles: ["core", "dispatch"],
|
|
349
|
+
capabilities: ["implicit-members", "muted-senders"],
|
|
350
|
+
systemAddress: SYSTEM_ADDRESS,
|
|
351
|
+
gateTypes: [...GATE_TYPES],
|
|
352
|
+
render: {
|
|
353
|
+
quotePrefix: "│ ",
|
|
354
|
+
header: "^\\[message from ",
|
|
355
|
+
hostLines: [
|
|
356
|
+
"^\\(in reply to ",
|
|
357
|
+
"^choices: ",
|
|
358
|
+
"^choice: ",
|
|
359
|
+
"^refs: ",
|
|
360
|
+
"^The sender is waiting\\. "
|
|
361
|
+
],
|
|
362
|
+
digestLead: "^📬(?: #[^ ]+ ·)? [^ ]+ from [^ ]+(?: \\(external\\))?: "
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
function mapStrings(value, f) {
|
|
366
|
+
if (typeof value === "string") return f(value);
|
|
367
|
+
if (Array.isArray(value)) return value.map((v) => mapStrings(v, f));
|
|
368
|
+
if (isObject(value)) return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, mapStrings(v, f)]));
|
|
369
|
+
return value;
|
|
370
|
+
}
|
|
371
|
+
var RoleBinder = class {
|
|
372
|
+
bound = /* @__PURE__ */ new Map();
|
|
373
|
+
created = /* @__PURE__ */ new Set();
|
|
374
|
+
seeded;
|
|
375
|
+
constructor(db, vector) {
|
|
376
|
+
this.db = db;
|
|
377
|
+
this.seeded = new Set((vector.given.store?.messages ?? []).flatMap((r) => typeof r["id"] === "string" ? [r["id"]] : []));
|
|
378
|
+
}
|
|
379
|
+
bindStep(step, id) {
|
|
380
|
+
this.bound.set(`$s${step}`, id);
|
|
381
|
+
this.created.add(id);
|
|
382
|
+
}
|
|
383
|
+
resolve(step) {
|
|
384
|
+
this.bindRaised();
|
|
385
|
+
return mapStrings(step, (s) => s.replace(ROLE, (token) => this.bound.get(token) ?? token));
|
|
386
|
+
}
|
|
387
|
+
bindRaised() {
|
|
388
|
+
let gates = 0;
|
|
389
|
+
let notices = 0;
|
|
390
|
+
const rows = queryAll(this.db, "SELECT id, kind FROM messages WHERE from_addr = ? ORDER BY rowid", [SYSTEM_ADDRESS]);
|
|
391
|
+
for (const { id, kind } of rows) {
|
|
392
|
+
if (this.created.has(id) || this.seeded.has(id)) continue;
|
|
393
|
+
if (kind === "question") this.bound.set(`$gate${++gates}`, id);
|
|
394
|
+
else if (kind === "notice") this.bound.set(`$notice${++notices}`, id);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
function gateTypesOf(options) {
|
|
399
|
+
return options.gateTypes ?? GATE_TYPES;
|
|
400
|
+
}
|
|
401
|
+
function senderOf(step) {
|
|
402
|
+
const as = asObject(step["as"], "as");
|
|
403
|
+
const canDecide = as["canDecide"];
|
|
404
|
+
if (typeof canDecide !== "boolean") malformed("as.canDecide", "expected a boolean");
|
|
405
|
+
return {
|
|
406
|
+
address: text(as, "address", "as"),
|
|
407
|
+
canDecide
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
function inputOf(step) {
|
|
411
|
+
return asObject(step["input"], "input");
|
|
412
|
+
}
|
|
413
|
+
function originOf(step) {
|
|
414
|
+
const origin = step["origin"] ?? "local";
|
|
415
|
+
return ORIGINS.find((o) => o === origin) ?? malformed("origin", `expected one of ${ORIGINS.join(", ")}`);
|
|
416
|
+
}
|
|
417
|
+
function sendResult(r) {
|
|
418
|
+
const result = {
|
|
419
|
+
message: r.message.id,
|
|
420
|
+
downgraded: r.downgraded
|
|
421
|
+
};
|
|
422
|
+
if (r.replayed === true) result["replayed"] = true;
|
|
423
|
+
return result;
|
|
424
|
+
}
|
|
425
|
+
function setAgentStatus(store, value) {
|
|
426
|
+
const where = "world.agentStatus";
|
|
427
|
+
const change = asObject(value, where);
|
|
428
|
+
const address = text(change, "address", where);
|
|
429
|
+
const status = AGENT_STATUSES.find((s) => s === change["status"]) ?? malformed(`${where}.status`, `expected one of ${AGENT_STATUSES.join(", ")}`);
|
|
430
|
+
const agent = store.getAgent(address) ?? malformed(`${where}.address`, "no such agent");
|
|
431
|
+
store.putAgent({
|
|
432
|
+
...agent,
|
|
433
|
+
status
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
function statesOf(step) {
|
|
437
|
+
if (step["states"] === void 0) return void 0;
|
|
438
|
+
return texts(step["states"], "states").map((s) => DELIVERY_STATES.find((d) => d === s) ?? malformed("states", `unknown delivery state ${s}`));
|
|
439
|
+
}
|
|
440
|
+
function createdId(result) {
|
|
441
|
+
if (result === void 0 || !isObject(result)) return null;
|
|
442
|
+
const id = result["message"];
|
|
443
|
+
return typeof id === "string" ? id : null;
|
|
444
|
+
}
|
|
445
|
+
async function runStep(step, ctx, options, record) {
|
|
446
|
+
const custom = options.ops?.[step.op];
|
|
447
|
+
if (custom !== void 0) return custom(step, ctx);
|
|
448
|
+
const { engine, store, host } = ctx;
|
|
449
|
+
switch (step.op) {
|
|
450
|
+
case "send": return sendResult(await engine.send(inputOf(step), senderOf(step), { origin: originOf(step) }));
|
|
451
|
+
case "reply": return sendResult(await engine.reply(text(step, "message", "reply"), inputOf(step), senderOf(step)));
|
|
452
|
+
case "close": return { message: engine.close(text(step, "question", "close"), text(step, "reason", "close")).id };
|
|
453
|
+
case "markRead": {
|
|
454
|
+
const recipient = text(step, "recipient", "markRead");
|
|
455
|
+
const found = store.deliveries({ messageId: text(step, "message", "markRead") }).find((d) => d.recipient === recipient);
|
|
456
|
+
if (found === void 0) throw new MessagingError("not-found", `no delivery to ${recipient}`, "recipient");
|
|
457
|
+
return { state: engine.markRead(found.id).state };
|
|
458
|
+
}
|
|
459
|
+
case "inbox": return { messages: engine.inbox(text(step, "recipient", "inbox"), statesOf(step)).map((x) => x.message.id) };
|
|
460
|
+
case "thread": return { messages: engine.thread(text(step, "thread", "thread")).messages.map((m) => m.id) };
|
|
461
|
+
case "canRead": return { readable: engine.canRead(text(step, "message", "canRead"), senderOf(step)) };
|
|
462
|
+
case "openBlocking": return { messages: engine.openBlocking().map((m) => m.id) };
|
|
463
|
+
case "join":
|
|
464
|
+
engine.join(text(step, "channel", "join"), text(step, "member", "join"));
|
|
465
|
+
return {};
|
|
466
|
+
case "leave": return { removed: engine.leave(text(step, "channel", "leave"), text(step, "member", "leave")) };
|
|
467
|
+
case "deliverHeld": return { deliveries: (await engine.deliverHeld(bare(text(step, "session", "deliverHeld")), text(step, "workItem", "deliverHeld"))).map((d) => ({
|
|
468
|
+
message: d.messageId,
|
|
469
|
+
recipient: d.recipient,
|
|
470
|
+
state: d.state
|
|
471
|
+
})) };
|
|
472
|
+
case "recover": {
|
|
473
|
+
const { retried, reverted, replayed, voided } = await engine.recover();
|
|
474
|
+
return {
|
|
475
|
+
retried,
|
|
476
|
+
reverted,
|
|
477
|
+
replayed,
|
|
478
|
+
voided
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
case "parseAddress": return { ...parseAddress(text(step, "input", "parseAddress")) };
|
|
482
|
+
case "validate": {
|
|
483
|
+
const as = senderOf(step);
|
|
484
|
+
const target = optionalText(step, "replyTarget", "validate");
|
|
485
|
+
validateSendInput(inputOf(step), as.address, as.canDecide, target === void 0 ? null : store.getMessage(target), {
|
|
486
|
+
gateTypes: new Set(gateTypesOf(options)),
|
|
487
|
+
origin: originOf(step)
|
|
488
|
+
});
|
|
489
|
+
return {};
|
|
490
|
+
}
|
|
491
|
+
case "render": {
|
|
492
|
+
const id = text(step, "message", "render");
|
|
493
|
+
const message = store.getMessage(id);
|
|
494
|
+
if (message === null) throw new MessagingError("not-found", `no message ${id}`, "message");
|
|
495
|
+
const external = step["external"] === true;
|
|
496
|
+
const rendered = step["form"] === "digest" ? renderDigestLine(message, external) : renderForAgent(message, external);
|
|
497
|
+
record(rendered);
|
|
498
|
+
return { text: rendered };
|
|
499
|
+
}
|
|
500
|
+
case "world": {
|
|
501
|
+
const change = asObject(step["change"], "change");
|
|
502
|
+
const keys = Object.keys(change);
|
|
503
|
+
if (keys.length === 1 && keys[0] === "agentStatus") setAgentStatus(store, change["agentStatus"]);
|
|
504
|
+
else applyWorld(host.world, change);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
default: throw new UnsupportedOp(`op ${step.op} is not implemented`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
function observedMessage(m) {
|
|
511
|
+
return {
|
|
512
|
+
id: m.id,
|
|
513
|
+
thread: m.thread,
|
|
514
|
+
replyTo: m.replyTo,
|
|
515
|
+
from: m.from,
|
|
516
|
+
to: m.to,
|
|
517
|
+
kind: m.kind,
|
|
518
|
+
body: m.body,
|
|
519
|
+
refs: m.refs.map((r) => r.at === void 0 ? {
|
|
520
|
+
type: r.type,
|
|
521
|
+
id: r.id
|
|
522
|
+
} : {
|
|
523
|
+
type: r.type,
|
|
524
|
+
id: r.id,
|
|
525
|
+
at: r.at
|
|
526
|
+
}),
|
|
527
|
+
...m.data === void 0 ? {} : { data: m.data },
|
|
528
|
+
urgent: m.urgent,
|
|
529
|
+
blocking: m.blocking,
|
|
530
|
+
...m.choices === void 0 ? {} : { choices: m.choices },
|
|
531
|
+
...m.choice === void 0 ? {} : { choice: m.choice },
|
|
532
|
+
wake: m.wake,
|
|
533
|
+
createdAt: m.createdAt
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
function observedDelivery(d) {
|
|
537
|
+
return {
|
|
538
|
+
id: d.id,
|
|
539
|
+
message: d.messageId,
|
|
540
|
+
recipient: d.recipient,
|
|
541
|
+
session: d.runId === null ? null : `run:${d.runId}`,
|
|
542
|
+
via: d.via,
|
|
543
|
+
state: d.state
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
function observe(id, db, store, host, steps, render) {
|
|
547
|
+
const messages = queryAll(db, "SELECT id FROM messages ORDER BY rowid").flatMap((row) => {
|
|
548
|
+
const m = store.getMessage(row.id);
|
|
549
|
+
return m === null ? [] : [observedMessage(m)];
|
|
550
|
+
});
|
|
551
|
+
const gateEffects = queryAll(db, "SELECT question_id FROM gate_effects ORDER BY question_id").map((row) => row.question_id);
|
|
552
|
+
return {
|
|
553
|
+
dmp: "observation",
|
|
554
|
+
id,
|
|
555
|
+
steps,
|
|
556
|
+
messages,
|
|
557
|
+
deliveries: store.deliveries({}).map(observedDelivery),
|
|
558
|
+
calls: host.calls,
|
|
559
|
+
gateEffects,
|
|
560
|
+
voided: queryAll(db, "SELECT answer_id FROM voided_answers ORDER BY answer_id").map((row) => row.answer_id),
|
|
561
|
+
channels: store.channels().map((c) => ({
|
|
562
|
+
name: c.name,
|
|
563
|
+
members: store.members(c.name)
|
|
564
|
+
})),
|
|
565
|
+
render
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
async function runVector(vector, options = {}) {
|
|
569
|
+
const db = openMessagesDb(":memory:");
|
|
570
|
+
try {
|
|
571
|
+
const store = new SqliteMessageStore(db);
|
|
572
|
+
const host = new ConformanceHost(worldFrom(vector.given));
|
|
573
|
+
seedStore(store, vector.given, host.world);
|
|
574
|
+
const engine = new DeliveryEngine({
|
|
575
|
+
store,
|
|
576
|
+
host,
|
|
577
|
+
limits: vector.given.limits,
|
|
578
|
+
newUlid: createUlidFactory(seededBytes(vector.given.seed ?? 1)),
|
|
579
|
+
gateTypes: gateTypesOf(options)
|
|
580
|
+
});
|
|
581
|
+
engine.subscribe((e) => {
|
|
582
|
+
if (e.type === "message") host.calls.push({
|
|
583
|
+
hook: "published",
|
|
584
|
+
message: e.message.id
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
const roles = new RoleBinder(db, vector);
|
|
588
|
+
const steps = [];
|
|
589
|
+
const render = [];
|
|
590
|
+
for (const [i, raw] of vector.when.entries()) {
|
|
591
|
+
const step = roles.resolve(raw);
|
|
592
|
+
try {
|
|
593
|
+
const result = await runStep(step, {
|
|
594
|
+
engine,
|
|
595
|
+
store,
|
|
596
|
+
host
|
|
597
|
+
}, options, (rendered) => render.push({
|
|
598
|
+
step: i + 1,
|
|
599
|
+
text: rendered
|
|
600
|
+
}));
|
|
601
|
+
steps.push(result === void 0 ? { ok: true } : {
|
|
602
|
+
ok: true,
|
|
603
|
+
result
|
|
604
|
+
});
|
|
605
|
+
const created = createdId(result);
|
|
606
|
+
if (CREATES.has(step.op) && created !== null) roles.bindStep(i + 1, created);
|
|
607
|
+
} catch (err) {
|
|
608
|
+
if (!(err instanceof MessagingError)) throw err;
|
|
609
|
+
steps.push({
|
|
610
|
+
ok: false,
|
|
611
|
+
error: err.field === void 0 ? { code: err.code } : {
|
|
612
|
+
code: err.code,
|
|
613
|
+
field: err.field
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return observe(vector.id, db, store, host, steps, render);
|
|
619
|
+
} finally {
|
|
620
|
+
db.close();
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
//#endregion
|
|
625
|
+
//#region src/conformance/serve.ts
|
|
626
|
+
const SEPARATORS = /[\u2028\u2029]/g;
|
|
627
|
+
function out(msg) {
|
|
628
|
+
const text$1 = JSON.stringify(msg).replace(SEPARATORS, (c) => `\\u${c.charCodeAt(0).toString(16)}`);
|
|
629
|
+
process.stdout.write(`${text$1}\n`);
|
|
630
|
+
}
|
|
631
|
+
function serveStdio(hello, run) {
|
|
632
|
+
async function handle(line) {
|
|
633
|
+
const msg = JSON.parse(line);
|
|
634
|
+
if (msg.dmp === "hello") {
|
|
635
|
+
out(hello);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (msg.dmp === "bye") process.exit(0);
|
|
639
|
+
if (msg.dmp !== "run" || msg.vector === void 0) return;
|
|
640
|
+
try {
|
|
641
|
+
out(await run(msg.vector));
|
|
642
|
+
} catch (err) {
|
|
643
|
+
if (!(err instanceof UnsupportedOp)) throw err;
|
|
644
|
+
out({
|
|
645
|
+
dmp: "unsupported",
|
|
646
|
+
id: msg.vector.id,
|
|
647
|
+
reason: err.message
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
let queue = Promise.resolve();
|
|
652
|
+
function enqueue(line) {
|
|
653
|
+
queue = queue.then(() => handle(line)).catch((err) => {
|
|
654
|
+
const why = err instanceof Error ? err.stack ?? err.message : String(err);
|
|
655
|
+
process.stderr.write(`${why}\n`, () => process.exit(1));
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
let buffered = "";
|
|
659
|
+
process.stdin.setEncoding("utf8");
|
|
660
|
+
process.stdin.on("data", (chunk) => {
|
|
661
|
+
const lines = (buffered + chunk).split("\n");
|
|
662
|
+
buffered = lines.pop() ?? "";
|
|
663
|
+
for (const line of lines) enqueue(line.endsWith("\r") ? line.slice(0, -1) : line);
|
|
664
|
+
});
|
|
665
|
+
process.stdin.on("end", () => {
|
|
666
|
+
if (buffered !== "") enqueue(buffered);
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
//#endregion
|
|
671
|
+
export { applyWorld as a, ConformanceHost as i, REFERENCE_HELLO as n, worldFrom as o, runVector as r, UnsupportedOp as s, serveStdio as t };
|
|
672
|
+
//# sourceMappingURL=serve-CVPZbCpT.js.map
|