dsh-interconnect 0.10.1 → 0.11.1
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 +19 -13
- package/assets/dsh-interconnect.md +5 -5
- package/dsh.plugin.json +2 -2
- package/lib/index.js +304 -161
- package/lib/interconnect/index.js +306 -162
- 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 +71 -59
- package/lib/tool-interconnect/index.js.map +2 -2
- package/lib/types/index.d.ts +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/interconnect/index.d.ts +64 -29
- package/lib/types/interconnect/index.d.ts.map +1 -1
- package/lib/types/interconnect/types.d.ts +9 -49
- package/lib/types/interconnect/types.d.ts.map +1 -1
- package/lib/types/skill-interconnect/index.d.ts +4 -4
- package/lib/types/skill-interconnect/index.d.ts.map +1 -1
- package/lib/types/tool-interconnect/index.d.ts +10 -7
- package/lib/types/tool-interconnect/index.d.ts.map +1 -1
- package/package.json +2 -1
|
@@ -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,84 @@ 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_TOKEN_REF = "DSH_INTERCONNECT_TOKEN";
|
|
833
833
|
var PLUGIN_SOURCE = "dsh-interconnect";
|
|
834
|
+
var MAX_LINK_FRAME_BYTES = 1024 * 1024;
|
|
835
|
+
var LINK_HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
834
836
|
var LINK_CHANNEL = "/interconnect/link";
|
|
837
|
+
var MAX_LISTED_SESSIONS = 100;
|
|
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
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
847
|
+
var pingResultSchema = Schema.object({ pong: Schema.const(true).required(), instance: Schema.string().required() });
|
|
848
|
+
var listResultSchema = Schema.object({
|
|
849
|
+
instance: Schema.string().required(),
|
|
850
|
+
sessions: Schema.array(Schema.object({
|
|
851
|
+
sessionId: Schema.string().required(),
|
|
852
|
+
title: Schema.string().required(false),
|
|
853
|
+
status: Schema.string().required(false)
|
|
854
|
+
})).required()
|
|
855
|
+
});
|
|
856
|
+
var acceptsQueryResult = (schema, result) => {
|
|
857
|
+
try {
|
|
858
|
+
const [parsed] = Schema.resolve(result, schema, {});
|
|
859
|
+
return parsed !== void 0 && parsed !== null;
|
|
860
|
+
} catch {
|
|
861
|
+
return false;
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
var queryResultSchema = Schema.union([
|
|
865
|
+
pingResultSchema,
|
|
866
|
+
listResultSchema,
|
|
867
|
+
Schema.object({ accepted: Schema.const(true).required() })
|
|
861
868
|
]);
|
|
869
|
+
var messageSchema = Schema.object({
|
|
870
|
+
kind: Schema.const("send").required(),
|
|
871
|
+
sessionId: Schema.string().required(),
|
|
872
|
+
text: Schema.string().required(),
|
|
873
|
+
sender: senderSchema,
|
|
874
|
+
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]),
|
|
875
|
+
resume: Schema.boolean()
|
|
876
|
+
});
|
|
862
877
|
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 })
|
|
878
|
+
Schema.object({ kind: Schema.const("ping").required() }),
|
|
879
|
+
Schema.object({ kind: Schema.const("list").required() }),
|
|
880
|
+
Schema.object({ kind: Schema.const("event").required(), notification: notificationSchema.required() })
|
|
881
|
+
]);
|
|
882
|
+
var msgResultSchema = Schema.union([
|
|
883
|
+
Schema.object({ delivered: Schema.const(true).required(), instance: Schema.string().required(), delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]) }),
|
|
884
|
+
Schema.object({ delivered: Schema.const(false).required(), instance: Schema.string().required(), reason: Schema.union([
|
|
885
|
+
Schema.const("session-not-live"),
|
|
886
|
+
Schema.const("unreachable"),
|
|
887
|
+
Schema.const("resume-refused"),
|
|
888
|
+
Schema.const("resume-failed"),
|
|
889
|
+
Schema.const("session-owned-by-subagent"),
|
|
890
|
+
Schema.const("no-sender-known")
|
|
891
|
+
]).required() })
|
|
866
892
|
]);
|
|
867
893
|
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() })
|
|
894
|
+
Schema.object({ type: Schema.const("hello").required(), sender: Schema.string().required() }),
|
|
895
|
+
Schema.object({ type: Schema.const("event").required(), notification: notificationSchema.required() }),
|
|
896
|
+
Schema.object({ type: Schema.const("msg").required(), reqId: Schema.string().required(), message: messageSchema.required() }),
|
|
897
|
+
Schema.object({ type: Schema.const("msg-result").required(), reqId: Schema.string().required(), result: msgResultSchema.required() }),
|
|
898
|
+
Schema.object({ type: Schema.const("query").required(), reqId: Schema.string().required(), query: querySchema.required() }),
|
|
899
|
+
Schema.object({ type: Schema.const("query-result").required(), reqId: Schema.string().required(), result: queryResultSchema.required() })
|
|
879
900
|
]);
|
|
880
901
|
var InterconnectService = class extends Service {
|
|
881
|
-
static inject = ["
|
|
902
|
+
static inject = ["agents", "credentials"];
|
|
882
903
|
static Config = Schema.object({
|
|
883
904
|
instanceId: Schema.string().default("dsh"),
|
|
884
905
|
requestTimeoutMs: Schema.natural().max(6e4).default(1e4),
|
|
885
|
-
peers: Schema.
|
|
906
|
+
peers: Schema.dict(Schema.string()).default({}),
|
|
886
907
|
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]).default("followup"),
|
|
887
908
|
allowResume: Schema.boolean().default(true)
|
|
888
909
|
});
|
|
@@ -891,7 +912,7 @@ var InterconnectService = class extends Service {
|
|
|
891
912
|
delivery;
|
|
892
913
|
allowResume;
|
|
893
914
|
subscriptions = [];
|
|
894
|
-
server = new WebSocketServer({ noServer: true });
|
|
915
|
+
server = new WebSocketServer({ noServer: true, maxPayload: MAX_LINK_FRAME_BYTES });
|
|
895
916
|
sockets = /* @__PURE__ */ new Set();
|
|
896
917
|
/** Outbound peer links keyed by the peer's `instanceId`. */
|
|
897
918
|
linkStates = /* @__PURE__ */ new Map();
|
|
@@ -903,24 +924,38 @@ var InterconnectService = class extends Service {
|
|
|
903
924
|
/** In-flight frames sent over a peer link, keyed by `reqId`, awaiting a correlated result. */
|
|
904
925
|
pendingMessages = /* @__PURE__ */ new Map();
|
|
905
926
|
reqIdCounter = 0;
|
|
927
|
+
/** Set when the service fiber unwinds; guards late inbound deliveries against driving disposed agents. */
|
|
928
|
+
disposed = false;
|
|
906
929
|
constructor(ctx, config) {
|
|
907
930
|
super(ctx, "interconnect");
|
|
908
931
|
this.instanceId = config.instanceId;
|
|
909
932
|
this.requestTimeoutMs = config.requestTimeoutMs;
|
|
910
933
|
this.delivery = config.delivery ?? "followup";
|
|
911
934
|
this.allowResume = config.allowResume ?? true;
|
|
935
|
+
for (const origin of Object.values(config.peers ?? {})) {
|
|
936
|
+
linkUrl(trimBase(origin));
|
|
937
|
+
}
|
|
912
938
|
for (const [peerInstanceId, origin] of Object.entries(config.peers ?? {})) {
|
|
913
939
|
this.link(peerInstanceId, origin);
|
|
914
940
|
}
|
|
915
941
|
const upgrade = {
|
|
916
942
|
path: LINK_CHANNEL,
|
|
917
943
|
handler: (req, socket, head) => {
|
|
918
|
-
void this.handleUpgrade(req, socket, head)
|
|
944
|
+
void this.handleUpgrade(req, socket, head).catch((error) => {
|
|
945
|
+
this.ctx.logger.warn(`interconnect: upgrade failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
946
|
+
socket.destroy();
|
|
947
|
+
});
|
|
919
948
|
}
|
|
920
949
|
};
|
|
921
|
-
ctx.
|
|
950
|
+
ctx.inject(["webServer"], (webCtx) => {
|
|
951
|
+
webCtx.effect(() => webCtx.webServer.registerUpgrade(upgrade), "interconnect: /interconnect/link websocket");
|
|
952
|
+
});
|
|
922
953
|
this.heartbeatTimer = setInterval(() => {
|
|
923
954
|
for (const socket of this.sockets) {
|
|
955
|
+
if (socket.readyState !== WebSocket.OPEN) {
|
|
956
|
+
this.sockets.delete(socket);
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
924
959
|
if (socket.isAlive === false) {
|
|
925
960
|
socket.terminate();
|
|
926
961
|
this.sockets.delete(socket);
|
|
@@ -949,6 +984,7 @@ var InterconnectService = class extends Service {
|
|
|
949
984
|
});
|
|
950
985
|
}));
|
|
951
986
|
this.subscriptions.push(ctx.on("session/disposed", (session) => {
|
|
987
|
+
this.senders.delete(String(session.id));
|
|
952
988
|
this.fanout({ kind: "session/disposed", sessionId: String(session.id) });
|
|
953
989
|
}));
|
|
954
990
|
this.subscriptions.push(ctx.on("subagent/end", (info) => {
|
|
@@ -970,16 +1006,28 @@ var InterconnectService = class extends Service {
|
|
|
970
1006
|
for (const socket of this.sockets) socket.terminate();
|
|
971
1007
|
this.sockets.clear();
|
|
972
1008
|
this.server.close();
|
|
1009
|
+
for (const pending of this.pendingMessages.values()) {
|
|
1010
|
+
clearTimeout(pending.timer);
|
|
1011
|
+
pending.resolve?.(void 0);
|
|
1012
|
+
}
|
|
1013
|
+
this.pendingMessages.clear();
|
|
1014
|
+
this.disposed = true;
|
|
973
1015
|
}, "interconnect: websocket teardown");
|
|
974
1016
|
}
|
|
975
1017
|
/**
|
|
976
1018
|
* This instance's identity to attach to outbound messages. Always present:
|
|
977
1019
|
* it needs no address, only this instance's id and the calling session.
|
|
978
1020
|
* @param sessionId - the local session that is sending, used as the reply target.
|
|
1021
|
+
* @returns this instance's address-free identity for the calling session.
|
|
979
1022
|
*/
|
|
980
1023
|
selfSender(sessionId) {
|
|
981
1024
|
return { instanceId: this.instanceId, sessionId };
|
|
982
1025
|
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Deliver one text message to a live session on a peer instance.
|
|
1028
|
+
* @param request - the peer instance id, target session, text, and optional delivery/resume overrides.
|
|
1029
|
+
* @returns the peer's answer, or an unreachable result when no link answers.
|
|
1030
|
+
*/
|
|
983
1031
|
async send(request) {
|
|
984
1032
|
const payload = {
|
|
985
1033
|
sessionId: request.sessionId,
|
|
@@ -988,14 +1036,20 @@ var InterconnectService = class extends Service {
|
|
|
988
1036
|
...request.delivery === void 0 ? {} : { delivery: request.delivery },
|
|
989
1037
|
...request.resume === void 0 ? {} : { resume: request.resume }
|
|
990
1038
|
};
|
|
991
|
-
|
|
992
|
-
|
|
1039
|
+
try {
|
|
1040
|
+
const result = await this.msgRequest(request.instanceId, "send", payload);
|
|
1041
|
+
return result ?? { delivered: false, instance: request.instanceId, reason: "unreachable" };
|
|
1042
|
+
} catch {
|
|
1043
|
+
return { delivered: false, instance: request.instanceId, reason: "unreachable" };
|
|
1044
|
+
}
|
|
993
1045
|
}
|
|
994
1046
|
/**
|
|
995
1047
|
* Deliver one text message back to the peer that a local session last
|
|
996
1048
|
* received a send from. `sessionId` names the LOCAL replying session; the
|
|
997
1049
|
* outbound target is the `sender` that session recorded, addressed through
|
|
998
1050
|
* this instance's own link to the sender's instance.
|
|
1051
|
+
* @param request - the LOCAL replying session id, the reply text, and optional delivery/resume overrides.
|
|
1052
|
+
* @returns the recalled sender's answer, or an unreachable result when no link answers.
|
|
999
1053
|
*/
|
|
1000
1054
|
async reply(request) {
|
|
1001
1055
|
const sender = this.senders.get(request.sessionId);
|
|
@@ -1011,13 +1065,19 @@ var InterconnectService = class extends Service {
|
|
|
1011
1065
|
// back — chaining the conversation.
|
|
1012
1066
|
sender: this.selfSender(request.sessionId)
|
|
1013
1067
|
};
|
|
1014
|
-
|
|
1015
|
-
|
|
1068
|
+
try {
|
|
1069
|
+
const result = await this.msgRequest(sender.instanceId, "send", payload);
|
|
1070
|
+
return result ?? { delivered: false, instance: sender.instanceId, reason: "unreachable" };
|
|
1071
|
+
} catch {
|
|
1072
|
+
return { delivered: false, instance: sender.instanceId, reason: "unreachable" };
|
|
1073
|
+
}
|
|
1016
1074
|
}
|
|
1017
1075
|
/**
|
|
1018
1076
|
* Probe a peer instance for liveness and identity over its persistent link.
|
|
1019
1077
|
* Returns the peer identity when reachable, or undefined when the link is
|
|
1020
1078
|
* not up or no answer arrives.
|
|
1079
|
+
* @param instanceId - the peer's `instanceId` as configured under {@link Config.peers}.
|
|
1080
|
+
* @returns the peer's identity, or undefined when the link is not up or no answer arrives.
|
|
1021
1081
|
*/
|
|
1022
1082
|
async ping(instanceId) {
|
|
1023
1083
|
return this.queryRequest(instanceId, { kind: "ping" });
|
|
@@ -1025,15 +1085,12 @@ var InterconnectService = class extends Service {
|
|
|
1025
1085
|
/**
|
|
1026
1086
|
* List a peer instance's live sessions over its persistent link. Undefined on
|
|
1027
1087
|
* transport failure, matching `ping`.
|
|
1088
|
+
* @param instanceId - the peer's `instanceId` as configured under {@link Config.peers}.
|
|
1089
|
+
* @returns the peer's live session rows, or undefined on transport failure.
|
|
1028
1090
|
*/
|
|
1029
1091
|
async list(instanceId) {
|
|
1030
1092
|
return this.queryRequest(instanceId, { kind: "list" });
|
|
1031
1093
|
}
|
|
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
1094
|
/**
|
|
1038
1095
|
* Send a `msg` frame over the live link to a peer and resolve with its
|
|
1039
1096
|
* correlated `msg-result`. Undefined when the peer has no live link here
|
|
@@ -1043,7 +1100,7 @@ var InterconnectService = class extends Service {
|
|
|
1043
1100
|
async msgRequest(instanceId, kind, payload) {
|
|
1044
1101
|
const state = this.linkStates.get(instanceId);
|
|
1045
1102
|
if (state === void 0 || !state.writable()) return void 0;
|
|
1046
|
-
const reqId = `m${++this.reqIdCounter}-${
|
|
1103
|
+
const reqId = `m${++this.reqIdCounter}-${randomUUID()}`;
|
|
1047
1104
|
const message = {
|
|
1048
1105
|
kind,
|
|
1049
1106
|
sessionId: payload.sessionId,
|
|
@@ -1052,7 +1109,7 @@ var InterconnectService = class extends Service {
|
|
|
1052
1109
|
...payload.delivery === void 0 ? {} : { delivery: payload.delivery },
|
|
1053
1110
|
...payload.resume === void 0 ? {} : { resume: payload.resume }
|
|
1054
1111
|
};
|
|
1055
|
-
return this.waitForResult(reqId, (failure) => {
|
|
1112
|
+
return this.waitForResult(reqId, "msg", (failure) => {
|
|
1056
1113
|
const wrote = state.sendFrame({ type: "msg", reqId, message });
|
|
1057
1114
|
if (!wrote) failure(new Error(`interconnect: peer link to ${instanceId} closed while sending`));
|
|
1058
1115
|
});
|
|
@@ -1061,32 +1118,34 @@ var InterconnectService = class extends Service {
|
|
|
1061
1118
|
async queryRequest(instanceId, query) {
|
|
1062
1119
|
const state = this.linkStates.get(instanceId);
|
|
1063
1120
|
if (state === void 0 || !state.writable()) return void 0;
|
|
1064
|
-
const reqId = `q${++this.reqIdCounter}-${
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1121
|
+
const reqId = `q${++this.reqIdCounter}-${randomUUID()}`;
|
|
1122
|
+
const accept = query.kind === "ping" ? (result) => acceptsQueryResult(pingResultSchema, result) : (result) => acceptsQueryResult(listResultSchema, result);
|
|
1123
|
+
try {
|
|
1124
|
+
return await this.waitForResult(reqId, "query", (failure) => {
|
|
1125
|
+
const wrote = state.sendFrame({ type: "query", reqId, query });
|
|
1126
|
+
if (!wrote) failure(new Error(`interconnect: peer link to ${instanceId} closed while querying`));
|
|
1127
|
+
}, accept);
|
|
1128
|
+
} catch {
|
|
1129
|
+
return void 0;
|
|
1130
|
+
}
|
|
1069
1131
|
}
|
|
1070
1132
|
/** Settle a `reqId` frame against its `*-result`, timing out at `requestTimeoutMs`. */
|
|
1071
|
-
waitForResult(reqId, send) {
|
|
1133
|
+
waitForResult(reqId, kind, send, accept) {
|
|
1072
1134
|
return new Promise((resolve2, reject) => {
|
|
1073
1135
|
const timer = setTimeout(() => {
|
|
1074
1136
|
this.pendingMessages.delete(reqId);
|
|
1075
1137
|
reject(new Error(`interconnect: no result for ${reqId} within ${this.requestTimeoutMs}ms`));
|
|
1076
1138
|
}, 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
|
-
};
|
|
1139
|
+
this.pendingMessages.set(reqId, {
|
|
1140
|
+
timer,
|
|
1141
|
+
kind,
|
|
1142
|
+
...accept === void 0 ? {} : { accept },
|
|
1143
|
+
resolve: (result) => {
|
|
1144
|
+
clearTimeout(timer);
|
|
1145
|
+
this.pendingMessages.delete(reqId);
|
|
1146
|
+
resolve2(result);
|
|
1147
|
+
}
|
|
1148
|
+
});
|
|
1090
1149
|
send((error) => {
|
|
1091
1150
|
clearTimeout(timer);
|
|
1092
1151
|
this.pendingMessages.delete(reqId);
|
|
@@ -1102,12 +1161,20 @@ var InterconnectService = class extends Service {
|
|
|
1102
1161
|
* @returns disposer removing the peer route.
|
|
1103
1162
|
*/
|
|
1104
1163
|
subscribe(instanceId, origin) {
|
|
1105
|
-
this.link(instanceId, origin);
|
|
1164
|
+
const linked = this.link(instanceId, origin);
|
|
1165
|
+
const dialedOrigin = trimBase(origin);
|
|
1106
1166
|
return () => {
|
|
1107
|
-
this.
|
|
1167
|
+
const state = this.linkStates.get(instanceId);
|
|
1168
|
+
if (state === linked && state.peer === dialedOrigin) {
|
|
1169
|
+
state.close();
|
|
1170
|
+
this.linkStates.delete(instanceId);
|
|
1171
|
+
}
|
|
1108
1172
|
};
|
|
1109
1173
|
}
|
|
1110
|
-
/**
|
|
1174
|
+
/**
|
|
1175
|
+
* Remove a peer route, closing its outbound link.
|
|
1176
|
+
* @param instanceId - the peer route to remove.
|
|
1177
|
+
*/
|
|
1111
1178
|
unsubscribe(instanceId) {
|
|
1112
1179
|
const state = this.linkStates.get(instanceId);
|
|
1113
1180
|
if (state !== void 0) state.close();
|
|
@@ -1124,19 +1191,23 @@ var InterconnectService = class extends Service {
|
|
|
1124
1191
|
*/
|
|
1125
1192
|
link(instanceId, origin) {
|
|
1126
1193
|
const existing = this.linkStates.get(instanceId);
|
|
1127
|
-
if (existing !== void 0) {
|
|
1194
|
+
if (existing !== void 0 && !existing.isClosed()) {
|
|
1128
1195
|
existing.reroute(trimBase(origin));
|
|
1129
1196
|
return existing;
|
|
1130
1197
|
}
|
|
1131
|
-
const state = new LinkState(
|
|
1198
|
+
const state = new LinkState(
|
|
1199
|
+
(socket) => {
|
|
1200
|
+
this.attachSocket(socket);
|
|
1201
|
+
},
|
|
1202
|
+
this.ctx.logger,
|
|
1203
|
+
() => this.resolveTokenForDial(),
|
|
1204
|
+
instanceId,
|
|
1205
|
+
trimBase(origin)
|
|
1206
|
+
);
|
|
1132
1207
|
this.linkStates.set(instanceId, state);
|
|
1133
1208
|
state.dial();
|
|
1134
1209
|
return state;
|
|
1135
1210
|
}
|
|
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
1211
|
/** Push one serialized lifecycle fact out to every linked peer over WS. */
|
|
1141
1212
|
fanout(notification) {
|
|
1142
1213
|
this.broadcast(notification);
|
|
@@ -1146,11 +1217,15 @@ var InterconnectService = class extends Service {
|
|
|
1146
1217
|
if (this.sockets.size === 0) return;
|
|
1147
1218
|
const frame = { type: "event", notification };
|
|
1148
1219
|
const encoded = JSON.stringify(frame);
|
|
1220
|
+
const sent = /* @__PURE__ */ new Set();
|
|
1149
1221
|
for (const socket of this.sockets) {
|
|
1150
1222
|
if (socket.readyState !== WebSocket.OPEN) {
|
|
1151
1223
|
this.sockets.delete(socket);
|
|
1152
1224
|
continue;
|
|
1153
1225
|
}
|
|
1226
|
+
const peer = this.peerOf.get(socket);
|
|
1227
|
+
if (peer !== void 0 && sent.has(peer)) continue;
|
|
1228
|
+
if (peer !== void 0) sent.add(peer);
|
|
1154
1229
|
socket.send(encoded);
|
|
1155
1230
|
}
|
|
1156
1231
|
}
|
|
@@ -1160,27 +1235,18 @@ var InterconnectService = class extends Service {
|
|
|
1160
1235
|
* result so a throwing handler cannot escape the socket's message callback.
|
|
1161
1236
|
*/
|
|
1162
1237
|
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
|
-
}
|
|
1238
|
+
const result = await this.deliver({
|
|
1239
|
+
sessionId: message.sessionId,
|
|
1240
|
+
text: message.text,
|
|
1241
|
+
/* v8 ignore next 1 -- schemastery resolves an absent optional `sender` to an empty object, never undefined. */
|
|
1242
|
+
...message.sender === void 0 ? {} : { sender: message.sender },
|
|
1243
|
+
...message.delivery === void 0 ? {} : { delivery: message.delivery },
|
|
1244
|
+
...message.resume === void 0 ? {} : { resume: message.resume }
|
|
1245
|
+
});
|
|
1180
1246
|
this.sendFrame(socket, { type: "msg-result", reqId, result });
|
|
1181
1247
|
}
|
|
1182
1248
|
/** Dispatch one inbound `query` frame (ping/list/event) and answer on the socket. */
|
|
1183
|
-
|
|
1249
|
+
handleQueryFrame(socket, reqId, query) {
|
|
1184
1250
|
let result;
|
|
1185
1251
|
if (query.kind === "ping") {
|
|
1186
1252
|
result = { pong: true, instance: this.instanceId };
|
|
@@ -1202,9 +1268,11 @@ var InterconnectService = class extends Service {
|
|
|
1202
1268
|
this.ctx.emit("interconnect/event", eventPayload.notification, eventPayload.sender);
|
|
1203
1269
|
}
|
|
1204
1270
|
/**
|
|
1205
|
-
* Summarize
|
|
1271
|
+
* Summarize the live local sessions so a sender can discover valid targets
|
|
1206
1272
|
* instead of having to know a session id already. Only live agents are listed
|
|
1207
|
-
* because `send` can reach exactly those
|
|
1273
|
+
* because `send` can reach exactly those, and the answer carries at most
|
|
1274
|
+
* {@link MAX_LISTED_SESSIONS} rows so a large live set cannot push the
|
|
1275
|
+
* `query-result` frame past the link's frame cap.
|
|
1208
1276
|
*
|
|
1209
1277
|
* Title and status are best-effort: the title projection is an optional
|
|
1210
1278
|
* service, and a receiver without it still returns the ids. A projection that
|
|
@@ -1213,7 +1281,7 @@ var InterconnectService = class extends Service {
|
|
|
1213
1281
|
*/
|
|
1214
1282
|
listSessions() {
|
|
1215
1283
|
const reachable = this.ctx.agents.list().filter((agent) => !isSessionOwnedBySubagent(this.ctx, agent.session, agent));
|
|
1216
|
-
const sessions = reachable.map((agent) => {
|
|
1284
|
+
const sessions = reachable.slice(0, MAX_LISTED_SESSIONS).map((agent) => {
|
|
1217
1285
|
let title;
|
|
1218
1286
|
try {
|
|
1219
1287
|
const snapshot = this.ctx.get("sessionProjections")?.snapshot(agent.session);
|
|
@@ -1253,7 +1321,9 @@ var InterconnectService = class extends Service {
|
|
|
1253
1321
|
if (resolved === void 0 || resolved === null) return { reason: "session-not-live" };
|
|
1254
1322
|
return { agent: resolved };
|
|
1255
1323
|
} catch (error) {
|
|
1256
|
-
this.ctx.logger.info(
|
|
1324
|
+
this.ctx.logger.info(
|
|
1325
|
+
`interconnect: resume refused for ${payload.sessionId}: ${error instanceof Error ? error.message : String(error)}`
|
|
1326
|
+
);
|
|
1257
1327
|
return { reason: "resume-failed" };
|
|
1258
1328
|
}
|
|
1259
1329
|
}
|
|
@@ -1262,9 +1332,11 @@ var InterconnectService = class extends Service {
|
|
|
1262
1332
|
* when the sender asked and this receiver allows it.
|
|
1263
1333
|
*/
|
|
1264
1334
|
async deliver(payload) {
|
|
1335
|
+
if (this.disposed) return { delivered: false, instance: this.instanceId, reason: "unreachable" };
|
|
1265
1336
|
let agent = this.ctx.agents.get(payload.sessionId);
|
|
1266
1337
|
if (agent === void 0) {
|
|
1267
1338
|
const woken = await this.wake(payload);
|
|
1339
|
+
if (this.disposed) return { delivered: false, instance: this.instanceId, reason: "unreachable" };
|
|
1268
1340
|
if ("reason" in woken) {
|
|
1269
1341
|
return { delivered: false, instance: this.instanceId, reason: woken.reason };
|
|
1270
1342
|
}
|
|
@@ -1275,11 +1347,11 @@ var InterconnectService = class extends Service {
|
|
|
1275
1347
|
}
|
|
1276
1348
|
const sender = (() => {
|
|
1277
1349
|
const candidate = payload.sender;
|
|
1278
|
-
|
|
1350
|
+
if (typeof candidate !== "object" || candidate === null) return void 0;
|
|
1351
|
+
const record = candidate;
|
|
1352
|
+
if (typeof record.instanceId !== "string" || typeof record.sessionId !== "string") return void 0;
|
|
1353
|
+
return { instanceId: record.instanceId, sessionId: record.sessionId };
|
|
1279
1354
|
})();
|
|
1280
|
-
if (sender !== void 0) {
|
|
1281
|
-
this.senders.set(payload.sessionId, sender);
|
|
1282
|
-
}
|
|
1283
1355
|
const message = createUserMessage({
|
|
1284
1356
|
source: {
|
|
1285
1357
|
kind: "plugin",
|
|
@@ -1302,27 +1374,21 @@ var InterconnectService = class extends Service {
|
|
|
1302
1374
|
case "followup":
|
|
1303
1375
|
agent.followup(message);
|
|
1304
1376
|
break;
|
|
1377
|
+
/* v8 ignore next -- closed-union exhaustiveness guard */
|
|
1378
|
+
default:
|
|
1379
|
+
assertNever(mode);
|
|
1380
|
+
}
|
|
1381
|
+
if (sender !== void 0) {
|
|
1382
|
+
this.senders.set(payload.sessionId, sender);
|
|
1383
|
+
} else {
|
|
1384
|
+
this.senders.delete(payload.sessionId);
|
|
1305
1385
|
}
|
|
1306
1386
|
return { delivered: true, instance: this.instanceId, delivery: mode };
|
|
1307
1387
|
}
|
|
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
1388
|
async resolveToken() {
|
|
1322
1389
|
const credential = await this.ctx.credentials.resolve(credentialRef(INTERCONNECT_TOKEN_REF));
|
|
1323
1390
|
return credential === void 0 || credential.value.length === 0 ? void 0 : credential.value;
|
|
1324
1391
|
}
|
|
1325
|
-
/** Resolve the shared token for an outbound dial (same source as inbound). */
|
|
1326
1392
|
resolveTokenForDial() {
|
|
1327
1393
|
return this.resolveToken();
|
|
1328
1394
|
}
|
|
@@ -1340,6 +1406,10 @@ var InterconnectService = class extends Service {
|
|
|
1340
1406
|
socket.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Length: 9\r\n\r\nforbidden");
|
|
1341
1407
|
return;
|
|
1342
1408
|
}
|
|
1409
|
+
if (this.disposed) {
|
|
1410
|
+
socket.end("HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\nContent-Length: 11\r\n\r\nunavailable");
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1343
1413
|
const expected = `Bearer ${token}`;
|
|
1344
1414
|
const header = req.headers.authorization;
|
|
1345
1415
|
if (header === void 0 || !timingSafeEqual(header, expected)) {
|
|
@@ -1353,7 +1423,10 @@ var InterconnectService = class extends Service {
|
|
|
1353
1423
|
/**
|
|
1354
1424
|
* Install frame + liveness handling on one socket and add it to the live
|
|
1355
1425
|
* pool. Used by both the server half (accepted upgrade) and the client half
|
|
1356
|
-
* (outbound dial), so a single socket carries events both directions.
|
|
1426
|
+
* (outbound dial), so a single socket carries events both directions. Not a
|
|
1427
|
+
* model-facing entry point; the outbound dial reaches it through a private
|
|
1428
|
+
* closure.
|
|
1429
|
+
* @param websocket - the accepted or dialed socket to install handlers on.
|
|
1357
1430
|
*/
|
|
1358
1431
|
attachSocket(websocket) {
|
|
1359
1432
|
this.sockets.add(websocket);
|
|
@@ -1362,7 +1435,8 @@ var InterconnectService = class extends Service {
|
|
|
1362
1435
|
;
|
|
1363
1436
|
websocket.isAlive = true;
|
|
1364
1437
|
});
|
|
1365
|
-
websocket.on("message", (data) => {
|
|
1438
|
+
websocket.on("message", (data, isBinary) => {
|
|
1439
|
+
if (isBinary) return;
|
|
1366
1440
|
this.handleFrame(websocket, data);
|
|
1367
1441
|
});
|
|
1368
1442
|
websocket.on("close", () => {
|
|
@@ -1376,31 +1450,43 @@ var InterconnectService = class extends Service {
|
|
|
1376
1450
|
}
|
|
1377
1451
|
/** Parse and route one inbound link frame, attributing events to the socket's announced peer. */
|
|
1378
1452
|
handleFrame(socket, data) {
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1453
|
+
const byteLength = typeof data === "string" ? Buffer.byteLength(data) : Array.isArray(data) ? data.reduce((total, part) => total + part.byteLength, 0) : data.byteLength;
|
|
1454
|
+
if (byteLength > MAX_LINK_FRAME_BYTES) {
|
|
1455
|
+
this.ctx.logger.warn("interconnect: dropping oversized link frame");
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
const text = typeof data === "string" ? data : Array.isArray(data) ? Buffer.concat(data).toString("utf8") : Buffer.isBuffer(data) ? data.toString("utf8") : Buffer.from(data).toString("utf8");
|
|
1459
|
+
let parsed;
|
|
1382
1460
|
try {
|
|
1383
|
-
|
|
1461
|
+
parsed = Schema.resolve(JSON.parse(text), linkFrameSchema, {})[0];
|
|
1384
1462
|
} catch {
|
|
1385
1463
|
this.ctx.logger.warn("interconnect: dropping malformed link frame");
|
|
1386
1464
|
return;
|
|
1387
1465
|
}
|
|
1466
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
1467
|
+
this.ctx.logger.warn("interconnect: dropping malformed link frame");
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
const frame = parsed;
|
|
1388
1471
|
if (frame.type === "hello") {
|
|
1389
1472
|
this.peerOf.set(socket, frame.sender);
|
|
1390
1473
|
return;
|
|
1391
1474
|
}
|
|
1392
1475
|
if (frame.type === "event") {
|
|
1393
1476
|
const sender = this.peerOf.get(socket) ?? "unknown-peer";
|
|
1477
|
+
const kind = frame.notification.kind;
|
|
1394
1478
|
try {
|
|
1395
1479
|
this.receiveEvent({ sender, notification: frame.notification });
|
|
1396
1480
|
} catch (error) {
|
|
1397
|
-
this.ctx.logger.warn(
|
|
1481
|
+
this.ctx.logger.warn(
|
|
1482
|
+
`interconnect: listener for a ${kind} event from ${sender} threw: ` + (error instanceof Error ? error.message : String(error))
|
|
1483
|
+
);
|
|
1398
1484
|
}
|
|
1399
1485
|
return;
|
|
1400
1486
|
}
|
|
1401
1487
|
if (frame.type === "msg-result") {
|
|
1402
1488
|
const pending = this.pendingMessages.get(frame.reqId);
|
|
1403
|
-
if (pending !== void 0) {
|
|
1489
|
+
if (pending !== void 0 && pending.kind === "msg") {
|
|
1404
1490
|
;
|
|
1405
1491
|
pending.resolve?.(frame.result);
|
|
1406
1492
|
}
|
|
@@ -1408,7 +1494,7 @@ var InterconnectService = class extends Service {
|
|
|
1408
1494
|
}
|
|
1409
1495
|
if (frame.type === "query-result") {
|
|
1410
1496
|
const pending = this.pendingMessages.get(frame.reqId);
|
|
1411
|
-
if (pending !== void 0) {
|
|
1497
|
+
if (pending !== void 0 && pending.kind === "query" && (pending.accept === void 0 || pending.accept(frame.result))) {
|
|
1412
1498
|
;
|
|
1413
1499
|
pending.resolve?.(frame.result);
|
|
1414
1500
|
}
|
|
@@ -1427,8 +1513,8 @@ var InterconnectService = class extends Service {
|
|
|
1427
1513
|
});
|
|
1428
1514
|
return;
|
|
1429
1515
|
}
|
|
1430
|
-
void Promise.resolve().then(
|
|
1431
|
-
|
|
1516
|
+
void Promise.resolve().then(() => {
|
|
1517
|
+
this.handleQueryFrame(socket, frame.reqId, frame.query);
|
|
1432
1518
|
}).catch((error) => {
|
|
1433
1519
|
this.ctx.logger.warn(`interconnect: query ${frame.reqId} handler threw: ${error instanceof Error ? error.message : String(error)}`);
|
|
1434
1520
|
});
|
|
@@ -1441,15 +1527,21 @@ var InterconnectService = class extends Service {
|
|
|
1441
1527
|
}
|
|
1442
1528
|
};
|
|
1443
1529
|
var LinkState = class {
|
|
1444
|
-
constructor(
|
|
1445
|
-
this.
|
|
1530
|
+
constructor(attachSocket, logger, resolveToken, instanceId, origin) {
|
|
1531
|
+
this.attachSocket = attachSocket;
|
|
1532
|
+
this.logger = logger;
|
|
1533
|
+
this.resolveToken = resolveToken;
|
|
1446
1534
|
this.instanceId = instanceId;
|
|
1447
1535
|
this.peer = origin;
|
|
1448
1536
|
}
|
|
1449
1537
|
socket;
|
|
1538
|
+
dialEpoch = 0;
|
|
1450
1539
|
closed = false;
|
|
1451
1540
|
retry = 0;
|
|
1452
1541
|
reconnectTimer;
|
|
1542
|
+
warnedNoToken = false;
|
|
1543
|
+
/** Dial-registered handlers, removed by reference on close/reroute so the pool-cleanup handlers attachSocket added stay alive. */
|
|
1544
|
+
dialListeners;
|
|
1453
1545
|
// `peer` is mutable so `reroute` can point the link at a new origin.
|
|
1454
1546
|
peer;
|
|
1455
1547
|
instanceId;
|
|
@@ -1457,9 +1549,16 @@ var LinkState = class {
|
|
|
1457
1549
|
reroute(origin) {
|
|
1458
1550
|
if (origin === this.peer) return;
|
|
1459
1551
|
this.peer = origin;
|
|
1552
|
+
this.dialEpoch += 1;
|
|
1553
|
+
if (this.reconnectTimer !== void 0) {
|
|
1554
|
+
clearTimeout(this.reconnectTimer);
|
|
1555
|
+
this.reconnectTimer = void 0;
|
|
1556
|
+
}
|
|
1460
1557
|
const socket = this.socket;
|
|
1461
1558
|
if (socket !== void 0) {
|
|
1462
|
-
|
|
1559
|
+
this.removeDialListeners(socket);
|
|
1560
|
+
socket.on("error", () => {
|
|
1561
|
+
});
|
|
1463
1562
|
socket.terminate();
|
|
1464
1563
|
this.socket = void 0;
|
|
1465
1564
|
}
|
|
@@ -1468,29 +1567,59 @@ var LinkState = class {
|
|
|
1468
1567
|
/** Open the socket; reconnect is scheduled by the close handler. */
|
|
1469
1568
|
dial() {
|
|
1470
1569
|
if (this.closed) return;
|
|
1471
|
-
const
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1570
|
+
const epoch = this.dialEpoch;
|
|
1571
|
+
let url;
|
|
1572
|
+
try {
|
|
1573
|
+
url = linkUrl(this.peer);
|
|
1574
|
+
} catch {
|
|
1575
|
+
this.logger.warn(`interconnect: invalid peer origin ${this.peer}; link to ${this.instanceId} stays down`);
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
void this.resolveToken().then((token) => {
|
|
1579
|
+
if (token === void 0) {
|
|
1580
|
+
if (!this.warnedNoToken) {
|
|
1581
|
+
this.warnedNoToken = true;
|
|
1582
|
+
this.logger.warn(`interconnect: no shared token configured; peer ${this.instanceId} link stays down, retrying until one is set`);
|
|
1583
|
+
}
|
|
1584
|
+
if (this.closed || epoch !== this.dialEpoch) return;
|
|
1585
|
+
this.scheduleReconnect();
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
if (this.closed || epoch !== this.dialEpoch) return;
|
|
1475
1589
|
const socket = new WebSocket(url, {
|
|
1476
|
-
headers: { authorization: `Bearer ${token}` }
|
|
1590
|
+
headers: { authorization: `Bearer ${token}` },
|
|
1591
|
+
maxPayload: MAX_LINK_FRAME_BYTES,
|
|
1592
|
+
// A peer that accepts TCP but never completes the upgrade would leave
|
|
1593
|
+
// the socket in CONNECTING forever: no open (so no heartbeat) and no
|
|
1594
|
+
// close (so no reconnect). ws aborts the connection past the handshake
|
|
1595
|
+
// window, so its own error+close fire and the close handler schedules
|
|
1596
|
+
// a reconnect — and ws owns the timer, so nothing leaks on teardown.
|
|
1597
|
+
handshakeTimeout: LINK_HANDSHAKE_TIMEOUT_MS
|
|
1477
1598
|
});
|
|
1478
1599
|
this.socket = socket;
|
|
1479
|
-
|
|
1600
|
+
const onOpen = () => {
|
|
1480
1601
|
this.retry = 0;
|
|
1481
|
-
this.
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1602
|
+
this.attachSocket(socket);
|
|
1603
|
+
};
|
|
1604
|
+
const onClose = () => {
|
|
1484
1605
|
if (this.closed) return;
|
|
1485
1606
|
this.scheduleReconnect();
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
}
|
|
1607
|
+
};
|
|
1608
|
+
const onError = () => {
|
|
1609
|
+
};
|
|
1610
|
+
this.dialListeners = { open: onOpen, close: onClose, error: onError };
|
|
1611
|
+
socket.once("open", onOpen);
|
|
1612
|
+
socket.once("close", onClose);
|
|
1613
|
+
socket.on("error", onError);
|
|
1489
1614
|
}).catch(() => {
|
|
1490
|
-
if (this.closed) return;
|
|
1615
|
+
if (this.closed || epoch !== this.dialEpoch) return;
|
|
1491
1616
|
this.scheduleReconnect();
|
|
1492
1617
|
});
|
|
1493
1618
|
}
|
|
1619
|
+
/** Whether this link has been closed; a closed state is never reused by `link()`. */
|
|
1620
|
+
isClosed() {
|
|
1621
|
+
return this.closed;
|
|
1622
|
+
}
|
|
1494
1623
|
/** Whether this link currently holds an open socket that can carry frames. */
|
|
1495
1624
|
writable() {
|
|
1496
1625
|
return this.socket !== void 0 && this.socket.readyState === WebSocket.OPEN;
|
|
@@ -1504,14 +1633,24 @@ var LinkState = class {
|
|
|
1504
1633
|
}
|
|
1505
1634
|
close() {
|
|
1506
1635
|
this.closed = true;
|
|
1507
|
-
this.owner.forgetLink(this.peer);
|
|
1508
1636
|
if (this.reconnectTimer !== void 0) clearTimeout(this.reconnectTimer);
|
|
1509
1637
|
const socket = this.socket;
|
|
1510
1638
|
if (socket !== void 0) {
|
|
1511
|
-
|
|
1639
|
+
this.removeDialListeners(socket);
|
|
1640
|
+
socket.on("error", () => {
|
|
1641
|
+
});
|
|
1512
1642
|
socket.terminate();
|
|
1513
1643
|
}
|
|
1514
1644
|
}
|
|
1645
|
+
/** Detach the dial-registered handlers from a socket before terminating it. */
|
|
1646
|
+
removeDialListeners(socket) {
|
|
1647
|
+
const listeners = this.dialListeners;
|
|
1648
|
+
if (listeners === void 0) return;
|
|
1649
|
+
socket.removeListener("open", listeners.open);
|
|
1650
|
+
socket.removeListener("close", listeners.close);
|
|
1651
|
+
socket.removeListener("error", listeners.error);
|
|
1652
|
+
this.dialListeners = void 0;
|
|
1653
|
+
}
|
|
1515
1654
|
scheduleReconnect() {
|
|
1516
1655
|
const delay = Math.min(3e4, 1e3 * 2 ** this.retry);
|
|
1517
1656
|
this.retry += 1;
|
|
@@ -1522,12 +1661,16 @@ var LinkState = class {
|
|
|
1522
1661
|
}
|
|
1523
1662
|
};
|
|
1524
1663
|
function timingSafeEqual(a, b) {
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1664
|
+
return constantTimeEqual(createHash("sha256").update(a, "utf8").digest(), createHash("sha256").update(b, "utf8").digest());
|
|
1665
|
+
}
|
|
1666
|
+
function linkUrl(origin) {
|
|
1667
|
+
const url = new URL("/interconnect/link", origin);
|
|
1668
|
+
if (url.protocol === "http:") url.protocol = "ws:";
|
|
1669
|
+
else if (url.protocol === "https:") url.protocol = "wss:";
|
|
1670
|
+
if (url.protocol !== "ws:" && url.protocol !== "wss:") {
|
|
1671
|
+
throw new TypeError(`interconnect: unsupported peer origin protocol ${url.protocol}`);
|
|
1672
|
+
}
|
|
1673
|
+
return url;
|
|
1531
1674
|
}
|
|
1532
1675
|
function trimBase(baseUrl) {
|
|
1533
1676
|
return baseUrl.replace(/\/+$/, "");
|
|
@@ -1536,6 +1679,7 @@ var index_default = InterconnectService;
|
|
|
1536
1679
|
export {
|
|
1537
1680
|
INTERCONNECT_TOKEN_REF,
|
|
1538
1681
|
InterconnectService,
|
|
1539
|
-
index_default as default
|
|
1682
|
+
index_default as default,
|
|
1683
|
+
linkUrl
|
|
1540
1684
|
};
|
|
1541
1685
|
//# sourceMappingURL=index.js.map
|