@semiont/gateway 0.5.28 → 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 CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import winston from 'winston';
3
- import { recordSubscriberConnect, recordSubscriberDisconnect, withTraceparent, withSpan, recordBusEmit, SpanKind, injectTraceparent, getLogTraceContext } from '@semiont/observability';
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.28",
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.28",
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 fetch(
11498
- `${base}/events/${encodeURIComponent(resourceId2)}?fromSequence=${fromSequence}`,
11499
- { headers }
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 REPLY_CHANNELS = [
11532
- ...new Set(Object.values(BUS_OPERATIONS).flatMap((op) => [op.result, op.failure]))
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
- function createReplyRetention(eventBus2, opts = {}) {
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 buffer = /* @__PURE__ */ new Map();
11542
- const subs = REPLY_CHANNELS.map(
11543
- (channel) => eventBus2.get(channel).subscribe((payload) => {
11544
- const cid = payload?.correlationId;
11545
- if (typeof cid !== "string" || cid.length === 0) return;
11546
- buffer.delete(cid);
11547
- buffer.set(cid, { channel, payload, retainedAt: now() });
11548
- while (buffer.size > max) {
11549
- const oldest = buffer.keys().next().value;
11550
- if (oldest === void 0) break;
11551
- buffer.delete(oldest);
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
- lookup(correlationId) {
11557
- const entry = buffer.get(correlationId);
11558
- if (!entry) return void 0;
11559
- if (now() - entry.retainedAt > ttlMs) {
11560
- buffer.delete(correlationId);
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 entry;
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 s of subs) s.unsubscribe();
11567
- buffer.clear();
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 retentionByBus = /* @__PURE__ */ new WeakMap();
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 retention = retentionByBus.get(eventBus2);
11620
- if (!retention) {
11621
- retention = createReplyRetention(eventBus2);
11622
- retentionByBus.set(eventBus2, retention);
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 replyRetention = retention;
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
- stream.onAbort(() => {
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 stream.writeSSE({ event: "bus-event", data, id }).catch(() => {
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 stream.writeSSE({
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 subs = channels.map(
11708
- (channel) => eventBus2.get(channel).subscribe((payload) => {
11709
- emitOrBuffer(channel, payload, void 0);
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
- const retained = replyRetention.lookup(cid);
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 (true) {
11764
- await stream.writeSSE({ event: "ping", data: "" });
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", { channel, scope, subscribers, correlationId: payload.correlationId });
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.28" };
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) {