borgmcp-server 0.1.1 → 0.1.5
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 +70 -22
- package/THIRD_PARTY_NOTICES.md +1 -0
- package/dist/cli.js +62 -11
- package/dist/cli.js.map +1 -1
- package/dist/coordination-api.d.ts +3 -1
- package/dist/coordination-api.js +476 -78
- package/dist/coordination-api.js.map +1 -1
- package/dist/credentials.d.ts +13 -7
- package/dist/credentials.js +77 -15
- package/dist/credentials.js.map +1 -1
- package/dist/debug-log.d.ts +76 -0
- package/dist/debug-log.js +124 -0
- package/dist/debug-log.js.map +1 -0
- package/dist/enrollment.d.ts +3 -11
- package/dist/enrollment.js +21 -60
- package/dist/enrollment.js.map +1 -1
- package/dist/https-server.d.ts +5 -20
- package/dist/https-server.js +108 -50
- package/dist/https-server.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/message-taxonomy.d.ts +38 -0
- package/dist/message-taxonomy.js +218 -0
- package/dist/message-taxonomy.js.map +1 -0
- package/dist/migrations.d.ts +4 -0
- package/dist/migrations.js +99 -0
- package/dist/migrations.js.map +1 -1
- package/dist/operator-error.d.ts +2 -1
- package/dist/operator-error.js +23 -5
- package/dist/operator-error.js.map +1 -1
- package/dist/role-section.d.ts +14 -0
- package/dist/role-section.js +87 -0
- package/dist/role-section.js.map +1 -0
- package/dist/service.d.ts +14 -4
- package/dist/service.js +243 -25
- package/dist/service.js.map +1 -1
- package/dist/start-options.d.ts +5 -1
- package/dist/start-options.js +17 -3
- package/dist/start-options.js.map +1 -1
- package/dist/store.d.ts +106 -8
- package/dist/store.js +772 -120
- package/dist/store.js.map +1 -1
- package/npm-shrinkwrap.json +6 -7
- package/package.json +2 -2
- package/src/cli.ts +61 -11
- package/src/coordination-api.ts +490 -72
- package/src/credentials.ts +103 -19
- package/src/debug-log.ts +165 -0
- package/src/enrollment.ts +32 -78
- package/src/https-server.ts +113 -78
- package/src/index.ts +1 -1
- package/src/message-taxonomy.ts +284 -0
- package/src/migrations.ts +102 -0
- package/src/operator-error.ts +40 -6
- package/src/role-section.ts +108 -0
- package/src/service.ts +268 -27
- package/src/start-options.ts +21 -4
- package/src/store.ts +887 -142
- package/dist/protocol-draft.d.ts +0 -2
- package/dist/protocol-draft.js +0 -31
- package/dist/protocol-draft.js.map +0 -1
- package/src/protocol-draft.ts +0 -32
package/src/coordination-api.ts
CHANGED
|
@@ -1,14 +1,35 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
ATTACH_PATH,
|
|
4
|
+
ErrorCode,
|
|
5
|
+
PROTOCOL_VERSION,
|
|
6
|
+
ProtocolContractError,
|
|
7
|
+
createProtocolEnvelope,
|
|
8
|
+
decodeAttachRequestEnvelope,
|
|
9
|
+
decodeProtocolEnvelope,
|
|
10
|
+
} from "borgmcp-shared/protocol";
|
|
1
11
|
import type { Principal } from "./principal.js";
|
|
2
12
|
import { assertServerDerivedPrincipal } from "./principal.js";
|
|
13
|
+
import { disabledDebugLogger, type DebugLogger } from "./debug-log.js";
|
|
14
|
+
import {
|
|
15
|
+
patchMessageTaxonomy,
|
|
16
|
+
resolveMessageRouting,
|
|
17
|
+
validateMessageTaxonomy,
|
|
18
|
+
} from "./message-taxonomy.js";
|
|
3
19
|
import type { CredentialAuthority } from "./credentials.js";
|
|
4
20
|
import {
|
|
5
21
|
CursorExpiredError,
|
|
6
|
-
|
|
22
|
+
AttachSessionRejectedError,
|
|
7
23
|
AccessDeniedError,
|
|
8
24
|
CreateCubeConflictError,
|
|
25
|
+
DefaultRoleRequiredError,
|
|
26
|
+
RoleConflictError,
|
|
27
|
+
RoleSectionConflictError,
|
|
9
28
|
ScopedStoreError,
|
|
10
29
|
StorageCapacityError,
|
|
11
|
-
type
|
|
30
|
+
type ActivityPage,
|
|
31
|
+
type ActivityStreamRecord,
|
|
32
|
+
type CubeRecord,
|
|
12
33
|
type LogCursor,
|
|
13
34
|
type StoreRuntime,
|
|
14
35
|
} from "./store.js";
|
|
@@ -19,6 +40,7 @@ export interface CoordinationRequest {
|
|
|
19
40
|
readonly principal: Principal;
|
|
20
41
|
readonly body?: unknown;
|
|
21
42
|
readonly cursor?: string;
|
|
43
|
+
readonly since?: string;
|
|
22
44
|
readonly signal: AbortSignal;
|
|
23
45
|
}
|
|
24
46
|
|
|
@@ -42,11 +64,20 @@ interface ReplayBarrier {
|
|
|
42
64
|
export class CoordinationApi {
|
|
43
65
|
readonly #runtime: StoreRuntime;
|
|
44
66
|
readonly #authority: CredentialAuthority;
|
|
67
|
+
readonly #debugLogger: DebugLogger;
|
|
68
|
+
readonly #streamHeartbeatMs: number;
|
|
45
69
|
#replayBarrier: ReplayBarrier | undefined;
|
|
46
70
|
|
|
47
|
-
constructor(
|
|
71
|
+
constructor(
|
|
72
|
+
runtime: StoreRuntime,
|
|
73
|
+
authority: CredentialAuthority,
|
|
74
|
+
debugLogger: DebugLogger = disabledDebugLogger,
|
|
75
|
+
streamHeartbeatMs = 5_000,
|
|
76
|
+
) {
|
|
48
77
|
this.#runtime = runtime;
|
|
49
78
|
this.#authority = authority;
|
|
79
|
+
this.#debugLogger = debugLogger;
|
|
80
|
+
this.#streamHeartbeatMs = streamHeartbeatMs;
|
|
50
81
|
}
|
|
51
82
|
|
|
52
83
|
armReplayTransition(): { readonly reached: Promise<void>; readonly release: () => void } {
|
|
@@ -67,37 +98,38 @@ export class CoordinationApi {
|
|
|
67
98
|
assertServerDerivedPrincipal(request.principal);
|
|
68
99
|
const authentication = request.principal;
|
|
69
100
|
|
|
70
|
-
if (request.path ===
|
|
101
|
+
if (request.path === ATTACH_PATH && request.method === "POST") {
|
|
71
102
|
const requestId = safeRequestId(request.body);
|
|
72
103
|
try {
|
|
73
|
-
const envelope =
|
|
74
|
-
|
|
75
|
-
const priorDroneId = envelope.payload["prior_drone_id"] === undefined
|
|
76
|
-
? undefined
|
|
77
|
-
: requiredUuid(envelope.payload, "prior_drone_id");
|
|
104
|
+
const envelope = decodeAttachRequestEnvelope(request.body);
|
|
105
|
+
const priorDroneId = envelope.payload.prior_drone_id;
|
|
78
106
|
const attachment = this.#authority.attachSeat(
|
|
79
107
|
this.#runtime.forPrincipal(authentication),
|
|
80
108
|
{
|
|
81
|
-
cubeId:
|
|
82
|
-
roleId:
|
|
83
|
-
|
|
109
|
+
cubeId: envelope.payload.cube_id,
|
|
110
|
+
roleId: envelope.payload.role_id,
|
|
111
|
+
sessionCredential: envelope.payload.session_credential,
|
|
84
112
|
...(priorDroneId === undefined ? {} : { priorDroneId }),
|
|
85
113
|
},
|
|
86
114
|
);
|
|
87
|
-
return success(201, envelope.
|
|
115
|
+
return success(attachment.result === "created" ? 201 : 200, envelope.request_id, {
|
|
116
|
+
result: attachment.result,
|
|
88
117
|
cube: attachment.cube,
|
|
89
118
|
role: attachment.role,
|
|
90
119
|
drone: attachment.drone,
|
|
91
120
|
session: {
|
|
92
|
-
|
|
121
|
+
id: attachment.sessionId,
|
|
93
122
|
expires_at: attachment.expiresAt,
|
|
94
|
-
generation: attachment.generation,
|
|
95
123
|
},
|
|
96
|
-
reattached: attachment.reattached,
|
|
97
124
|
});
|
|
98
125
|
} catch (error) {
|
|
99
|
-
if (error instanceof
|
|
100
|
-
return
|
|
126
|
+
if (error instanceof ProtocolContractError) {
|
|
127
|
+
return error.code === ErrorCode.UNSUPPORTED_PROTOCOL_VERSION
|
|
128
|
+
? failure(426, error.code, "Unsupported protocol version.", requestId)
|
|
129
|
+
: failure(400, "INVALID_INPUT", "Invalid protocol request.", requestId);
|
|
130
|
+
}
|
|
131
|
+
if (error instanceof AttachSessionRejectedError) {
|
|
132
|
+
return failure(401, ErrorCode.SESSION_REJECTED, error.message, requestId);
|
|
101
133
|
}
|
|
102
134
|
if (error instanceof ScopedStoreError) {
|
|
103
135
|
return failure(404, "NOT_FOUND", error.message, requestId);
|
|
@@ -118,6 +150,7 @@ export class CoordinationApi {
|
|
|
118
150
|
owner_id: cube.ownerId,
|
|
119
151
|
name: cube.name,
|
|
120
152
|
cube_directive: cube.directive,
|
|
153
|
+
message_taxonomy: cube.messageTaxonomy,
|
|
121
154
|
created_at: cube.createdAt,
|
|
122
155
|
updated_at: cube.updatedAt,
|
|
123
156
|
}));
|
|
@@ -152,6 +185,11 @@ export class CoordinationApi {
|
|
|
152
185
|
if (error instanceof StorageCapacityError) {
|
|
153
186
|
return failure(507, "CAPACITY_EXCEEDED", error.message, requestId);
|
|
154
187
|
}
|
|
188
|
+
if (error instanceof ProtocolContractError) {
|
|
189
|
+
return error.code === ErrorCode.UNSUPPORTED_PROTOCOL_VERSION
|
|
190
|
+
? failure(426, error.code, "Unsupported protocol version.", requestId)
|
|
191
|
+
: failure(400, "INVALID_INPUT", "Invalid protocol request.", requestId);
|
|
192
|
+
}
|
|
155
193
|
if (error instanceof InputError || error instanceof TypeError || error instanceof RangeError) {
|
|
156
194
|
return failure(400, "INVALID_INPUT", "Invalid protocol request.", requestId);
|
|
157
195
|
}
|
|
@@ -159,14 +197,20 @@ export class CoordinationApi {
|
|
|
159
197
|
}
|
|
160
198
|
}
|
|
161
199
|
|
|
162
|
-
const
|
|
200
|
+
const roleMatch = /^\/api\/cubes\/([0-9a-f-]{36})\/roles\/([0-9a-f-]{36})(\/section-patch)?$/u
|
|
201
|
+
.exec(request.path);
|
|
202
|
+
const match = /^\/api\/cubes\/([0-9a-f-]{36})(?:\/(roles|drones|logs|acks|decisions|stream|taxonomy-patch))?$/u
|
|
163
203
|
.exec(request.path);
|
|
164
|
-
if (match === null
|
|
165
|
-
const cubeId = match[1]!;
|
|
166
|
-
if (!uuidPattern.test(cubeId)) {
|
|
204
|
+
if (match === null && roleMatch === null) {
|
|
167
205
|
return failure(404, "NOT_FOUND", "The requested resource was not found.");
|
|
168
206
|
}
|
|
169
|
-
const
|
|
207
|
+
const cubeId = (roleMatch?.[1] ?? match?.[1])!;
|
|
208
|
+
const roleId = roleMatch?.[2];
|
|
209
|
+
if (!uuidPattern.test(cubeId) || (roleId !== undefined && !uuidPattern.test(roleId))) {
|
|
210
|
+
return failure(404, "NOT_FOUND", "The requested resource was not found.");
|
|
211
|
+
}
|
|
212
|
+
const resource = roleMatch === null ? match?.[2] : "role";
|
|
213
|
+
const sectionPatch = roleMatch?.[3] !== undefined;
|
|
170
214
|
const store = this.#runtime.forPrincipal(authentication);
|
|
171
215
|
|
|
172
216
|
try {
|
|
@@ -179,31 +223,187 @@ export class CoordinationApi {
|
|
|
179
223
|
owner_id: cube.ownerId,
|
|
180
224
|
name: cube.name,
|
|
181
225
|
cube_directive: cube.directive,
|
|
226
|
+
message_taxonomy: cube.messageTaxonomy,
|
|
182
227
|
created_at: cube.createdAt,
|
|
183
228
|
updated_at: cube.updatedAt,
|
|
184
229
|
},
|
|
185
230
|
});
|
|
186
231
|
}
|
|
232
|
+
if (resource === undefined && request.method === "PATCH") {
|
|
233
|
+
const envelope = decodeEnvelope(request.body);
|
|
234
|
+
exactKeys(envelope.payload, [], ["cube_directive", "message_taxonomy"]);
|
|
235
|
+
if (Object.keys(envelope.payload).length === 0) throw new InputError();
|
|
236
|
+
const directive = optionalText(envelope.payload, "cube_directive", 100_000);
|
|
237
|
+
const messageTaxonomy = optionalMessageTaxonomy(envelope.payload["message_taxonomy"]);
|
|
238
|
+
const cube = store.updateCube(cubeId, {
|
|
239
|
+
...(directive === undefined ? {} : { directive }),
|
|
240
|
+
...(messageTaxonomy === undefined ? {} : { messageTaxonomy }),
|
|
241
|
+
});
|
|
242
|
+
return success(200, envelope.requestId, { cube: cubePayload(cube) });
|
|
243
|
+
}
|
|
244
|
+
if (resource === "taxonomy-patch" && request.method === "POST") {
|
|
245
|
+
const envelope = decodeEnvelope(request.body);
|
|
246
|
+
const action = envelope.payload["action"];
|
|
247
|
+
const cube = store.getCube(cubeId);
|
|
248
|
+
if (cube === null) throw new ScopedStoreError();
|
|
249
|
+
if (action === "remove") {
|
|
250
|
+
exactKeys(envelope.payload, ["action", "class"]);
|
|
251
|
+
const messageTaxonomy = patchMessageTaxonomy(cube.messageTaxonomy, {
|
|
252
|
+
action,
|
|
253
|
+
className: requiredString(envelope.payload, "class", 64),
|
|
254
|
+
});
|
|
255
|
+
return success(200, envelope.requestId, {
|
|
256
|
+
cube: cubePayload(store.updateCube(cubeId, { messageTaxonomy })),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
if (action !== "add" && action !== "replace") throw new InputError();
|
|
260
|
+
exactKeys(envelope.payload, ["action", "class_def"]);
|
|
261
|
+
const classDef = validateMessageTaxonomy([envelope.payload["class_def"]])![0]!;
|
|
262
|
+
const messageTaxonomy = patchMessageTaxonomy(cube.messageTaxonomy, { action, classDef });
|
|
263
|
+
return success(200, envelope.requestId, {
|
|
264
|
+
cube: cubePayload(store.updateCube(cubeId, { messageTaxonomy })),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
187
267
|
if (resource === "roles" && request.method === "GET") {
|
|
188
268
|
return success(200, "roles-read", { roles: store.listRoles(cubeId) });
|
|
189
269
|
}
|
|
270
|
+
if (resource === "roles" && request.method === "POST") {
|
|
271
|
+
const envelope = decodeEnvelope(request.body);
|
|
272
|
+
exactKeys(envelope.payload, ["name"], [
|
|
273
|
+
"short_description",
|
|
274
|
+
"detailed_description",
|
|
275
|
+
"is_default",
|
|
276
|
+
"is_mandatory",
|
|
277
|
+
"is_human_seat",
|
|
278
|
+
"can_broadcast",
|
|
279
|
+
"receives_all_direct",
|
|
280
|
+
"role_class",
|
|
281
|
+
]);
|
|
282
|
+
const role = store.createRole(cubeId, {
|
|
283
|
+
name: requiredRoleName(envelope.payload, "name"),
|
|
284
|
+
shortDescription: optionalText(envelope.payload, "short_description", 1_024) ?? "",
|
|
285
|
+
detailedDescription: optionalText(envelope.payload, "detailed_description", 51_200) ?? "",
|
|
286
|
+
isDefault: optionalBoolean(envelope.payload["is_default"]),
|
|
287
|
+
isMandatory: optionalBoolean(envelope.payload["is_mandatory"]),
|
|
288
|
+
isHumanSeat: optionalBoolean(envelope.payload["is_human_seat"]),
|
|
289
|
+
canBroadcast: optionalBoolean(envelope.payload["can_broadcast"]),
|
|
290
|
+
receivesAllDirect: optionalBoolean(envelope.payload["receives_all_direct"]),
|
|
291
|
+
...(envelope.payload["role_class"] === undefined
|
|
292
|
+
? {}
|
|
293
|
+
: { roleClass: optionalRoleClass(envelope.payload["role_class"])! }),
|
|
294
|
+
});
|
|
295
|
+
return success(201, envelope.requestId, { role });
|
|
296
|
+
}
|
|
297
|
+
if (resource === "role" && !sectionPatch && request.method === "PATCH") {
|
|
298
|
+
const envelope = decodeEnvelope(request.body);
|
|
299
|
+
exactKeys(envelope.payload, [], [
|
|
300
|
+
"name",
|
|
301
|
+
"short_description",
|
|
302
|
+
"detailed_description",
|
|
303
|
+
"is_default",
|
|
304
|
+
"is_mandatory",
|
|
305
|
+
"is_human_seat",
|
|
306
|
+
"can_broadcast",
|
|
307
|
+
"receives_all_direct",
|
|
308
|
+
"role_class",
|
|
309
|
+
]);
|
|
310
|
+
if (Object.keys(envelope.payload).length === 0) throw new InputError();
|
|
311
|
+
const name = envelope.payload["name"] === undefined
|
|
312
|
+
? undefined
|
|
313
|
+
: requiredRoleName(envelope.payload, "name");
|
|
314
|
+
const shortDescription = optionalText(envelope.payload, "short_description", 1_024);
|
|
315
|
+
const detailedDescription = optionalText(envelope.payload, "detailed_description", 51_200);
|
|
316
|
+
const isDefault = optionalBooleanValue(envelope.payload["is_default"]);
|
|
317
|
+
const isMandatory = optionalBooleanValue(envelope.payload["is_mandatory"]);
|
|
318
|
+
const isHumanSeat = optionalBooleanValue(envelope.payload["is_human_seat"]);
|
|
319
|
+
const canBroadcast = optionalBooleanValue(envelope.payload["can_broadcast"]);
|
|
320
|
+
const receivesAllDirect = optionalBooleanValue(envelope.payload["receives_all_direct"]);
|
|
321
|
+
const roleClass = optionalRoleClass(envelope.payload["role_class"]);
|
|
322
|
+
const role = store.updateRole(cubeId, roleId!, {
|
|
323
|
+
...(name === undefined ? {} : { name }),
|
|
324
|
+
...(shortDescription === undefined ? {} : { shortDescription }),
|
|
325
|
+
...(detailedDescription === undefined ? {} : { detailedDescription }),
|
|
326
|
+
...(isDefault === undefined ? {} : { isDefault }),
|
|
327
|
+
...(isMandatory === undefined ? {} : { isMandatory }),
|
|
328
|
+
...(isHumanSeat === undefined ? {} : { isHumanSeat }),
|
|
329
|
+
...(canBroadcast === undefined ? {} : { canBroadcast }),
|
|
330
|
+
...(receivesAllDirect === undefined ? {} : { receivesAllDirect }),
|
|
331
|
+
...(roleClass === undefined ? {} : { roleClass }),
|
|
332
|
+
});
|
|
333
|
+
return success(200, envelope.requestId, { role });
|
|
334
|
+
}
|
|
335
|
+
if (resource === "role" && sectionPatch && request.method === "POST") {
|
|
336
|
+
const envelope = decodeEnvelope(request.body);
|
|
337
|
+
const action = envelope.payload["action"];
|
|
338
|
+
if (action === "delete") {
|
|
339
|
+
exactKeys(envelope.payload, ["action", "heading"]);
|
|
340
|
+
const role = store.patchRoleSection(cubeId, roleId!, {
|
|
341
|
+
action,
|
|
342
|
+
heading: requiredSectionHeading(envelope.payload, "heading"),
|
|
343
|
+
});
|
|
344
|
+
return success(200, envelope.requestId, { role });
|
|
345
|
+
}
|
|
346
|
+
if (action !== "replace" && action !== "insert") throw new InputError();
|
|
347
|
+
exactKeys(envelope.payload, ["action", "heading", "body"], action === "insert" ? ["after"] : []);
|
|
348
|
+
const body = optionalText(envelope.payload, "body", 51_200);
|
|
349
|
+
if (body === undefined) throw new InputError();
|
|
350
|
+
const heading = requiredSectionHeading(envelope.payload, "heading");
|
|
351
|
+
if (action === "replace") {
|
|
352
|
+
return success(200, envelope.requestId, {
|
|
353
|
+
role: store.patchRoleSection(cubeId, roleId!, { action, heading, body }),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
const after = optionalNullableSectionHeading(envelope.payload["after"]);
|
|
357
|
+
return success(200, envelope.requestId, {
|
|
358
|
+
role: store.patchRoleSection(cubeId, roleId!, {
|
|
359
|
+
action,
|
|
360
|
+
heading,
|
|
361
|
+
body,
|
|
362
|
+
...(after === undefined ? {} : { after }),
|
|
363
|
+
}),
|
|
364
|
+
});
|
|
365
|
+
}
|
|
190
366
|
if (resource === "drones" && request.method === "GET") {
|
|
191
|
-
|
|
367
|
+
if (request.since === undefined) {
|
|
368
|
+
return success(200, "drones-read", { drones: store.listDrones(cubeId) });
|
|
369
|
+
}
|
|
370
|
+
const since = decodeSince(request.since);
|
|
371
|
+
return success(200, "drones-read", store.listDronesSince(cubeId, since));
|
|
192
372
|
}
|
|
193
373
|
if (resource === "logs" && request.method === "POST") {
|
|
194
374
|
const envelope = decodeEnvelope(request.body);
|
|
195
|
-
exactKeys(envelope.payload, ["message"], [
|
|
375
|
+
exactKeys(envelope.payload, ["message"], [
|
|
376
|
+
"visibility", "recipientDroneIds", "class", "to",
|
|
377
|
+
]);
|
|
196
378
|
const message = requiredString(envelope.payload, "message", 10_240);
|
|
197
379
|
const visibility = optionalVisibility(envelope.payload["visibility"]);
|
|
198
380
|
const recipientDroneIds = optionalUuidArray(envelope.payload["recipientDroneIds"]);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const
|
|
381
|
+
const className = optionalString(envelope.payload, "class", 64);
|
|
382
|
+
const to = optionalStringArray(envelope.payload["to"]);
|
|
383
|
+
const cube = store.getCube(cubeId);
|
|
384
|
+
if (cube === null) throw new ScopedStoreError();
|
|
385
|
+
const resolved = resolveMessageRouting({
|
|
204
386
|
message,
|
|
205
387
|
...(visibility === undefined ? {} : { visibility }),
|
|
206
388
|
...(recipientDroneIds === undefined ? {} : { recipientDroneIds }),
|
|
389
|
+
...(className === undefined ? {} : { className }),
|
|
390
|
+
...(to === undefined ? {} : { to }),
|
|
391
|
+
}, cube.messageTaxonomy, store.listRoles(cubeId), store.listDrones(cubeId));
|
|
392
|
+
const entry = store.appendLog(cubeId, {
|
|
393
|
+
message,
|
|
394
|
+
visibility: resolved.visibility,
|
|
395
|
+
...(resolved.visibility === "direct"
|
|
396
|
+
? { recipientDroneIds: resolved.recipientDroneIds }
|
|
397
|
+
: {}),
|
|
398
|
+
});
|
|
399
|
+
this.#debugLogger.emit({
|
|
400
|
+
event: "activity_append",
|
|
401
|
+
cubeId,
|
|
402
|
+
entryId: entry.id,
|
|
403
|
+
principal: authentication,
|
|
404
|
+
droneId: entry.drone_id,
|
|
405
|
+
visibility: entry.visibility,
|
|
406
|
+
recipientDroneIds: entry.recipient_drone_ids,
|
|
207
407
|
});
|
|
208
408
|
return success(201, envelope.requestId, { entry });
|
|
209
409
|
}
|
|
@@ -212,7 +412,17 @@ export class CoordinationApi {
|
|
|
212
412
|
exactKeys(envelope.payload, ["cursor"], ["limit"]);
|
|
213
413
|
const cursor = decodeCursor(envelope.payload["cursor"]);
|
|
214
414
|
const limit = optionalLimit(envelope.payload["limit"]);
|
|
215
|
-
|
|
415
|
+
const page = store.readLog(cubeId, cursor, limit);
|
|
416
|
+
this.#debugLogger.emit({
|
|
417
|
+
event: "cursor_replay",
|
|
418
|
+
mode: "page",
|
|
419
|
+
cubeId,
|
|
420
|
+
cursorId: cursor?.id ?? null,
|
|
421
|
+
returnedCount: page.entries.length,
|
|
422
|
+
behindBy: page.behind_by,
|
|
423
|
+
truncated: page.has_more,
|
|
424
|
+
});
|
|
425
|
+
return success(200, envelope.requestId, page);
|
|
216
426
|
}
|
|
217
427
|
if (resource === "acks" && request.method === "POST") {
|
|
218
428
|
const envelope = decodeEnvelope(request.body);
|
|
@@ -221,6 +431,7 @@ export class CoordinationApi {
|
|
|
221
431
|
if (kind !== "ack" && kind !== "claim") throw new InputError();
|
|
222
432
|
exactKeys(envelope.payload, ["entry_id", "kind"]);
|
|
223
433
|
store.acknowledge(cubeId, entryId, kind);
|
|
434
|
+
this.#debugLogger.emit({ event: "ack_write", cubeId, entryId, kind, principal: authentication });
|
|
224
435
|
return { status: 204 };
|
|
225
436
|
}
|
|
226
437
|
if (resource === "decisions" && request.method === "POST") {
|
|
@@ -229,13 +440,18 @@ export class CoordinationApi {
|
|
|
229
440
|
const topic = requiredString(envelope.payload, "topic", 120);
|
|
230
441
|
const decision = requiredString(envelope.payload, "decision", 100_000);
|
|
231
442
|
const rationale = optionalString(envelope.payload, "rationale", 100_000);
|
|
232
|
-
|
|
233
|
-
decision: store.recordDecision(cubeId, {
|
|
443
|
+
const decisionRecord = store.recordDecision(cubeId, {
|
|
234
444
|
topic,
|
|
235
445
|
decision,
|
|
236
446
|
...(rationale === undefined ? {} : { rationale }),
|
|
237
|
-
})
|
|
447
|
+
});
|
|
448
|
+
this.#debugLogger.emit({
|
|
449
|
+
event: "decision_write",
|
|
450
|
+
cubeId,
|
|
451
|
+
decisionId: decisionRecord.id,
|
|
452
|
+
principal: authentication,
|
|
238
453
|
});
|
|
454
|
+
return success(201, envelope.requestId, { decision: decisionRecord });
|
|
239
455
|
}
|
|
240
456
|
if (resource === "decisions" && request.method === "PUT") {
|
|
241
457
|
const envelope = decodeEnvelope(request.body);
|
|
@@ -250,12 +466,29 @@ export class CoordinationApi {
|
|
|
250
466
|
if (error instanceof CursorExpiredError) {
|
|
251
467
|
return failure(410, "CURSOR_EXPIRED", error.message);
|
|
252
468
|
}
|
|
469
|
+
if (error instanceof AccessDeniedError) {
|
|
470
|
+
return failure(403, ErrorCode.ACCESS_DENIED, error.message);
|
|
471
|
+
}
|
|
253
472
|
if (error instanceof ScopedStoreError) {
|
|
254
473
|
return failure(404, "NOT_FOUND", error.message);
|
|
255
474
|
}
|
|
475
|
+
if (error instanceof RoleConflictError) {
|
|
476
|
+
return failure(409, error.code, error.message, safeRequestId(request.body));
|
|
477
|
+
}
|
|
478
|
+
if (error instanceof DefaultRoleRequiredError) {
|
|
479
|
+
return failure(409, error.code, error.message, safeRequestId(request.body));
|
|
480
|
+
}
|
|
481
|
+
if (error instanceof RoleSectionConflictError) {
|
|
482
|
+
return failure(409, error.code, error.message, safeRequestId(request.body));
|
|
483
|
+
}
|
|
256
484
|
if (error instanceof StorageCapacityError) {
|
|
257
485
|
return failure(507, "CAPACITY_EXCEEDED", error.message, safeRequestId(request.body));
|
|
258
486
|
}
|
|
487
|
+
if (error instanceof ProtocolContractError) {
|
|
488
|
+
return error.code === ErrorCode.UNSUPPORTED_PROTOCOL_VERSION
|
|
489
|
+
? failure(426, error.code, "Unsupported protocol version.", safeRequestId(request.body))
|
|
490
|
+
: failure(400, "INVALID_INPUT", "Invalid protocol request.", safeRequestId(request.body));
|
|
491
|
+
}
|
|
259
492
|
if (error instanceof InputError || error instanceof TypeError || error instanceof RangeError) {
|
|
260
493
|
return failure(400, "INVALID_INPUT", "Invalid protocol request.", safeRequestId(request.body));
|
|
261
494
|
}
|
|
@@ -270,14 +503,30 @@ export class CoordinationApi {
|
|
|
270
503
|
requestSignal: AbortSignal,
|
|
271
504
|
): Promise<CoordinationResponse> {
|
|
272
505
|
const store = this.#runtime.forPrincipal(principal);
|
|
506
|
+
const connectionId = randomUUID();
|
|
273
507
|
const session = this.#authority.registerLiveSession(principal);
|
|
274
508
|
const signal = AbortSignal.any([requestSignal, session.signal]);
|
|
275
509
|
let unsubscribe = (): void => undefined;
|
|
510
|
+
let subscribed = false;
|
|
511
|
+
let deliveryCount = 0;
|
|
512
|
+
let heartbeat: NodeJS.Timeout | undefined;
|
|
276
513
|
const queue = new AsyncStringQueue(() => {
|
|
514
|
+
if (heartbeat !== undefined) clearInterval(heartbeat);
|
|
277
515
|
unsubscribe();
|
|
278
516
|
session.release();
|
|
279
|
-
|
|
280
|
-
|
|
517
|
+
if (subscribed) {
|
|
518
|
+
this.#debugLogger.emit({
|
|
519
|
+
event: "sse_unsubscribe",
|
|
520
|
+
connectionId,
|
|
521
|
+
cubeId,
|
|
522
|
+
principal,
|
|
523
|
+
deliveryCount,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}, () => { deliveryCount += 1; });
|
|
527
|
+
let replayDirty = false;
|
|
528
|
+
const pendingNotifications: ActivityStreamRecord[] = [];
|
|
529
|
+
let notificationIndex = 0;
|
|
281
530
|
let live = false;
|
|
282
531
|
try {
|
|
283
532
|
unsubscribe = store.subscribeActivity(cubeId, (entry) => {
|
|
@@ -286,11 +535,13 @@ export class CoordinationApi {
|
|
|
286
535
|
return;
|
|
287
536
|
}
|
|
288
537
|
if (live) queue.push(encodeLogEvent(entry));
|
|
289
|
-
else if (
|
|
290
|
-
else
|
|
538
|
+
else if ("kind" in entry) pendingNotifications.push(entry);
|
|
539
|
+
else replayDirty = true;
|
|
291
540
|
});
|
|
541
|
+
subscribed = true;
|
|
292
542
|
signal.addEventListener("abort", () => queue.close(), { once: true });
|
|
293
|
-
const
|
|
543
|
+
const firstPage = store.readLog(cubeId, cursor, 200);
|
|
544
|
+
this.#logReplayPage(cubeId, cursor, firstPage);
|
|
294
545
|
const barrier = this.#replayBarrier;
|
|
295
546
|
this.#replayBarrier = undefined;
|
|
296
547
|
if (barrier !== undefined) {
|
|
@@ -298,19 +549,61 @@ export class CoordinationApi {
|
|
|
298
549
|
barrier.markReached();
|
|
299
550
|
await barrier.release;
|
|
300
551
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
552
|
+
void (async () => {
|
|
553
|
+
let page = firstPage;
|
|
554
|
+
let replayCursor = cursor;
|
|
555
|
+
let replayCount = 0;
|
|
556
|
+
try {
|
|
557
|
+
for (;;) {
|
|
558
|
+
for (const entry of page.entries) {
|
|
559
|
+
if (!await queue.write(encodeLogEvent(entry))) return;
|
|
560
|
+
replayCursor = { id: entry.id, created_at: entry.created_at };
|
|
561
|
+
replayCount += 1;
|
|
562
|
+
}
|
|
563
|
+
if (page.has_more || replayDirty) {
|
|
564
|
+
replayDirty = false;
|
|
565
|
+
page = store.readLog(cubeId, replayCursor, 200);
|
|
566
|
+
this.#logReplayPage(cubeId, replayCursor, page);
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
while (notificationIndex < pendingNotifications.length) {
|
|
571
|
+
if (!await queue.write(encodeLogEvent(pendingNotifications[notificationIndex]!))) return;
|
|
572
|
+
notificationIndex += 1;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Reserve room for the bookmark before making live callbacks visible.
|
|
576
|
+
// Any append while waiting marks replay dirty and is fetched first.
|
|
577
|
+
if (!await queue.waitForSpace()) return;
|
|
578
|
+
if (replayDirty || notificationIndex < pendingNotifications.length) {
|
|
579
|
+
replayDirty = false;
|
|
580
|
+
page = store.readLog(cubeId, replayCursor, 200);
|
|
581
|
+
this.#logReplayPage(cubeId, replayCursor, page);
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
queue.push(`event: bookmark\ndata: ${JSON.stringify({
|
|
585
|
+
as_of: new Date().toISOString(),
|
|
586
|
+
replay_complete: true,
|
|
587
|
+
})}\n\n`);
|
|
588
|
+
live = true;
|
|
589
|
+
heartbeat = setInterval(() => {
|
|
590
|
+
queue.tryPush(encodeHeartbeat());
|
|
591
|
+
}, this.#streamHeartbeatMs);
|
|
592
|
+
heartbeat.unref();
|
|
593
|
+
this.#debugLogger.emit({
|
|
594
|
+
event: "sse_subscribe",
|
|
595
|
+
connectionId,
|
|
596
|
+
cubeId,
|
|
597
|
+
principal,
|
|
598
|
+
replayCount,
|
|
599
|
+
truncated: false,
|
|
600
|
+
});
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
} catch {
|
|
604
|
+
queue.close();
|
|
307
605
|
}
|
|
308
|
-
}
|
|
309
|
-
queue.push(`event: bookmark\ndata: ${JSON.stringify({
|
|
310
|
-
as_of: new Date().toISOString(),
|
|
311
|
-
replay_complete: true,
|
|
312
|
-
})}\n\n`);
|
|
313
|
-
live = true;
|
|
606
|
+
})();
|
|
314
607
|
return { status: 200, stream: queue };
|
|
315
608
|
} catch (error) {
|
|
316
609
|
queue.close();
|
|
@@ -319,6 +612,22 @@ export class CoordinationApi {
|
|
|
319
612
|
throw error;
|
|
320
613
|
}
|
|
321
614
|
}
|
|
615
|
+
|
|
616
|
+
#logReplayPage(
|
|
617
|
+
cubeId: string,
|
|
618
|
+
cursor: LogCursor | null,
|
|
619
|
+
page: ActivityPage,
|
|
620
|
+
): void {
|
|
621
|
+
this.#debugLogger.emit({
|
|
622
|
+
event: "cursor_replay",
|
|
623
|
+
mode: "sse",
|
|
624
|
+
cubeId,
|
|
625
|
+
cursorId: cursor?.id ?? null,
|
|
626
|
+
returnedCount: page.entries.length,
|
|
627
|
+
behindBy: page.behind_by,
|
|
628
|
+
truncated: page.has_more,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
322
631
|
}
|
|
323
632
|
|
|
324
633
|
class InputError extends Error {}
|
|
@@ -326,11 +635,14 @@ class InputError extends Error {}
|
|
|
326
635
|
class AsyncStringQueue implements AsyncIterable<string> {
|
|
327
636
|
readonly #values: string[] = [];
|
|
328
637
|
readonly #waiters: Array<(result: IteratorResult<string>) => void> = [];
|
|
638
|
+
readonly #spaceWaiters: Array<(available: boolean) => void> = [];
|
|
329
639
|
readonly #cleanup: () => void;
|
|
640
|
+
readonly #onDelivered: () => void;
|
|
330
641
|
#closed = false;
|
|
331
642
|
|
|
332
|
-
constructor(cleanup: () => void) {
|
|
643
|
+
constructor(cleanup: () => void, onDelivered: () => void = () => undefined) {
|
|
333
644
|
this.#cleanup = cleanup;
|
|
645
|
+
this.#onDelivered = onDelivered;
|
|
334
646
|
}
|
|
335
647
|
|
|
336
648
|
push(value: string): void {
|
|
@@ -339,9 +651,28 @@ class AsyncStringQueue implements AsyncIterable<string> {
|
|
|
339
651
|
this.close();
|
|
340
652
|
return;
|
|
341
653
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
654
|
+
this.#enqueue(value);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
tryPush(value: string): boolean {
|
|
658
|
+
if (this.#closed || this.#values.length >= 200) return false;
|
|
659
|
+
this.#enqueue(value);
|
|
660
|
+
return true;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async write(value: string): Promise<boolean> {
|
|
664
|
+
while (!this.#closed && this.#values.length >= 200) {
|
|
665
|
+
if (!await this.waitForSpace()) return false;
|
|
666
|
+
}
|
|
667
|
+
if (this.#closed) return false;
|
|
668
|
+
this.#enqueue(value);
|
|
669
|
+
return true;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
waitForSpace(): Promise<boolean> {
|
|
673
|
+
if (this.#closed) return Promise.resolve(false);
|
|
674
|
+
if (this.#values.length < 200) return Promise.resolve(true);
|
|
675
|
+
return new Promise((resolve) => this.#spaceWaiters.push(resolve));
|
|
345
676
|
}
|
|
346
677
|
|
|
347
678
|
close(): void {
|
|
@@ -349,13 +680,18 @@ class AsyncStringQueue implements AsyncIterable<string> {
|
|
|
349
680
|
this.#closed = true;
|
|
350
681
|
this.#cleanup();
|
|
351
682
|
for (const waiter of this.#waiters.splice(0)) waiter({ value: undefined, done: true });
|
|
683
|
+
for (const waiter of this.#spaceWaiters.splice(0)) waiter(false);
|
|
352
684
|
}
|
|
353
685
|
|
|
354
686
|
[Symbol.asyncIterator](): AsyncIterator<string> {
|
|
355
687
|
return {
|
|
356
688
|
next: async () => {
|
|
357
689
|
const value = this.#values.shift();
|
|
358
|
-
if (value !== undefined)
|
|
690
|
+
if (value !== undefined) {
|
|
691
|
+
this.#spaceWaiters.shift()?.(true);
|
|
692
|
+
this.#onDelivered();
|
|
693
|
+
return { value, done: false };
|
|
694
|
+
}
|
|
359
695
|
if (this.#closed) return { value: undefined, done: true };
|
|
360
696
|
return new Promise<IteratorResult<string>>((resolve) => this.#waiters.push(resolve));
|
|
361
697
|
},
|
|
@@ -365,17 +701,20 @@ class AsyncStringQueue implements AsyncIterable<string> {
|
|
|
365
701
|
},
|
|
366
702
|
};
|
|
367
703
|
}
|
|
704
|
+
|
|
705
|
+
#enqueue(value: string): void {
|
|
706
|
+
const waiter = this.#waiters.shift();
|
|
707
|
+
if (waiter === undefined) this.#values.push(value);
|
|
708
|
+
else {
|
|
709
|
+
this.#onDelivered();
|
|
710
|
+
waiter({ value, done: false });
|
|
711
|
+
}
|
|
712
|
+
}
|
|
368
713
|
}
|
|
369
714
|
|
|
370
715
|
function decodeEnvelope(value: unknown): RequestEnvelope {
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
if (record["protocol_version"] !== "1") throw new InputError();
|
|
374
|
-
const requestId = record["request_id"];
|
|
375
|
-
if (typeof requestId !== "string" || !/^[A-Za-z0-9._-]{8,128}$/u.test(requestId)) {
|
|
376
|
-
throw new InputError();
|
|
377
|
-
}
|
|
378
|
-
return { requestId, payload: object(record["payload"]) };
|
|
716
|
+
const envelope = decodeProtocolEnvelope(value, object);
|
|
717
|
+
return { requestId: envelope.request_id, payload: envelope.payload };
|
|
379
718
|
}
|
|
380
719
|
|
|
381
720
|
function object(value: unknown): Record<string, unknown> {
|
|
@@ -403,6 +742,12 @@ function requiredPresentationName(record: Record<string, unknown>, key: string):
|
|
|
403
742
|
return value;
|
|
404
743
|
}
|
|
405
744
|
|
|
745
|
+
function requiredRoleName(record: Record<string, unknown>, key: string): string {
|
|
746
|
+
const value = record[key];
|
|
747
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 64) throw new InputError();
|
|
748
|
+
return value;
|
|
749
|
+
}
|
|
750
|
+
|
|
406
751
|
function optionalString(
|
|
407
752
|
record: Record<string, unknown>,
|
|
408
753
|
key: string,
|
|
@@ -413,6 +758,63 @@ function optionalString(
|
|
|
413
758
|
return requiredString(record, key, maxBytes);
|
|
414
759
|
}
|
|
415
760
|
|
|
761
|
+
function optionalText(
|
|
762
|
+
record: Record<string, unknown>,
|
|
763
|
+
key: string,
|
|
764
|
+
maxCharacters: number,
|
|
765
|
+
): string | undefined {
|
|
766
|
+
const value = record[key];
|
|
767
|
+
if (value === undefined) return undefined;
|
|
768
|
+
if (typeof value !== "string" || value.length > maxCharacters) throw new InputError();
|
|
769
|
+
return value;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function optionalBoolean(value: unknown): boolean {
|
|
773
|
+
if (value === undefined) return false;
|
|
774
|
+
if (typeof value !== "boolean") throw new InputError();
|
|
775
|
+
return value;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function optionalBooleanValue(value: unknown): boolean | undefined {
|
|
779
|
+
if (value === undefined) return undefined;
|
|
780
|
+
if (typeof value !== "boolean") throw new InputError();
|
|
781
|
+
return value;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function optionalRoleClass(value: unknown): "queen" | "worker" | undefined {
|
|
785
|
+
if (value === undefined) return undefined;
|
|
786
|
+
if (value !== "queen" && value !== "worker") throw new InputError();
|
|
787
|
+
return value;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function optionalMessageTaxonomy(value: unknown) {
|
|
791
|
+
return value === undefined ? undefined : validateMessageTaxonomy(value);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function optionalStringArray(value: unknown): string[] | undefined {
|
|
795
|
+
if (value === undefined) return undefined;
|
|
796
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 100 ||
|
|
797
|
+
value.some((entry) => typeof entry !== "string" || entry.length < 1 || entry.length > 120)) {
|
|
798
|
+
throw new InputError();
|
|
799
|
+
}
|
|
800
|
+
const entries = value as string[];
|
|
801
|
+
if (new Set(entries).size !== entries.length) throw new InputError();
|
|
802
|
+
return entries;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function requiredSectionHeading(record: Record<string, unknown>, key: string): string {
|
|
806
|
+
const value = requiredString(record, key, 60);
|
|
807
|
+
const heading = value.trim();
|
|
808
|
+
if (heading.length === 0 || /^\s/u.test(value) || /[:\n\r]/u.test(heading) ||
|
|
809
|
+
/^[*\-#>`]/u.test(heading)) throw new InputError();
|
|
810
|
+
return heading;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function optionalNullableSectionHeading(value: unknown): string | null | undefined {
|
|
814
|
+
if (value === undefined || value === null) return value;
|
|
815
|
+
return requiredSectionHeading({ value }, "value");
|
|
816
|
+
}
|
|
817
|
+
|
|
416
818
|
function requiredUuid(record: Record<string, unknown>, key: string): string {
|
|
417
819
|
const value = record[key];
|
|
418
820
|
if (typeof value !== "string" || !uuidPattern.test(value)) throw new InputError();
|
|
@@ -460,6 +862,12 @@ function decodeOpaqueCursor(value: string | undefined): LogCursor | null {
|
|
|
460
862
|
}
|
|
461
863
|
}
|
|
462
864
|
|
|
865
|
+
function decodeSince(value: string): string {
|
|
866
|
+
if (uuidPattern.test(value)) return value.toLowerCase();
|
|
867
|
+
if (timestampPattern.test(value) && new Date(value).toISOString() === value) return value;
|
|
868
|
+
throw new InputError();
|
|
869
|
+
}
|
|
870
|
+
|
|
463
871
|
function safeRequestId(value: unknown): string | undefined {
|
|
464
872
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
465
873
|
const requestId = (value as Record<string, unknown>)["request_id"];
|
|
@@ -469,7 +877,7 @@ function safeRequestId(value: unknown): string | undefined {
|
|
|
469
877
|
}
|
|
470
878
|
|
|
471
879
|
function success(status: number, requestId: string, payload: unknown): CoordinationResponse {
|
|
472
|
-
return { status, body:
|
|
880
|
+
return { status, body: createProtocolEnvelope(requestId, payload) };
|
|
473
881
|
}
|
|
474
882
|
|
|
475
883
|
function failure(
|
|
@@ -481,27 +889,37 @@ function failure(
|
|
|
481
889
|
return {
|
|
482
890
|
status,
|
|
483
891
|
body: {
|
|
484
|
-
protocol_version:
|
|
892
|
+
protocol_version: PROTOCOL_VERSION,
|
|
485
893
|
...(requestId === undefined ? {} : { request_id: requestId }),
|
|
486
894
|
error: { code, message },
|
|
487
895
|
},
|
|
488
896
|
};
|
|
489
897
|
}
|
|
490
898
|
|
|
491
|
-
function encodeLogEvent(entry:
|
|
899
|
+
function encodeLogEvent(entry: ActivityStreamRecord): string {
|
|
900
|
+
if ("kind" in entry) {
|
|
901
|
+
return `event: log\nid: ${entry.id}\ndata: ${JSON.stringify({ entry })}\n\n`;
|
|
902
|
+
}
|
|
492
903
|
return `event: log\nid: ${entry.id}\ndata: ${JSON.stringify({
|
|
493
904
|
cursor: { id: entry.id, created_at: entry.created_at },
|
|
494
905
|
entry,
|
|
495
906
|
})}\n\n`;
|
|
496
907
|
}
|
|
497
908
|
|
|
498
|
-
function
|
|
499
|
-
return
|
|
909
|
+
function encodeHeartbeat(): string {
|
|
910
|
+
return `event: heartbeat\ndata: ${JSON.stringify({ ts: new Date().toISOString() })}\n\n`;
|
|
500
911
|
}
|
|
501
912
|
|
|
502
|
-
function
|
|
503
|
-
return
|
|
504
|
-
|
|
913
|
+
function cubePayload(cube: CubeRecord) {
|
|
914
|
+
return {
|
|
915
|
+
id: cube.id,
|
|
916
|
+
owner_id: cube.ownerId,
|
|
917
|
+
name: cube.name,
|
|
918
|
+
cube_directive: cube.directive,
|
|
919
|
+
message_taxonomy: cube.messageTaxonomy,
|
|
920
|
+
created_at: cube.createdAt,
|
|
921
|
+
updated_at: cube.updatedAt,
|
|
922
|
+
};
|
|
505
923
|
}
|
|
506
924
|
|
|
507
925
|
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|