dsh-interconnect 0.10.1 → 0.11.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/README.md +15 -13
- package/assets/dsh-interconnect.md +5 -5
- package/dsh.plugin.json +2 -2
- package/lib/index.js +270 -152
- package/lib/interconnect/index.js +272 -153
- package/lib/interconnect/index.js.map +3 -3
- package/lib/skill-interconnect/index.js +1 -1
- package/lib/skill-interconnect/index.js.map +2 -2
- package/lib/tool-interconnect/index.js +70 -58
- package/lib/tool-interconnect/index.js.map +2 -2
- package/lib/types/interconnect/index.d.ts +64 -26
- package/lib/types/interconnect/index.d.ts.map +1 -1
- package/lib/types/interconnect/types.d.ts +7 -47
- package/lib/types/interconnect/types.d.ts.map +1 -1
- package/lib/types/skill-interconnect/index.d.ts.map +1 -1
- package/lib/types/tool-interconnect/index.d.ts +7 -3
- package/lib/types/tool-interconnect/index.d.ts.map +1 -1
- package/package.json +2 -1
package/lib/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/interconnect/index.ts
|
|
2
2
|
import { Service } from "@deepseek-ai/cordis";
|
|
3
|
+
import { createHash, randomUUID, timingSafeEqual as constantTimeEqual } from "node:crypto";
|
|
3
4
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
4
5
|
import { boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
5
6
|
|
|
@@ -818,11 +819,6 @@ defineMethod("transform", [
|
|
|
818
819
|
|
|
819
820
|
// src/interconnect/index.ts
|
|
820
821
|
import WebSocket, { WebSocketServer } from "ws";
|
|
821
|
-
|
|
822
|
-
// src/interconnect/types.ts
|
|
823
|
-
var INTERCONNECT_TOKEN_REF = "DSH_INTERCONNECT_TOKEN";
|
|
824
|
-
|
|
825
|
-
// src/interconnect/index.ts
|
|
826
822
|
function isSessionOwnedBySubagent(ctx, session, agent) {
|
|
827
823
|
if (session.header.origin === "subagent") return true;
|
|
828
824
|
const parentId = session.header.parentSession;
|
|
@@ -830,59 +826,74 @@ function isSessionOwnedBySubagent(ctx, session, agent) {
|
|
|
830
826
|
const parent = ctx.agents.get(parentId);
|
|
831
827
|
return parent !== void 0 && ctx.agents.isOwnedBy(agent.id, parent);
|
|
832
828
|
}
|
|
829
|
+
function assertNever(value) {
|
|
830
|
+
throw new Error(`interconnect: unhandled variant ${JSON.stringify(value)}`);
|
|
831
|
+
}
|
|
832
|
+
var INTERCONNECT_CHANNEL = "/interconnect";
|
|
833
|
+
var INTERCONNECT_TOKEN_REF = "DSH_INTERCONNECT_TOKEN";
|
|
833
834
|
var PLUGIN_SOURCE = "dsh-interconnect";
|
|
835
|
+
var MAX_LINK_FRAME_BYTES = 1024 * 1024;
|
|
836
|
+
var LINK_HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
834
837
|
var LINK_CHANNEL = "/interconnect/link";
|
|
835
838
|
var notificationSchema = Schema.union([
|
|
836
|
-
Schema.object({ kind: Schema.const("agent/created"), sessionId: Schema.string() }),
|
|
837
|
-
Schema.object({ kind: Schema.const("agent/disposed"), sessionId: Schema.string() }),
|
|
838
|
-
Schema.object({ kind: Schema.const("agent/status"), sessionId: Schema.string(), status: Schema.union([Schema.const("idle"), Schema.const("running")]) }),
|
|
839
|
-
Schema.object({ kind: Schema.const("session/created"), sessionId: Schema.string(), parentSessionId: Schema.string() }),
|
|
840
|
-
Schema.object({ kind: Schema.const("session/disposed"), sessionId: Schema.string() }),
|
|
841
|
-
Schema.object({ kind: Schema.const("subagent/end"), provider: Schema.string(), childSessionId: Schema.string(), stopReason: Schema.string() })
|
|
839
|
+
Schema.object({ kind: Schema.const("agent/created").required(), sessionId: Schema.string().required() }),
|
|
840
|
+
Schema.object({ kind: Schema.const("agent/disposed").required(), sessionId: Schema.string().required() }),
|
|
841
|
+
Schema.object({ kind: Schema.const("agent/status").required(), sessionId: Schema.string().required(), status: Schema.union([Schema.const("idle"), Schema.const("running")]).required() }),
|
|
842
|
+
Schema.object({ kind: Schema.const("session/created").required(), sessionId: Schema.string().required(), parentSessionId: Schema.string() }),
|
|
843
|
+
Schema.object({ kind: Schema.const("session/disposed").required(), sessionId: Schema.string().required() }),
|
|
844
|
+
Schema.object({ kind: Schema.const("subagent/end").required(), provider: Schema.string().required(), childSessionId: Schema.string().required(), stopReason: Schema.string().required() })
|
|
842
845
|
]);
|
|
843
846
|
var senderSchema = Schema.object({ instanceId: Schema.string(), sessionId: Schema.string() });
|
|
844
|
-
var
|
|
847
|
+
var queryResultSchema = Schema.union([
|
|
848
|
+
Schema.object({ pong: Schema.const(true).required(), instance: Schema.string().required() }),
|
|
845
849
|
Schema.object({
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
850
|
+
instance: Schema.string().required(),
|
|
851
|
+
sessions: Schema.array(Schema.object({
|
|
852
|
+
sessionId: Schema.string().required(),
|
|
853
|
+
title: Schema.string().required(false),
|
|
854
|
+
status: Schema.string().required(false)
|
|
855
|
+
})).required()
|
|
852
856
|
}),
|
|
853
|
-
Schema.object({
|
|
854
|
-
kind: Schema.const("reply"),
|
|
855
|
-
sessionId: Schema.string(),
|
|
856
|
-
text: Schema.string(),
|
|
857
|
-
sender: senderSchema,
|
|
858
|
-
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]),
|
|
859
|
-
resume: Schema.boolean()
|
|
860
|
-
})
|
|
857
|
+
Schema.object({ accepted: Schema.const(true).required() })
|
|
861
858
|
]);
|
|
859
|
+
var messageSchema = Schema.object({
|
|
860
|
+
kind: Schema.const("send").required(),
|
|
861
|
+
sessionId: Schema.string().required(),
|
|
862
|
+
text: Schema.string().required(),
|
|
863
|
+
sender: senderSchema,
|
|
864
|
+
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]),
|
|
865
|
+
resume: Schema.boolean()
|
|
866
|
+
});
|
|
862
867
|
var querySchema = Schema.union([
|
|
863
|
-
Schema.object({ kind: Schema.const("ping") }),
|
|
864
|
-
Schema.object({ kind: Schema.const("list") }),
|
|
865
|
-
Schema.object({ kind: Schema.const("event"), notification: notificationSchema })
|
|
868
|
+
Schema.object({ kind: Schema.const("ping").required() }),
|
|
869
|
+
Schema.object({ kind: Schema.const("list").required() }),
|
|
870
|
+
Schema.object({ kind: Schema.const("event").required(), notification: notificationSchema.required() })
|
|
871
|
+
]);
|
|
872
|
+
var msgResultSchema = Schema.union([
|
|
873
|
+
Schema.object({ delivered: Schema.const(true).required(), instance: Schema.string().required(), delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]) }),
|
|
874
|
+
Schema.object({ delivered: Schema.const(false).required(), instance: Schema.string().required(), reason: Schema.union([
|
|
875
|
+
Schema.const("session-not-live"),
|
|
876
|
+
Schema.const("unreachable"),
|
|
877
|
+
Schema.const("resume-refused"),
|
|
878
|
+
Schema.const("resume-failed"),
|
|
879
|
+
Schema.const("session-owned-by-subagent"),
|
|
880
|
+
Schema.const("no-sender-known")
|
|
881
|
+
]).required() })
|
|
866
882
|
]);
|
|
867
883
|
var linkFrameSchema = Schema.union([
|
|
868
|
-
Schema.object({ type: Schema.const("hello"), sender: Schema.string() }),
|
|
869
|
-
Schema.object({ type: Schema.const("event"), notification: notificationSchema }),
|
|
870
|
-
Schema.object({ type: Schema.const("msg"), reqId: Schema.string(), message: messageSchema }),
|
|
871
|
-
Schema.object({ type: Schema.const("msg-result"), reqId: Schema.string(), result:
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]),
|
|
875
|
-
reason: Schema.string()
|
|
876
|
-
}) }),
|
|
877
|
-
Schema.object({ type: Schema.const("query"), reqId: Schema.string(), query: querySchema }),
|
|
878
|
-
Schema.object({ type: Schema.const("query-result"), reqId: Schema.string(), result: Schema.any() })
|
|
884
|
+
Schema.object({ type: Schema.const("hello").required(), sender: Schema.string().required() }),
|
|
885
|
+
Schema.object({ type: Schema.const("event").required(), notification: notificationSchema.required() }),
|
|
886
|
+
Schema.object({ type: Schema.const("msg").required(), reqId: Schema.string().required(), message: messageSchema.required() }),
|
|
887
|
+
Schema.object({ type: Schema.const("msg-result").required(), reqId: Schema.string().required(), result: msgResultSchema.required() }),
|
|
888
|
+
Schema.object({ type: Schema.const("query").required(), reqId: Schema.string().required(), query: querySchema.required() }),
|
|
889
|
+
Schema.object({ type: Schema.const("query-result").required(), reqId: Schema.string().required(), result: queryResultSchema.required() })
|
|
879
890
|
]);
|
|
880
891
|
var InterconnectService = class extends Service {
|
|
881
|
-
static inject = ["
|
|
892
|
+
static inject = ["agents", "credentials"];
|
|
882
893
|
static Config = Schema.object({
|
|
883
894
|
instanceId: Schema.string().default("dsh"),
|
|
884
895
|
requestTimeoutMs: Schema.natural().max(6e4).default(1e4),
|
|
885
|
-
peers: Schema.
|
|
896
|
+
peers: Schema.dict(Schema.string()).default({}),
|
|
886
897
|
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]).default("followup"),
|
|
887
898
|
allowResume: Schema.boolean().default(true)
|
|
888
899
|
});
|
|
@@ -891,7 +902,7 @@ var InterconnectService = class extends Service {
|
|
|
891
902
|
delivery;
|
|
892
903
|
allowResume;
|
|
893
904
|
subscriptions = [];
|
|
894
|
-
server = new WebSocketServer({ noServer: true });
|
|
905
|
+
server = new WebSocketServer({ noServer: true, maxPayload: MAX_LINK_FRAME_BYTES });
|
|
895
906
|
sockets = /* @__PURE__ */ new Set();
|
|
896
907
|
/** Outbound peer links keyed by the peer's `instanceId`. */
|
|
897
908
|
linkStates = /* @__PURE__ */ new Map();
|
|
@@ -903,12 +914,17 @@ var InterconnectService = class extends Service {
|
|
|
903
914
|
/** In-flight frames sent over a peer link, keyed by `reqId`, awaiting a correlated result. */
|
|
904
915
|
pendingMessages = /* @__PURE__ */ new Map();
|
|
905
916
|
reqIdCounter = 0;
|
|
917
|
+
/** Set when the service fiber unwinds; guards late inbound deliveries against driving disposed agents. */
|
|
918
|
+
disposed = false;
|
|
906
919
|
constructor(ctx, config) {
|
|
907
920
|
super(ctx, "interconnect");
|
|
908
921
|
this.instanceId = config.instanceId;
|
|
909
922
|
this.requestTimeoutMs = config.requestTimeoutMs;
|
|
910
923
|
this.delivery = config.delivery ?? "followup";
|
|
911
924
|
this.allowResume = config.allowResume ?? true;
|
|
925
|
+
for (const origin of Object.values(config.peers ?? {})) {
|
|
926
|
+
linkUrl(trimBase(origin));
|
|
927
|
+
}
|
|
912
928
|
for (const [peerInstanceId, origin] of Object.entries(config.peers ?? {})) {
|
|
913
929
|
this.link(peerInstanceId, origin);
|
|
914
930
|
}
|
|
@@ -918,9 +934,15 @@ var InterconnectService = class extends Service {
|
|
|
918
934
|
void this.handleUpgrade(req, socket, head);
|
|
919
935
|
}
|
|
920
936
|
};
|
|
921
|
-
ctx.
|
|
937
|
+
ctx.inject(["webServer"], (webCtx) => {
|
|
938
|
+
webCtx.effect(() => webCtx.webServer.registerUpgrade(upgrade), "interconnect: /interconnect/link websocket");
|
|
939
|
+
});
|
|
922
940
|
this.heartbeatTimer = setInterval(() => {
|
|
923
941
|
for (const socket of this.sockets) {
|
|
942
|
+
if (socket.readyState !== WebSocket.OPEN) {
|
|
943
|
+
this.sockets.delete(socket);
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
924
946
|
if (socket.isAlive === false) {
|
|
925
947
|
socket.terminate();
|
|
926
948
|
this.sockets.delete(socket);
|
|
@@ -949,6 +971,7 @@ var InterconnectService = class extends Service {
|
|
|
949
971
|
});
|
|
950
972
|
}));
|
|
951
973
|
this.subscriptions.push(ctx.on("session/disposed", (session) => {
|
|
974
|
+
this.senders.delete(String(session.id));
|
|
952
975
|
this.fanout({ kind: "session/disposed", sessionId: String(session.id) });
|
|
953
976
|
}));
|
|
954
977
|
this.subscriptions.push(ctx.on("subagent/end", (info) => {
|
|
@@ -970,16 +993,28 @@ var InterconnectService = class extends Service {
|
|
|
970
993
|
for (const socket of this.sockets) socket.terminate();
|
|
971
994
|
this.sockets.clear();
|
|
972
995
|
this.server.close();
|
|
996
|
+
for (const pending of this.pendingMessages.values()) {
|
|
997
|
+
clearTimeout(pending.timer);
|
|
998
|
+
pending.resolve?.(void 0);
|
|
999
|
+
}
|
|
1000
|
+
this.pendingMessages.clear();
|
|
1001
|
+
this.disposed = true;
|
|
973
1002
|
}, "interconnect: websocket teardown");
|
|
974
1003
|
}
|
|
975
1004
|
/**
|
|
976
1005
|
* This instance's identity to attach to outbound messages. Always present:
|
|
977
1006
|
* it needs no address, only this instance's id and the calling session.
|
|
978
1007
|
* @param sessionId - the local session that is sending, used as the reply target.
|
|
1008
|
+
* @returns this instance's address-free identity for the calling session.
|
|
979
1009
|
*/
|
|
980
1010
|
selfSender(sessionId) {
|
|
981
1011
|
return { instanceId: this.instanceId, sessionId };
|
|
982
1012
|
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Deliver one text message to a live session on a peer instance.
|
|
1015
|
+
* @param request - the peer instance id, target session, text, and optional delivery/resume overrides.
|
|
1016
|
+
* @returns the peer's answer, or an unreachable result when no link answers.
|
|
1017
|
+
*/
|
|
983
1018
|
async send(request) {
|
|
984
1019
|
const payload = {
|
|
985
1020
|
sessionId: request.sessionId,
|
|
@@ -988,14 +1023,20 @@ var InterconnectService = class extends Service {
|
|
|
988
1023
|
...request.delivery === void 0 ? {} : { delivery: request.delivery },
|
|
989
1024
|
...request.resume === void 0 ? {} : { resume: request.resume }
|
|
990
1025
|
};
|
|
991
|
-
|
|
992
|
-
|
|
1026
|
+
try {
|
|
1027
|
+
const result = await this.msgRequest(request.instanceId, "send", payload);
|
|
1028
|
+
return result ?? { delivered: false, instance: request.instanceId, reason: "unreachable" };
|
|
1029
|
+
} catch {
|
|
1030
|
+
return { delivered: false, instance: request.instanceId, reason: "unreachable" };
|
|
1031
|
+
}
|
|
993
1032
|
}
|
|
994
1033
|
/**
|
|
995
1034
|
* Deliver one text message back to the peer that a local session last
|
|
996
1035
|
* received a send from. `sessionId` names the LOCAL replying session; the
|
|
997
1036
|
* outbound target is the `sender` that session recorded, addressed through
|
|
998
1037
|
* this instance's own link to the sender's instance.
|
|
1038
|
+
* @param request - the LOCAL replying session id, the reply text, and optional delivery/resume overrides.
|
|
1039
|
+
* @returns the recalled sender's answer, or an unreachable result when no link answers.
|
|
999
1040
|
*/
|
|
1000
1041
|
async reply(request) {
|
|
1001
1042
|
const sender = this.senders.get(request.sessionId);
|
|
@@ -1011,13 +1052,19 @@ var InterconnectService = class extends Service {
|
|
|
1011
1052
|
// back — chaining the conversation.
|
|
1012
1053
|
sender: this.selfSender(request.sessionId)
|
|
1013
1054
|
};
|
|
1014
|
-
|
|
1015
|
-
|
|
1055
|
+
try {
|
|
1056
|
+
const result = await this.msgRequest(sender.instanceId, "send", payload);
|
|
1057
|
+
return result ?? { delivered: false, instance: sender.instanceId, reason: "unreachable" };
|
|
1058
|
+
} catch {
|
|
1059
|
+
return { delivered: false, instance: sender.instanceId, reason: "unreachable" };
|
|
1060
|
+
}
|
|
1016
1061
|
}
|
|
1017
1062
|
/**
|
|
1018
1063
|
* Probe a peer instance for liveness and identity over its persistent link.
|
|
1019
1064
|
* Returns the peer identity when reachable, or undefined when the link is
|
|
1020
1065
|
* not up or no answer arrives.
|
|
1066
|
+
* @param instanceId - the peer's `instanceId` as configured under {@link Config.peers}.
|
|
1067
|
+
* @returns the peer's identity, or undefined when the link is not up or no answer arrives.
|
|
1021
1068
|
*/
|
|
1022
1069
|
async ping(instanceId) {
|
|
1023
1070
|
return this.queryRequest(instanceId, { kind: "ping" });
|
|
@@ -1025,15 +1072,12 @@ var InterconnectService = class extends Service {
|
|
|
1025
1072
|
/**
|
|
1026
1073
|
* List a peer instance's live sessions over its persistent link. Undefined on
|
|
1027
1074
|
* transport failure, matching `ping`.
|
|
1075
|
+
* @param instanceId - the peer's `instanceId` as configured under {@link Config.peers}.
|
|
1076
|
+
* @returns the peer's live session rows, or undefined on transport failure.
|
|
1028
1077
|
*/
|
|
1029
1078
|
async list(instanceId) {
|
|
1030
1079
|
return this.queryRequest(instanceId, { kind: "list" });
|
|
1031
1080
|
}
|
|
1032
|
-
/** The origin this instance dials to reach a configured peer, or undefined. */
|
|
1033
|
-
originOf(instanceId) {
|
|
1034
|
-
const state = this.linkStates.get(instanceId);
|
|
1035
|
-
return state?.peer;
|
|
1036
|
-
}
|
|
1037
1081
|
/**
|
|
1038
1082
|
* Send a `msg` frame over the live link to a peer and resolve with its
|
|
1039
1083
|
* correlated `msg-result`. Undefined when the peer has no live link here
|
|
@@ -1043,7 +1087,7 @@ var InterconnectService = class extends Service {
|
|
|
1043
1087
|
async msgRequest(instanceId, kind, payload) {
|
|
1044
1088
|
const state = this.linkStates.get(instanceId);
|
|
1045
1089
|
if (state === void 0 || !state.writable()) return void 0;
|
|
1046
|
-
const reqId = `m${++this.reqIdCounter}-${
|
|
1090
|
+
const reqId = `m${++this.reqIdCounter}-${randomUUID()}`;
|
|
1047
1091
|
const message = {
|
|
1048
1092
|
kind,
|
|
1049
1093
|
sessionId: payload.sessionId,
|
|
@@ -1052,7 +1096,7 @@ var InterconnectService = class extends Service {
|
|
|
1052
1096
|
...payload.delivery === void 0 ? {} : { delivery: payload.delivery },
|
|
1053
1097
|
...payload.resume === void 0 ? {} : { resume: payload.resume }
|
|
1054
1098
|
};
|
|
1055
|
-
return this.waitForResult(reqId, (failure) => {
|
|
1099
|
+
return this.waitForResult(reqId, "msg", (failure) => {
|
|
1056
1100
|
const wrote = state.sendFrame({ type: "msg", reqId, message });
|
|
1057
1101
|
if (!wrote) failure(new Error(`interconnect: peer link to ${instanceId} closed while sending`));
|
|
1058
1102
|
});
|
|
@@ -1061,32 +1105,34 @@ var InterconnectService = class extends Service {
|
|
|
1061
1105
|
async queryRequest(instanceId, query) {
|
|
1062
1106
|
const state = this.linkStates.get(instanceId);
|
|
1063
1107
|
if (state === void 0 || !state.writable()) return void 0;
|
|
1064
|
-
const reqId = `q${++this.reqIdCounter}-${
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1108
|
+
const reqId = `q${++this.reqIdCounter}-${randomUUID()}`;
|
|
1109
|
+
const accept = query.kind === "ping" ? (result) => typeof result === "object" && result !== null && result.pong === true && typeof result.instance === "string" : (result) => typeof result === "object" && result !== null && Array.isArray(result.sessions) && typeof result.instance === "string";
|
|
1110
|
+
try {
|
|
1111
|
+
return await this.waitForResult(reqId, "query", (failure) => {
|
|
1112
|
+
const wrote = state.sendFrame({ type: "query", reqId, query });
|
|
1113
|
+
if (!wrote) failure(new Error(`interconnect: peer link to ${instanceId} closed while querying`));
|
|
1114
|
+
}, accept);
|
|
1115
|
+
} catch {
|
|
1116
|
+
return void 0;
|
|
1117
|
+
}
|
|
1069
1118
|
}
|
|
1070
1119
|
/** Settle a `reqId` frame against its `*-result`, timing out at `requestTimeoutMs`. */
|
|
1071
|
-
waitForResult(reqId, send) {
|
|
1120
|
+
waitForResult(reqId, kind, send, accept) {
|
|
1072
1121
|
return new Promise((resolve2, reject) => {
|
|
1073
1122
|
const timer = setTimeout(() => {
|
|
1074
1123
|
this.pendingMessages.delete(reqId);
|
|
1075
1124
|
reject(new Error(`interconnect: no result for ${reqId} within ${this.requestTimeoutMs}ms`));
|
|
1076
1125
|
}, this.requestTimeoutMs);
|
|
1077
|
-
this.pendingMessages.set(reqId, {
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
this.pendingMessages.delete(reqId);
|
|
1088
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
1089
|
-
};
|
|
1126
|
+
this.pendingMessages.set(reqId, {
|
|
1127
|
+
timer,
|
|
1128
|
+
kind,
|
|
1129
|
+
...accept === void 0 ? {} : { accept },
|
|
1130
|
+
resolve: (result) => {
|
|
1131
|
+
clearTimeout(timer);
|
|
1132
|
+
this.pendingMessages.delete(reqId);
|
|
1133
|
+
resolve2(result);
|
|
1134
|
+
}
|
|
1135
|
+
});
|
|
1090
1136
|
send((error) => {
|
|
1091
1137
|
clearTimeout(timer);
|
|
1092
1138
|
this.pendingMessages.delete(reqId);
|
|
@@ -1102,12 +1148,20 @@ var InterconnectService = class extends Service {
|
|
|
1102
1148
|
* @returns disposer removing the peer route.
|
|
1103
1149
|
*/
|
|
1104
1150
|
subscribe(instanceId, origin) {
|
|
1105
|
-
this.link(instanceId, origin);
|
|
1151
|
+
const linked = this.link(instanceId, origin);
|
|
1152
|
+
const dialedOrigin = trimBase(origin);
|
|
1106
1153
|
return () => {
|
|
1107
|
-
this.
|
|
1154
|
+
const state = this.linkStates.get(instanceId);
|
|
1155
|
+
if (state === linked && state.peer === dialedOrigin) {
|
|
1156
|
+
state.close();
|
|
1157
|
+
this.linkStates.delete(instanceId);
|
|
1158
|
+
}
|
|
1108
1159
|
};
|
|
1109
1160
|
}
|
|
1110
|
-
/**
|
|
1161
|
+
/**
|
|
1162
|
+
* Remove a peer route, closing its outbound link.
|
|
1163
|
+
* @param instanceId - the peer route to remove.
|
|
1164
|
+
*/
|
|
1111
1165
|
unsubscribe(instanceId) {
|
|
1112
1166
|
const state = this.linkStates.get(instanceId);
|
|
1113
1167
|
if (state !== void 0) state.close();
|
|
@@ -1124,19 +1178,23 @@ var InterconnectService = class extends Service {
|
|
|
1124
1178
|
*/
|
|
1125
1179
|
link(instanceId, origin) {
|
|
1126
1180
|
const existing = this.linkStates.get(instanceId);
|
|
1127
|
-
if (existing !== void 0) {
|
|
1181
|
+
if (existing !== void 0 && !existing.isClosed()) {
|
|
1128
1182
|
existing.reroute(trimBase(origin));
|
|
1129
1183
|
return existing;
|
|
1130
1184
|
}
|
|
1131
|
-
const state = new LinkState(
|
|
1185
|
+
const state = new LinkState(
|
|
1186
|
+
(socket) => {
|
|
1187
|
+
this.attachSocket(socket);
|
|
1188
|
+
},
|
|
1189
|
+
this.ctx.logger,
|
|
1190
|
+
() => this.resolveTokenForDial(),
|
|
1191
|
+
instanceId,
|
|
1192
|
+
trimBase(origin)
|
|
1193
|
+
);
|
|
1132
1194
|
this.linkStates.set(instanceId, state);
|
|
1133
1195
|
state.dial();
|
|
1134
1196
|
return state;
|
|
1135
1197
|
}
|
|
1136
|
-
/** Remove a closed outbound link's state so a later `link` re-dials fresh. */
|
|
1137
|
-
forgetLink(instanceId) {
|
|
1138
|
-
this.linkStates.delete(instanceId);
|
|
1139
|
-
}
|
|
1140
1198
|
/** Push one serialized lifecycle fact out to every linked peer over WS. */
|
|
1141
1199
|
fanout(notification) {
|
|
1142
1200
|
this.broadcast(notification);
|
|
@@ -1146,11 +1204,15 @@ var InterconnectService = class extends Service {
|
|
|
1146
1204
|
if (this.sockets.size === 0) return;
|
|
1147
1205
|
const frame = { type: "event", notification };
|
|
1148
1206
|
const encoded = JSON.stringify(frame);
|
|
1207
|
+
const sent = /* @__PURE__ */ new Set();
|
|
1149
1208
|
for (const socket of this.sockets) {
|
|
1150
1209
|
if (socket.readyState !== WebSocket.OPEN) {
|
|
1151
1210
|
this.sockets.delete(socket);
|
|
1152
1211
|
continue;
|
|
1153
1212
|
}
|
|
1213
|
+
const peer = this.peerOf.get(socket);
|
|
1214
|
+
if (peer !== void 0 && sent.has(peer)) continue;
|
|
1215
|
+
if (peer !== void 0) sent.add(peer);
|
|
1154
1216
|
socket.send(encoded);
|
|
1155
1217
|
}
|
|
1156
1218
|
}
|
|
@@ -1160,27 +1222,18 @@ var InterconnectService = class extends Service {
|
|
|
1160
1222
|
* result so a throwing handler cannot escape the socket's message callback.
|
|
1161
1223
|
*/
|
|
1162
1224
|
async handleMsgFrame(socket, reqId, message) {
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
} else {
|
|
1172
|
-
result = await this.deliver({
|
|
1173
|
-
sessionId: message.sessionId,
|
|
1174
|
-
text: message.text,
|
|
1175
|
-
...message.sender === void 0 ? {} : { sender: message.sender },
|
|
1176
|
-
...message.delivery === void 0 ? {} : { delivery: message.delivery },
|
|
1177
|
-
...message.resume === void 0 ? {} : { resume: message.resume }
|
|
1178
|
-
});
|
|
1179
|
-
}
|
|
1225
|
+
const result = await this.deliver({
|
|
1226
|
+
sessionId: message.sessionId,
|
|
1227
|
+
text: message.text,
|
|
1228
|
+
/* v8 ignore next 1 -- schemastery resolves an absent optional `sender` to an empty object, never undefined. */
|
|
1229
|
+
...message.sender === void 0 ? {} : { sender: message.sender },
|
|
1230
|
+
...message.delivery === void 0 ? {} : { delivery: message.delivery },
|
|
1231
|
+
...message.resume === void 0 ? {} : { resume: message.resume }
|
|
1232
|
+
});
|
|
1180
1233
|
this.sendFrame(socket, { type: "msg-result", reqId, result });
|
|
1181
1234
|
}
|
|
1182
1235
|
/** Dispatch one inbound `query` frame (ping/list/event) and answer on the socket. */
|
|
1183
|
-
|
|
1236
|
+
handleQueryFrame(socket, reqId, query) {
|
|
1184
1237
|
let result;
|
|
1185
1238
|
if (query.kind === "ping") {
|
|
1186
1239
|
result = { pong: true, instance: this.instanceId };
|
|
@@ -1253,7 +1306,9 @@ var InterconnectService = class extends Service {
|
|
|
1253
1306
|
if (resolved === void 0 || resolved === null) return { reason: "session-not-live" };
|
|
1254
1307
|
return { agent: resolved };
|
|
1255
1308
|
} catch (error) {
|
|
1256
|
-
this.ctx.logger.info(
|
|
1309
|
+
this.ctx.logger.info(
|
|
1310
|
+
`interconnect: resume refused for ${payload.sessionId}: ${error instanceof Error ? error.message : String(error)}`
|
|
1311
|
+
);
|
|
1257
1312
|
return { reason: "resume-failed" };
|
|
1258
1313
|
}
|
|
1259
1314
|
}
|
|
@@ -1262,9 +1317,11 @@ var InterconnectService = class extends Service {
|
|
|
1262
1317
|
* when the sender asked and this receiver allows it.
|
|
1263
1318
|
*/
|
|
1264
1319
|
async deliver(payload) {
|
|
1320
|
+
if (this.disposed) return { delivered: false, instance: this.instanceId, reason: "unreachable" };
|
|
1265
1321
|
let agent = this.ctx.agents.get(payload.sessionId);
|
|
1266
1322
|
if (agent === void 0) {
|
|
1267
1323
|
const woken = await this.wake(payload);
|
|
1324
|
+
if (this.disposed) return { delivered: false, instance: this.instanceId, reason: "unreachable" };
|
|
1268
1325
|
if ("reason" in woken) {
|
|
1269
1326
|
return { delivered: false, instance: this.instanceId, reason: woken.reason };
|
|
1270
1327
|
}
|
|
@@ -1275,11 +1332,11 @@ var InterconnectService = class extends Service {
|
|
|
1275
1332
|
}
|
|
1276
1333
|
const sender = (() => {
|
|
1277
1334
|
const candidate = payload.sender;
|
|
1278
|
-
|
|
1335
|
+
if (typeof candidate !== "object" || candidate === null) return void 0;
|
|
1336
|
+
const record = candidate;
|
|
1337
|
+
if (typeof record.instanceId !== "string" || typeof record.sessionId !== "string") return void 0;
|
|
1338
|
+
return { instanceId: record.instanceId, sessionId: record.sessionId };
|
|
1279
1339
|
})();
|
|
1280
|
-
if (sender !== void 0) {
|
|
1281
|
-
this.senders.set(payload.sessionId, sender);
|
|
1282
|
-
}
|
|
1283
1340
|
const message = createUserMessage({
|
|
1284
1341
|
source: {
|
|
1285
1342
|
kind: "plugin",
|
|
@@ -1302,27 +1359,21 @@ var InterconnectService = class extends Service {
|
|
|
1302
1359
|
case "followup":
|
|
1303
1360
|
agent.followup(message);
|
|
1304
1361
|
break;
|
|
1362
|
+
/* v8 ignore next -- closed-union exhaustiveness guard */
|
|
1363
|
+
default:
|
|
1364
|
+
assertNever(mode);
|
|
1365
|
+
}
|
|
1366
|
+
if (sender !== void 0) {
|
|
1367
|
+
this.senders.set(payload.sessionId, sender);
|
|
1368
|
+
} else {
|
|
1369
|
+
this.senders.delete(payload.sessionId);
|
|
1305
1370
|
}
|
|
1306
1371
|
return { delivered: true, instance: this.instanceId, delivery: mode };
|
|
1307
1372
|
}
|
|
1308
|
-
/**
|
|
1309
|
-
* Inbound `reply` dispatch: forward a request to {link reply}, resolving the
|
|
1310
|
-
* outbound sender for the named local session. Kept separate from {link
|
|
1311
|
-
* deliver} so the two wire endpoints stay distinct in the request pipeline.
|
|
1312
|
-
*/
|
|
1313
|
-
async replyForSession(payload) {
|
|
1314
|
-
return this.reply({
|
|
1315
|
-
sessionId: payload.sessionId,
|
|
1316
|
-
text: payload.text,
|
|
1317
|
-
...payload.delivery === void 0 ? {} : { delivery: payload.delivery },
|
|
1318
|
-
...payload.resume === void 0 ? {} : { resume: payload.resume }
|
|
1319
|
-
});
|
|
1320
|
-
}
|
|
1321
1373
|
async resolveToken() {
|
|
1322
1374
|
const credential = await this.ctx.credentials.resolve(credentialRef(INTERCONNECT_TOKEN_REF));
|
|
1323
1375
|
return credential === void 0 || credential.value.length === 0 ? void 0 : credential.value;
|
|
1324
1376
|
}
|
|
1325
|
-
/** Resolve the shared token for an outbound dial (same source as inbound). */
|
|
1326
1377
|
resolveTokenForDial() {
|
|
1327
1378
|
return this.resolveToken();
|
|
1328
1379
|
}
|
|
@@ -1353,7 +1404,10 @@ var InterconnectService = class extends Service {
|
|
|
1353
1404
|
/**
|
|
1354
1405
|
* Install frame + liveness handling on one socket and add it to the live
|
|
1355
1406
|
* pool. Used by both the server half (accepted upgrade) and the client half
|
|
1356
|
-
* (outbound dial), so a single socket carries events both directions.
|
|
1407
|
+
* (outbound dial), so a single socket carries events both directions. Not a
|
|
1408
|
+
* model-facing entry point; the outbound dial reaches it through a private
|
|
1409
|
+
* closure.
|
|
1410
|
+
* @param websocket - the accepted or dialed socket to install handlers on.
|
|
1357
1411
|
*/
|
|
1358
1412
|
attachSocket(websocket) {
|
|
1359
1413
|
this.sockets.add(websocket);
|
|
@@ -1377,30 +1431,42 @@ var InterconnectService = class extends Service {
|
|
|
1377
1431
|
/** Parse and route one inbound link frame, attributing events to the socket's announced peer. */
|
|
1378
1432
|
handleFrame(socket, data) {
|
|
1379
1433
|
if (Array.isArray(data)) return;
|
|
1434
|
+
if (data.byteLength > MAX_LINK_FRAME_BYTES) {
|
|
1435
|
+
this.ctx.logger.warn("interconnect: dropping oversized link frame");
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1380
1438
|
const text = Buffer.isBuffer(data) ? data.toString("utf8") : Buffer.from(data).toString("utf8");
|
|
1381
|
-
let
|
|
1439
|
+
let parsed;
|
|
1382
1440
|
try {
|
|
1383
|
-
|
|
1441
|
+
parsed = Schema.resolve(JSON.parse(text), linkFrameSchema, {})[0];
|
|
1384
1442
|
} catch {
|
|
1385
1443
|
this.ctx.logger.warn("interconnect: dropping malformed link frame");
|
|
1386
1444
|
return;
|
|
1387
1445
|
}
|
|
1446
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
1447
|
+
this.ctx.logger.warn("interconnect: dropping malformed link frame");
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
const frame = parsed;
|
|
1388
1451
|
if (frame.type === "hello") {
|
|
1389
1452
|
this.peerOf.set(socket, frame.sender);
|
|
1390
1453
|
return;
|
|
1391
1454
|
}
|
|
1392
1455
|
if (frame.type === "event") {
|
|
1393
1456
|
const sender = this.peerOf.get(socket) ?? "unknown-peer";
|
|
1457
|
+
const kind = frame.notification.kind;
|
|
1394
1458
|
try {
|
|
1395
1459
|
this.receiveEvent({ sender, notification: frame.notification });
|
|
1396
1460
|
} catch (error) {
|
|
1397
|
-
this.ctx.logger.warn(
|
|
1461
|
+
this.ctx.logger.warn(
|
|
1462
|
+
`interconnect: listener for a ${kind} event from ${sender} threw: ` + (error instanceof Error ? error.message : String(error))
|
|
1463
|
+
);
|
|
1398
1464
|
}
|
|
1399
1465
|
return;
|
|
1400
1466
|
}
|
|
1401
1467
|
if (frame.type === "msg-result") {
|
|
1402
1468
|
const pending = this.pendingMessages.get(frame.reqId);
|
|
1403
|
-
if (pending !== void 0) {
|
|
1469
|
+
if (pending !== void 0 && pending.kind === "msg") {
|
|
1404
1470
|
;
|
|
1405
1471
|
pending.resolve?.(frame.result);
|
|
1406
1472
|
}
|
|
@@ -1408,7 +1474,7 @@ var InterconnectService = class extends Service {
|
|
|
1408
1474
|
}
|
|
1409
1475
|
if (frame.type === "query-result") {
|
|
1410
1476
|
const pending = this.pendingMessages.get(frame.reqId);
|
|
1411
|
-
if (pending !== void 0) {
|
|
1477
|
+
if (pending !== void 0 && pending.kind === "query" && (pending.accept === void 0 || pending.accept(frame.result))) {
|
|
1412
1478
|
;
|
|
1413
1479
|
pending.resolve?.(frame.result);
|
|
1414
1480
|
}
|
|
@@ -1427,8 +1493,8 @@ var InterconnectService = class extends Service {
|
|
|
1427
1493
|
});
|
|
1428
1494
|
return;
|
|
1429
1495
|
}
|
|
1430
|
-
void Promise.resolve().then(
|
|
1431
|
-
|
|
1496
|
+
void Promise.resolve().then(() => {
|
|
1497
|
+
this.handleQueryFrame(socket, frame.reqId, frame.query);
|
|
1432
1498
|
}).catch((error) => {
|
|
1433
1499
|
this.ctx.logger.warn(`interconnect: query ${frame.reqId} handler threw: ${error instanceof Error ? error.message : String(error)}`);
|
|
1434
1500
|
});
|
|
@@ -1441,15 +1507,20 @@ var InterconnectService = class extends Service {
|
|
|
1441
1507
|
}
|
|
1442
1508
|
};
|
|
1443
1509
|
var LinkState = class {
|
|
1444
|
-
constructor(
|
|
1445
|
-
this.
|
|
1510
|
+
constructor(attachSocket, logger, resolveToken, instanceId, origin) {
|
|
1511
|
+
this.attachSocket = attachSocket;
|
|
1512
|
+
this.logger = logger;
|
|
1513
|
+
this.resolveToken = resolveToken;
|
|
1446
1514
|
this.instanceId = instanceId;
|
|
1447
1515
|
this.peer = origin;
|
|
1448
1516
|
}
|
|
1449
1517
|
socket;
|
|
1518
|
+
dialEpoch = 0;
|
|
1450
1519
|
closed = false;
|
|
1451
1520
|
retry = 0;
|
|
1452
1521
|
reconnectTimer;
|
|
1522
|
+
/** Dial-registered handlers, removed by reference on close/reroute so the pool-cleanup handlers attachSocket added stay alive. */
|
|
1523
|
+
dialListeners;
|
|
1453
1524
|
// `peer` is mutable so `reroute` can point the link at a new origin.
|
|
1454
1525
|
peer;
|
|
1455
1526
|
instanceId;
|
|
@@ -1457,9 +1528,16 @@ var LinkState = class {
|
|
|
1457
1528
|
reroute(origin) {
|
|
1458
1529
|
if (origin === this.peer) return;
|
|
1459
1530
|
this.peer = origin;
|
|
1531
|
+
this.dialEpoch += 1;
|
|
1532
|
+
if (this.reconnectTimer !== void 0) {
|
|
1533
|
+
clearTimeout(this.reconnectTimer);
|
|
1534
|
+
this.reconnectTimer = void 0;
|
|
1535
|
+
}
|
|
1460
1536
|
const socket = this.socket;
|
|
1461
1537
|
if (socket !== void 0) {
|
|
1462
|
-
|
|
1538
|
+
this.removeDialListeners(socket);
|
|
1539
|
+
socket.on("error", () => {
|
|
1540
|
+
});
|
|
1463
1541
|
socket.terminate();
|
|
1464
1542
|
this.socket = void 0;
|
|
1465
1543
|
}
|
|
@@ -1468,29 +1546,54 @@ var LinkState = class {
|
|
|
1468
1546
|
/** Open the socket; reconnect is scheduled by the close handler. */
|
|
1469
1547
|
dial() {
|
|
1470
1548
|
if (this.closed) return;
|
|
1471
|
-
const
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1549
|
+
const epoch = this.dialEpoch;
|
|
1550
|
+
let url;
|
|
1551
|
+
try {
|
|
1552
|
+
url = linkUrl(this.peer);
|
|
1553
|
+
} catch {
|
|
1554
|
+
this.logger.warn(`interconnect: invalid peer origin ${this.peer}; link to ${this.instanceId} stays down`);
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
void this.resolveToken().then((token) => {
|
|
1558
|
+
if (token === void 0) {
|
|
1559
|
+
this.logger.warn(`interconnect: no shared token configured; peer ${this.instanceId} link stays down until one is set`);
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
if (this.closed || epoch !== this.dialEpoch) return;
|
|
1475
1563
|
const socket = new WebSocket(url, {
|
|
1476
|
-
headers: { authorization: `Bearer ${token}` }
|
|
1564
|
+
headers: { authorization: `Bearer ${token}` },
|
|
1565
|
+
maxPayload: MAX_LINK_FRAME_BYTES,
|
|
1566
|
+
// A peer that accepts TCP but never completes the upgrade would leave
|
|
1567
|
+
// the socket in CONNECTING forever: no open (so no heartbeat) and no
|
|
1568
|
+
// close (so no reconnect). ws aborts the connection past the handshake
|
|
1569
|
+
// window, so its own error+close fire and the close handler schedules
|
|
1570
|
+
// a reconnect — and ws owns the timer, so nothing leaks on teardown.
|
|
1571
|
+
handshakeTimeout: LINK_HANDSHAKE_TIMEOUT_MS
|
|
1477
1572
|
});
|
|
1478
1573
|
this.socket = socket;
|
|
1479
|
-
|
|
1574
|
+
const onOpen = () => {
|
|
1480
1575
|
this.retry = 0;
|
|
1481
|
-
this.
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1576
|
+
this.attachSocket(socket);
|
|
1577
|
+
};
|
|
1578
|
+
const onClose = () => {
|
|
1484
1579
|
if (this.closed) return;
|
|
1485
1580
|
this.scheduleReconnect();
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
}
|
|
1581
|
+
};
|
|
1582
|
+
const onError = () => {
|
|
1583
|
+
};
|
|
1584
|
+
this.dialListeners = { open: onOpen, close: onClose, error: onError };
|
|
1585
|
+
socket.once("open", onOpen);
|
|
1586
|
+
socket.once("close", onClose);
|
|
1587
|
+
socket.on("error", onError);
|
|
1489
1588
|
}).catch(() => {
|
|
1490
|
-
if (this.closed) return;
|
|
1589
|
+
if (this.closed || epoch !== this.dialEpoch) return;
|
|
1491
1590
|
this.scheduleReconnect();
|
|
1492
1591
|
});
|
|
1493
1592
|
}
|
|
1593
|
+
/** Whether this link has been closed; a closed state is never reused by `link()`. */
|
|
1594
|
+
isClosed() {
|
|
1595
|
+
return this.closed;
|
|
1596
|
+
}
|
|
1494
1597
|
/** Whether this link currently holds an open socket that can carry frames. */
|
|
1495
1598
|
writable() {
|
|
1496
1599
|
return this.socket !== void 0 && this.socket.readyState === WebSocket.OPEN;
|
|
@@ -1504,14 +1607,24 @@ var LinkState = class {
|
|
|
1504
1607
|
}
|
|
1505
1608
|
close() {
|
|
1506
1609
|
this.closed = true;
|
|
1507
|
-
this.owner.forgetLink(this.peer);
|
|
1508
1610
|
if (this.reconnectTimer !== void 0) clearTimeout(this.reconnectTimer);
|
|
1509
1611
|
const socket = this.socket;
|
|
1510
1612
|
if (socket !== void 0) {
|
|
1511
|
-
|
|
1613
|
+
this.removeDialListeners(socket);
|
|
1614
|
+
socket.on("error", () => {
|
|
1615
|
+
});
|
|
1512
1616
|
socket.terminate();
|
|
1513
1617
|
}
|
|
1514
1618
|
}
|
|
1619
|
+
/** Detach the dial-registered handlers from a socket before terminating it. */
|
|
1620
|
+
removeDialListeners(socket) {
|
|
1621
|
+
const listeners = this.dialListeners;
|
|
1622
|
+
if (listeners === void 0) return;
|
|
1623
|
+
socket.removeListener("open", listeners.open);
|
|
1624
|
+
socket.removeListener("close", listeners.close);
|
|
1625
|
+
socket.removeListener("error", listeners.error);
|
|
1626
|
+
this.dialListeners = void 0;
|
|
1627
|
+
}
|
|
1515
1628
|
scheduleReconnect() {
|
|
1516
1629
|
const delay = Math.min(3e4, 1e3 * 2 ** this.retry);
|
|
1517
1630
|
this.retry += 1;
|
|
@@ -1522,18 +1635,23 @@ var LinkState = class {
|
|
|
1522
1635
|
}
|
|
1523
1636
|
};
|
|
1524
1637
|
function timingSafeEqual(a, b) {
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1638
|
+
return constantTimeEqual(createHash("sha256").update(a, "utf8").digest(), createHash("sha256").update(b, "utf8").digest());
|
|
1639
|
+
}
|
|
1640
|
+
function linkUrl(origin) {
|
|
1641
|
+
const url = new URL("/interconnect/link", origin);
|
|
1642
|
+
if (url.protocol === "http:") url.protocol = "ws:";
|
|
1643
|
+
else if (url.protocol === "https:") url.protocol = "wss:";
|
|
1644
|
+
if (url.protocol !== "ws:" && url.protocol !== "wss:") {
|
|
1645
|
+
throw new TypeError(`interconnect: unsupported peer origin protocol ${url.protocol}`);
|
|
1646
|
+
}
|
|
1647
|
+
return url;
|
|
1531
1648
|
}
|
|
1532
1649
|
function trimBase(baseUrl) {
|
|
1533
1650
|
return baseUrl.replace(/\/+$/, "");
|
|
1534
1651
|
}
|
|
1535
1652
|
var interconnect_default = InterconnectService;
|
|
1536
1653
|
export {
|
|
1654
|
+
INTERCONNECT_CHANNEL,
|
|
1537
1655
|
INTERCONNECT_TOKEN_REF,
|
|
1538
1656
|
InterconnectService,
|
|
1539
1657
|
interconnect_default as default
|