@semiont/gateway 0.5.29 → 0.5.30
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/dist/index.js +275 -61
- package/dist/index.js.map +1 -1
- package/dist/openapi.json +172 -5
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import winston from 'winston';
|
|
3
|
-
import {
|
|
3
|
+
import { registerCorrelationRegistryProvider, recordSubscriberConnect, withTraceparent, withSpan, recordBusEmit, recordUnanswerableRequest, SpanKind, recordSubscriberDisconnect, recordReplySuppressed, recordResumeGap, injectTraceparent, getLogTraceContext } from '@semiont/observability';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import jwt from 'jsonwebtoken';
|
|
6
|
-
import { email, userId, googleCredential, agentToDid, kbDid, BUS_OPERATIONS, EventBus, accessToken, userToDid, resourceId, CHANNEL_SCHEMAS, busLog, baseMediaType, isSupportedMediaType } from '@semiont/core';
|
|
6
|
+
import { email, userId, googleCredential, agentToDid, kbDid, replyChannelsFor, BUS_OPERATIONS, EventBus, accessToken, userToDid, resourceId, CHANNEL_SCHEMAS, busLog, baseMediaType, isSupportedMediaType } from '@semiont/core';
|
|
7
7
|
import { cors } from 'hono/cors';
|
|
8
8
|
import { serve } from '@hono/node-server';
|
|
9
9
|
import { Hono } from 'hono';
|
|
@@ -10346,7 +10346,7 @@ healthRouter.get("/api/health", async (c) => {
|
|
|
10346
10346
|
const response = {
|
|
10347
10347
|
status: "operational",
|
|
10348
10348
|
message: "Semiont API is running",
|
|
10349
|
-
version: "0.5.
|
|
10349
|
+
version: "0.5.30",
|
|
10350
10350
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10351
10351
|
database: dbStatus ? "connected" : "disconnected",
|
|
10352
10352
|
environment: nodeEnv
|
|
@@ -11011,10 +11011,14 @@ authRouter.get("/api/cookies/export", authMiddleware, async (c) => {
|
|
|
11011
11011
|
|
|
11012
11012
|
// src/lib/archivist.ts
|
|
11013
11013
|
init_logger();
|
|
11014
|
+
var archivistSpan = (op, run) => withSpan(`archivist.${op}`, run, {
|
|
11015
|
+
kind: SpanKind.CLIENT,
|
|
11016
|
+
attrs: { "peer.service": "archivist" }
|
|
11017
|
+
});
|
|
11014
11018
|
async function kbBranch(config2) {
|
|
11015
11019
|
try {
|
|
11016
11020
|
const { base, headers } = archivistEndpoint(config2);
|
|
11017
|
-
const res = await fetch(`${base}/kb/branch`, { headers });
|
|
11021
|
+
const res = await archivistSpan("kb.branch", () => fetch(`${base}/kb/branch`, { headers }));
|
|
11018
11022
|
if (!res.ok) return void 0;
|
|
11019
11023
|
const { branch } = await res.json();
|
|
11020
11024
|
return branch ?? void 0;
|
|
@@ -11027,7 +11031,7 @@ async function putContent(config2, storageUri, body) {
|
|
|
11027
11031
|
const url = `${base}/content/${encodeURIComponent(storageUri)}`;
|
|
11028
11032
|
let res;
|
|
11029
11033
|
try {
|
|
11030
|
-
res = await fetch(url, { method: "PUT", headers, body });
|
|
11034
|
+
res = await archivistSpan("content.put", () => fetch(url, { method: "PUT", headers, body }));
|
|
11031
11035
|
} catch (error) {
|
|
11032
11036
|
getLogger().error("Archivist content write unreachable", {
|
|
11033
11037
|
component: "archivist-client",
|
|
@@ -11052,7 +11056,7 @@ async function getContent(config2, resourceId2) {
|
|
|
11052
11056
|
const url = `${base}/resources/${encodeURIComponent(resourceId2)}/content`;
|
|
11053
11057
|
let res;
|
|
11054
11058
|
try {
|
|
11055
|
-
res = await fetch(url, { headers });
|
|
11059
|
+
res = await archivistSpan("content.get", () => fetch(url, { headers }));
|
|
11056
11060
|
} catch (error) {
|
|
11057
11061
|
getLogger().error("Archivist content read unreachable", {
|
|
11058
11062
|
component: "archivist-client",
|
|
@@ -11096,7 +11100,7 @@ statusRouter.get("/api/status", async (c) => {
|
|
|
11096
11100
|
const did = kbDid(siteDomain);
|
|
11097
11101
|
const response = {
|
|
11098
11102
|
status: "operational",
|
|
11099
|
-
version: "0.5.
|
|
11103
|
+
version: "0.5.30",
|
|
11100
11104
|
features: {
|
|
11101
11105
|
semanticContent: "planned",
|
|
11102
11106
|
collaboration: "planned",
|
|
@@ -11494,9 +11498,13 @@ init_logger();
|
|
|
11494
11498
|
var getBusLogger = () => getLogger().child({ component: "bus" });
|
|
11495
11499
|
async function fetchArchivistReplay(config2, resourceId2, fromSequence) {
|
|
11496
11500
|
const { base, headers } = archivistEndpoint(config2);
|
|
11497
|
-
const res = await
|
|
11498
|
-
|
|
11499
|
-
|
|
11501
|
+
const res = await withSpan(
|
|
11502
|
+
"archivist.events.replay",
|
|
11503
|
+
() => fetch(
|
|
11504
|
+
`${base}/events/${encodeURIComponent(resourceId2)}?fromSequence=${fromSequence}`,
|
|
11505
|
+
{ headers }
|
|
11506
|
+
),
|
|
11507
|
+
{ kind: SpanKind.CLIENT, attrs: { "peer.service": "archivist" } }
|
|
11500
11508
|
);
|
|
11501
11509
|
if (!res.ok) {
|
|
11502
11510
|
throw new Error(`Archivist replay read failed: ${res.status} ${res.statusText}`);
|
|
@@ -11528,43 +11536,138 @@ function extractSequence(payload) {
|
|
|
11528
11536
|
}
|
|
11529
11537
|
var MAX_SCOPES = 512;
|
|
11530
11538
|
var SCOPE_WARN_THRESHOLD = 128;
|
|
11531
|
-
var
|
|
11532
|
-
|
|
11533
|
-
|
|
11539
|
+
var MAX_PENDING_WRITE_BYTES = 16 * 1024 * 1024;
|
|
11540
|
+
var MAX_REPLAY_BUFFER_EVENTS = 1e3;
|
|
11541
|
+
var CORRELATED_CHANNELS = new Set(replyChannelsFor(Object.keys(BUS_OPERATIONS)));
|
|
11542
|
+
var PROGRESS_CHANNELS = new Set(
|
|
11543
|
+
Object.values(BUS_OPERATIONS).flatMap(
|
|
11544
|
+
(op) => "progress" in op && op.progress ? [op.progress] : []
|
|
11545
|
+
)
|
|
11546
|
+
);
|
|
11534
11547
|
var REPLY_RETENTION_TTL_MS = 6e4;
|
|
11535
11548
|
var REPLY_RETENTION_MAX = 1024;
|
|
11536
11549
|
var PENDING_REPLIES_MAX = 256;
|
|
11537
|
-
|
|
11550
|
+
var CLAIM_TTL_MS = 15 * 6e4;
|
|
11551
|
+
var CLAIM_MAX_GLOBAL = 4096;
|
|
11552
|
+
var correlationIdOf = (payload) => {
|
|
11553
|
+
const cid = payload?.correlationId;
|
|
11554
|
+
return typeof cid === "string" && cid.length > 0 ? cid : void 0;
|
|
11555
|
+
};
|
|
11556
|
+
function createCorrelationRegistry(eventBus2, opts = {}) {
|
|
11538
11557
|
const ttlMs = opts.ttlMs ?? REPLY_RETENTION_TTL_MS;
|
|
11539
11558
|
const max = opts.max ?? REPLY_RETENTION_MAX;
|
|
11559
|
+
const claimTtlMs = opts.claimTtlMs ?? CLAIM_TTL_MS;
|
|
11540
11560
|
const now = opts.now ?? Date.now;
|
|
11541
|
-
const
|
|
11542
|
-
const
|
|
11543
|
-
|
|
11544
|
-
|
|
11545
|
-
|
|
11546
|
-
|
|
11547
|
-
|
|
11548
|
-
|
|
11549
|
-
|
|
11550
|
-
|
|
11551
|
-
|
|
11561
|
+
const claims = /* @__PURE__ */ new Map();
|
|
11562
|
+
const perClient = /* @__PURE__ */ new Map();
|
|
11563
|
+
const release = (clientId) => {
|
|
11564
|
+
const n = (perClient.get(clientId) ?? 1) - 1;
|
|
11565
|
+
if (n <= 0) perClient.delete(clientId);
|
|
11566
|
+
else perClient.set(clientId, n);
|
|
11567
|
+
};
|
|
11568
|
+
const forget = (cid) => {
|
|
11569
|
+
const claim = claims.get(cid);
|
|
11570
|
+
if (!claim) return;
|
|
11571
|
+
claims.delete(cid);
|
|
11572
|
+
if (!claim.answered) release(claim.clientId);
|
|
11573
|
+
};
|
|
11574
|
+
const sweepClaims = () => {
|
|
11575
|
+
const cutoff = now() - claimTtlMs;
|
|
11576
|
+
for (const [cid, claim] of claims) {
|
|
11577
|
+
if (claim.claimedAt > cutoff) break;
|
|
11578
|
+
if (!claim.reply) {
|
|
11579
|
+
getBusLogger().warn("[bus CLAIM-EXPIRED] claim swept with no reply", {
|
|
11580
|
+
correlationId: cid,
|
|
11581
|
+
clientId: claim.clientId,
|
|
11582
|
+
ageMs: now() - claim.claimedAt
|
|
11583
|
+
});
|
|
11552
11584
|
}
|
|
11585
|
+
forget(cid);
|
|
11586
|
+
}
|
|
11587
|
+
};
|
|
11588
|
+
const sweepReplies = () => {
|
|
11589
|
+
const cutoff = now() - ttlMs;
|
|
11590
|
+
let retained = 0;
|
|
11591
|
+
for (const claim of claims.values()) {
|
|
11592
|
+
if (!claim.reply) continue;
|
|
11593
|
+
if (claim.reply.retainedAt <= cutoff) delete claim.reply;
|
|
11594
|
+
else retained++;
|
|
11595
|
+
}
|
|
11596
|
+
if (retained <= max) return;
|
|
11597
|
+
let excess = retained - max;
|
|
11598
|
+
for (const claim of claims.values()) {
|
|
11599
|
+
if (excess === 0) break;
|
|
11600
|
+
if (claim.reply) {
|
|
11601
|
+
delete claim.reply;
|
|
11602
|
+
excess--;
|
|
11603
|
+
}
|
|
11604
|
+
}
|
|
11605
|
+
};
|
|
11606
|
+
const subs = [...CORRELATED_CHANNELS].map(
|
|
11607
|
+
(channel) => eventBus2.get(channel).subscribe((payload) => {
|
|
11608
|
+
const cid = correlationIdOf(payload);
|
|
11609
|
+
if (!cid) return;
|
|
11610
|
+
const claim = claims.get(cid);
|
|
11611
|
+
if (!claim) return;
|
|
11612
|
+
claim.claimedAt = now();
|
|
11613
|
+
if (PROGRESS_CHANNELS.has(channel)) return;
|
|
11614
|
+
if (!claim.answered) {
|
|
11615
|
+
claim.answered = true;
|
|
11616
|
+
release(claim.clientId);
|
|
11617
|
+
}
|
|
11618
|
+
claim.reply = { channel, payload, retainedAt: now() };
|
|
11619
|
+
sweepClaims();
|
|
11620
|
+
sweepReplies();
|
|
11553
11621
|
})
|
|
11554
11622
|
);
|
|
11555
11623
|
return {
|
|
11556
|
-
|
|
11557
|
-
|
|
11558
|
-
|
|
11559
|
-
if (
|
|
11560
|
-
|
|
11624
|
+
claim(cid, clientId, principalDid) {
|
|
11625
|
+
sweepClaims();
|
|
11626
|
+
const existing = claims.get(cid);
|
|
11627
|
+
if (existing) return "conflict";
|
|
11628
|
+
if ((perClient.get(clientId) ?? 0) >= PENDING_REPLIES_MAX) return "at-capacity";
|
|
11629
|
+
if (claims.size >= CLAIM_MAX_GLOBAL) {
|
|
11630
|
+
const oldest = claims.keys().next().value;
|
|
11631
|
+
if (oldest !== void 0) {
|
|
11632
|
+
getBusLogger().warn("[bus CLAIM-EVICTED] global claim cap reached", {
|
|
11633
|
+
correlationId: oldest,
|
|
11634
|
+
cap: CLAIM_MAX_GLOBAL
|
|
11635
|
+
});
|
|
11636
|
+
forget(oldest);
|
|
11637
|
+
}
|
|
11638
|
+
}
|
|
11639
|
+
claims.set(cid, { clientId, principalDid, claimedAt: now() });
|
|
11640
|
+
perClient.set(clientId, (perClient.get(clientId) ?? 0) + 1);
|
|
11641
|
+
return "ok";
|
|
11642
|
+
},
|
|
11643
|
+
owner(cid) {
|
|
11644
|
+
const claim = claims.get(cid);
|
|
11645
|
+
if (!claim) return void 0;
|
|
11646
|
+
if (now() - claim.claimedAt > claimTtlMs) return void 0;
|
|
11647
|
+
return { clientId: claim.clientId, principalDid: claim.principalDid };
|
|
11648
|
+
},
|
|
11649
|
+
lookupReply(cid, clientId, principalDid) {
|
|
11650
|
+
const claim = claims.get(cid);
|
|
11651
|
+
if (!claim?.reply) return void 0;
|
|
11652
|
+
if (claim.clientId !== clientId || claim.principalDid !== principalDid) return void 0;
|
|
11653
|
+
if (now() - claim.reply.retainedAt > ttlMs) {
|
|
11654
|
+
delete claim.reply;
|
|
11561
11655
|
return void 0;
|
|
11562
11656
|
}
|
|
11563
|
-
return
|
|
11657
|
+
return claim.reply;
|
|
11658
|
+
},
|
|
11659
|
+
size() {
|
|
11660
|
+
return claims.size;
|
|
11661
|
+
},
|
|
11662
|
+
occupancy() {
|
|
11663
|
+
let retainedReplies = 0;
|
|
11664
|
+
for (const claim of claims.values()) if (claim.reply) retainedReplies++;
|
|
11665
|
+
return { claims: claims.size, retainedReplies };
|
|
11564
11666
|
},
|
|
11565
11667
|
dispose() {
|
|
11566
|
-
for (const
|
|
11567
|
-
|
|
11668
|
+
for (const sub of subs) sub.unsubscribe();
|
|
11669
|
+
claims.clear();
|
|
11670
|
+
perClient.clear();
|
|
11568
11671
|
}
|
|
11569
11672
|
};
|
|
11570
11673
|
}
|
|
@@ -11573,7 +11676,10 @@ function parseSubscribeBody(raw) {
|
|
|
11573
11676
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
11574
11677
|
return { error: "body must be a JSON object" };
|
|
11575
11678
|
}
|
|
11576
|
-
const { global: rawGlobal, scoped: rawScoped, pendingReplies: rawPending } = raw;
|
|
11679
|
+
const { global: rawGlobal, scoped: rawScoped, pendingReplies: rawPending, clientId } = raw;
|
|
11680
|
+
if (typeof clientId !== "string" || clientId === "") {
|
|
11681
|
+
return { error: "`clientId` is required (BusSubscribeRequest)" };
|
|
11682
|
+
}
|
|
11577
11683
|
const global2 = rawGlobal === void 0 ? [] : rawGlobal;
|
|
11578
11684
|
if (!isStringArray(global2)) return { error: "`global` must be an array of channel names" };
|
|
11579
11685
|
const pendingReplies = rawPending === void 0 ? [] : rawPending;
|
|
@@ -11601,27 +11707,28 @@ function parseSubscribeBody(raw) {
|
|
|
11601
11707
|
if (scoped.length > MAX_SCOPES) {
|
|
11602
11708
|
return { error: `scope count ${scoped.length} exceeds the per-connection cap of ${MAX_SCOPES}` };
|
|
11603
11709
|
}
|
|
11604
|
-
return { global: global2, scoped, pendingReplies };
|
|
11710
|
+
return { global: global2, scoped, pendingReplies, clientId };
|
|
11605
11711
|
}
|
|
11606
11712
|
function createBusRouter(authMiddleware2) {
|
|
11607
11713
|
const busRouter2 = new Hono();
|
|
11608
11714
|
busRouter2.use("/bus/*", authMiddleware2);
|
|
11609
|
-
const
|
|
11715
|
+
const registryByBus = /* @__PURE__ */ new WeakMap();
|
|
11610
11716
|
busRouter2.post("/bus/subscribe", async (c) => {
|
|
11611
11717
|
const raw = await c.req.json().catch(() => null);
|
|
11612
11718
|
const parsed = parseSubscribeBody(raw);
|
|
11613
11719
|
if ("error" in parsed) {
|
|
11614
11720
|
throw new HTTPException(400, { message: parsed.error });
|
|
11615
11721
|
}
|
|
11616
|
-
const { global: channels, scoped, pendingReplies } = parsed;
|
|
11722
|
+
const { global: channels, scoped, pendingReplies, clientId } = parsed;
|
|
11617
11723
|
const eventBus2 = c.get("eventBus");
|
|
11618
11724
|
const subscriberDid = c.get("principalDid");
|
|
11619
|
-
let
|
|
11620
|
-
if (!
|
|
11621
|
-
|
|
11622
|
-
|
|
11725
|
+
let registry = registryByBus.get(eventBus2);
|
|
11726
|
+
if (!registry) {
|
|
11727
|
+
registry = createCorrelationRegistry(eventBus2);
|
|
11728
|
+
registryByBus.set(eventBus2, registry);
|
|
11729
|
+
registerCorrelationRegistryProvider(() => registry.occupancy());
|
|
11623
11730
|
}
|
|
11624
|
-
const
|
|
11731
|
+
const correlations = registry;
|
|
11625
11732
|
if (scoped.length >= SCOPE_WARN_THRESHOLD) {
|
|
11626
11733
|
getBusLogger().warn("large scope matrix", { scopeCount: scoped.length, cap: MAX_SCOPES });
|
|
11627
11734
|
}
|
|
@@ -11641,11 +11748,45 @@ function createBusRouter(authMiddleware2) {
|
|
|
11641
11748
|
const presence = { participant: subscriberDid ?? "", connectionId };
|
|
11642
11749
|
recordSubscriberConnect();
|
|
11643
11750
|
eventBus2.get("session:joined").next(presence);
|
|
11644
|
-
|
|
11751
|
+
const subs = [];
|
|
11752
|
+
let pendingBytes = 0;
|
|
11753
|
+
let tornDown = false;
|
|
11754
|
+
const { outgoing } = c.env ?? {};
|
|
11755
|
+
const teardown = (reason) => {
|
|
11756
|
+
if (tornDown) return;
|
|
11757
|
+
tornDown = true;
|
|
11758
|
+
for (const s of subs) s.unsubscribe();
|
|
11645
11759
|
recordSubscriberDisconnect();
|
|
11646
11760
|
eventBus2.get("session:left").next(presence);
|
|
11647
|
-
getBusLogger().info("SSE disconnect", { connectionId });
|
|
11648
|
-
|
|
11761
|
+
getBusLogger().info("SSE disconnect", { connectionId, reason, pendingBytes });
|
|
11762
|
+
stream.abort();
|
|
11763
|
+
outgoing?.destroy();
|
|
11764
|
+
};
|
|
11765
|
+
stream.onAbort(() => teardown("stream-abort"));
|
|
11766
|
+
c.req.raw.signal.addEventListener("abort", () => stream.abort(), { once: true });
|
|
11767
|
+
if (c.req.raw.signal.aborted) {
|
|
11768
|
+
teardown("pre-aborted");
|
|
11769
|
+
return;
|
|
11770
|
+
}
|
|
11771
|
+
const boundedWrite = async (frame) => {
|
|
11772
|
+
if (tornDown) return;
|
|
11773
|
+
const cost = frame.data.length;
|
|
11774
|
+
pendingBytes += cost;
|
|
11775
|
+
if (pendingBytes > MAX_PENDING_WRITE_BYTES) {
|
|
11776
|
+
getBusLogger().warn("SSE pending-write overflow \u2014 disconnecting dead or stalled subscriber", {
|
|
11777
|
+
connectionId,
|
|
11778
|
+
pendingBytes,
|
|
11779
|
+
cap: MAX_PENDING_WRITE_BYTES
|
|
11780
|
+
});
|
|
11781
|
+
teardown("pending-write-overflow");
|
|
11782
|
+
return;
|
|
11783
|
+
}
|
|
11784
|
+
try {
|
|
11785
|
+
await stream.writeSSE(frame);
|
|
11786
|
+
} finally {
|
|
11787
|
+
pendingBytes -= cost;
|
|
11788
|
+
}
|
|
11789
|
+
};
|
|
11649
11790
|
const lastDeliveredSeq = /* @__PURE__ */ new Map();
|
|
11650
11791
|
const writeBusEvent = async (channel, payload, eventScope) => {
|
|
11651
11792
|
const seq = extractSequence(payload);
|
|
@@ -11666,8 +11807,7 @@ function createBusRouter(authMiddleware2) {
|
|
|
11666
11807
|
}
|
|
11667
11808
|
const data = eventScope ? JSON.stringify({ channel, payload, scope: eventScope }) : JSON.stringify({ channel, payload });
|
|
11668
11809
|
busLog("SSE", channel, payload, eventScope);
|
|
11669
|
-
await
|
|
11670
|
-
});
|
|
11810
|
+
await boundedWrite({ event: "bus-event", data, id });
|
|
11671
11811
|
};
|
|
11672
11812
|
if (typeof cid === "string" && cid.length > 0) {
|
|
11673
11813
|
await withSpan(`sse.deliver:${channel}`, doWrite, {
|
|
@@ -11683,32 +11823,58 @@ function createBusRouter(authMiddleware2) {
|
|
|
11683
11823
|
}
|
|
11684
11824
|
};
|
|
11685
11825
|
const emitResumeGap = async (reason, gapScope, lastSeenId) => {
|
|
11826
|
+
recordResumeGap(reason);
|
|
11686
11827
|
const payload = { reason };
|
|
11687
11828
|
if (gapScope !== void 0) payload.scope = gapScope;
|
|
11688
11829
|
if (lastSeenId !== void 0) payload.lastSeenId = lastSeenId;
|
|
11689
|
-
await
|
|
11830
|
+
await boundedWrite({
|
|
11690
11831
|
event: "bus-event",
|
|
11691
11832
|
data: JSON.stringify({ channel: "bus:resume-gap", payload }),
|
|
11692
11833
|
id: nextEphemeralId()
|
|
11693
|
-
}).catch(() => {
|
|
11694
11834
|
});
|
|
11695
11835
|
};
|
|
11696
11836
|
const liveBuffer = [];
|
|
11697
11837
|
let mode = "live";
|
|
11698
11838
|
const emitOrBuffer = (channel, payload, eventScope) => {
|
|
11699
11839
|
if (mode === "buffering") {
|
|
11840
|
+
if (liveBuffer.length >= MAX_REPLAY_BUFFER_EVENTS) {
|
|
11841
|
+
getBusLogger().warn("SSE replay-buffer overflow \u2014 disconnecting stalled subscriber", {
|
|
11842
|
+
connectionId,
|
|
11843
|
+
cap: MAX_REPLAY_BUFFER_EVENTS
|
|
11844
|
+
});
|
|
11845
|
+
teardown("replay-buffer-overflow");
|
|
11846
|
+
return;
|
|
11847
|
+
}
|
|
11700
11848
|
liveBuffer.push({ channel, payload, scope: eventScope });
|
|
11701
11849
|
} else {
|
|
11702
11850
|
void writeBusEvent(channel, payload, eventScope);
|
|
11703
11851
|
}
|
|
11704
11852
|
};
|
|
11853
|
+
const mayDeliver = (channel, payload) => {
|
|
11854
|
+
const cid = correlationIdOf(payload);
|
|
11855
|
+
if (!cid) {
|
|
11856
|
+
if (!PROGRESS_CHANNELS.has(channel)) {
|
|
11857
|
+
getBusLogger().warn("[bus REPLY-NO-CID] correlated frame without a correlationId", { channel });
|
|
11858
|
+
}
|
|
11859
|
+
return false;
|
|
11860
|
+
}
|
|
11861
|
+
const owner = correlations.owner(cid);
|
|
11862
|
+
if (!owner) return false;
|
|
11863
|
+
if (owner.clientId === clientId && owner.principalDid === subscriberDid) return true;
|
|
11864
|
+
recordReplySuppressed(channel);
|
|
11865
|
+
return false;
|
|
11866
|
+
};
|
|
11705
11867
|
const willReplay = scoped.some((entry) => entry.lastEventId !== void 0);
|
|
11706
11868
|
if (willReplay) mode = "buffering";
|
|
11707
|
-
const
|
|
11708
|
-
|
|
11709
|
-
|
|
11710
|
-
|
|
11711
|
-
|
|
11869
|
+
for (const channel of channels) {
|
|
11870
|
+
const correlated = CORRELATED_CHANNELS.has(channel);
|
|
11871
|
+
subs.push(
|
|
11872
|
+
eventBus2.get(channel).subscribe((payload) => {
|
|
11873
|
+
if (correlated && !mayDeliver(channel, payload)) return;
|
|
11874
|
+
emitOrBuffer(channel, payload, void 0);
|
|
11875
|
+
})
|
|
11876
|
+
);
|
|
11877
|
+
}
|
|
11712
11878
|
for (const entry of scoped) {
|
|
11713
11879
|
const scopedBus = eventBus2.scope(entry.scope);
|
|
11714
11880
|
for (const channel of entry.channels) {
|
|
@@ -11719,8 +11885,8 @@ function createBusRouter(authMiddleware2) {
|
|
|
11719
11885
|
);
|
|
11720
11886
|
}
|
|
11721
11887
|
}
|
|
11722
|
-
stream.onAbort(() => subs.forEach((s) => s.unsubscribe()));
|
|
11723
11888
|
for (const entry of scoped) {
|
|
11889
|
+
if (tornDown) break;
|
|
11724
11890
|
if (entry.lastEventId === void 0) continue;
|
|
11725
11891
|
const parsed2 = parsePersistedId(entry.lastEventId);
|
|
11726
11892
|
if (!parsed2) {
|
|
@@ -11750,18 +11916,19 @@ function createBusRouter(authMiddleware2) {
|
|
|
11750
11916
|
}
|
|
11751
11917
|
}
|
|
11752
11918
|
for (const cid of pendingReplies) {
|
|
11753
|
-
|
|
11919
|
+
if (tornDown) break;
|
|
11920
|
+
const retained = correlations.lookupReply(cid, clientId, subscriberDid);
|
|
11754
11921
|
if (retained) {
|
|
11755
11922
|
await writeBusEvent(retained.channel, retained.payload, void 0);
|
|
11756
11923
|
}
|
|
11757
11924
|
}
|
|
11758
|
-
while (liveBuffer.length > 0) {
|
|
11925
|
+
while (liveBuffer.length > 0 && !tornDown) {
|
|
11759
11926
|
const next = liveBuffer.shift();
|
|
11760
11927
|
await writeBusEvent(next.channel, next.payload, next.scope);
|
|
11761
11928
|
}
|
|
11762
11929
|
mode = "live";
|
|
11763
|
-
while (
|
|
11764
|
-
await
|
|
11930
|
+
while (!tornDown && !stream.aborted && !stream.closed) {
|
|
11931
|
+
await boundedWrite({ event: "ping", data: "" });
|
|
11765
11932
|
await stream.sleep(15e3);
|
|
11766
11933
|
}
|
|
11767
11934
|
});
|
|
@@ -11770,6 +11937,7 @@ function createBusRouter(authMiddleware2) {
|
|
|
11770
11937
|
const eventBus2 = c.get("eventBus");
|
|
11771
11938
|
const body = await c.req.json();
|
|
11772
11939
|
const { channel, payload, scope } = body;
|
|
11940
|
+
const emitterClientId = typeof body.clientId === "string" && body.clientId !== "" ? body.clientId : void 0;
|
|
11773
11941
|
if (!channel || typeof channel !== "string") {
|
|
11774
11942
|
throw new HTTPException(400, { message: "channel is required" });
|
|
11775
11943
|
}
|
|
@@ -11796,6 +11964,30 @@ function createBusRouter(authMiddleware2) {
|
|
|
11796
11964
|
if (principalDid) {
|
|
11797
11965
|
payload._userId = principalDid;
|
|
11798
11966
|
}
|
|
11967
|
+
const claimCid = channel in BUS_OPERATIONS ? correlationIdOf(payload) : void 0;
|
|
11968
|
+
if (claimCid) {
|
|
11969
|
+
const clientId = emitterClientId;
|
|
11970
|
+
if (clientId === void 0) {
|
|
11971
|
+
throw new HTTPException(400, {
|
|
11972
|
+
message: `clientId is required to emit ${channel} with a correlationId`
|
|
11973
|
+
});
|
|
11974
|
+
}
|
|
11975
|
+
let registry = registryByBus.get(eventBus2);
|
|
11976
|
+
if (!registry) {
|
|
11977
|
+
registry = createCorrelationRegistry(eventBus2);
|
|
11978
|
+
registryByBus.set(eventBus2, registry);
|
|
11979
|
+
}
|
|
11980
|
+
const outcome = registry.claim(claimCid, clientId, principalDid);
|
|
11981
|
+
if (outcome === "conflict") {
|
|
11982
|
+
getBusLogger().warn("[bus CLAIM-CONFLICT] correlationId already claimed", { channel, correlationId: claimCid });
|
|
11983
|
+
throw new HTTPException(409, { message: `correlationId ${claimCid} is already claimed` });
|
|
11984
|
+
}
|
|
11985
|
+
if (outcome === "at-capacity") {
|
|
11986
|
+
throw new HTTPException(429, {
|
|
11987
|
+
message: `client has ${PENDING_REPLIES_MAX} unanswered requests; retry when one settles`
|
|
11988
|
+
});
|
|
11989
|
+
}
|
|
11990
|
+
}
|
|
11799
11991
|
const traceparent = c.req.header("traceparent");
|
|
11800
11992
|
const tracestate = c.req.header("tracestate");
|
|
11801
11993
|
const carrier = traceparent ? tracestate ? { traceparent, tracestate } : { traceparent } : void 0;
|
|
@@ -11811,13 +12003,35 @@ function createBusRouter(authMiddleware2) {
|
|
|
11811
12003
|
subject.next(payload);
|
|
11812
12004
|
busLog("EMIT", channel, payload, scope);
|
|
11813
12005
|
recordBusEmit(channel, scope);
|
|
11814
|
-
getBusLogger().info("emit", {
|
|
12006
|
+
getBusLogger().info("emit", {
|
|
12007
|
+
channel,
|
|
12008
|
+
scope,
|
|
12009
|
+
subscribers,
|
|
12010
|
+
clientId: emitterClientId,
|
|
12011
|
+
correlationId: payload.correlationId
|
|
12012
|
+
});
|
|
11815
12013
|
if (subscribers === 0) {
|
|
11816
12014
|
getBusLogger().warn("emit reached no subscribers", {
|
|
11817
12015
|
channel,
|
|
11818
12016
|
scope,
|
|
11819
12017
|
hint: "Nothing on this gateway subscribes to that channel. For a UI signal meant to cross to a participant, check that the channel is in BRIDGED_BROADCASTS and that a client subscribed to it."
|
|
11820
12018
|
});
|
|
12019
|
+
const operation = BUS_OPERATIONS[channel];
|
|
12020
|
+
const failureCid = correlationIdOf(payload);
|
|
12021
|
+
if (operation?.failure && failureCid) {
|
|
12022
|
+
const { _userId: _injected, ...echo } = payload;
|
|
12023
|
+
const failure = {
|
|
12024
|
+
...echo,
|
|
12025
|
+
message: `No subscriber for ${channel}: the service that answers it is not connected`
|
|
12026
|
+
};
|
|
12027
|
+
recordUnanswerableRequest(channel);
|
|
12028
|
+
getBusLogger().warn("[bus UNANSWERABLE] synthesizing failure for an unsubscribed request", {
|
|
12029
|
+
channel,
|
|
12030
|
+
failureChannel: operation.failure,
|
|
12031
|
+
correlationId: failureCid
|
|
12032
|
+
});
|
|
12033
|
+
eventBus2.get(operation.failure).next(failure);
|
|
12034
|
+
}
|
|
11821
12035
|
}
|
|
11822
12036
|
},
|
|
11823
12037
|
{
|
|
@@ -12032,7 +12246,7 @@ app.get("/api/openapi.json", (c) => {
|
|
|
12032
12246
|
const openApiPath = fs.existsSync(distPath) ? distPath : path.join(__dirname$1, "../../../specs/openapi.json");
|
|
12033
12247
|
const openApiContent = fs.readFileSync(openApiPath, "utf-8");
|
|
12034
12248
|
const openApiSpec = JSON.parse(openApiContent);
|
|
12035
|
-
openApiSpec.info = { ...openApiSpec.info, version: "0.5.
|
|
12249
|
+
openApiSpec.info = { ...openApiSpec.info, version: "0.5.30" };
|
|
12036
12250
|
const port2 = gatewayService.port || 4e3;
|
|
12037
12251
|
const apiUrl = gatewayService.publicURL || `http://localhost:${port2}`;
|
|
12038
12252
|
if (apiUrl) {
|