dsh-interconnect 0.7.0 → 0.10.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 +76 -32
- package/assets/dsh-interconnect.md +92 -0
- package/cordis.patch.yml +4 -1
- package/dsh.plugin.json +2 -2
- package/lib/index.js +268 -285
- package/lib/interconnect/index.js +268 -285
- package/lib/interconnect/index.js.map +3 -3
- package/lib/skill-interconnect/index.js +50 -0
- package/lib/skill-interconnect/index.js.map +7 -0
- package/lib/skill-interconnect/invariant.js +13 -0
- package/lib/skill-interconnect/invariant.js.map +7 -0
- package/lib/tool-interconnect/index.js +14 -15
- package/lib/tool-interconnect/index.js.map +2 -2
- package/lib/types/interconnect/index.d.ts +57 -33
- package/lib/types/interconnect/index.d.ts.map +1 -1
- package/lib/types/interconnect/types.d.ts +86 -32
- package/lib/types/interconnect/types.d.ts.map +1 -1
- package/lib/types/skill-interconnect/index.d.ts +20 -0
- package/lib/types/skill-interconnect/index.d.ts.map +1 -0
- package/lib/types/skill-interconnect/invariant.d.ts +16 -0
- package/lib/types/skill-interconnect/invariant.d.ts.map +1 -0
- package/lib/types/tool-interconnect/index.d.ts.map +1 -1
- package/package.json +13 -2
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
// src/interconnect/index.ts
|
|
2
2
|
import { Service } from "@deepseek-ai/cordis";
|
|
3
|
-
import {
|
|
4
|
-
clientRequestSchema,
|
|
5
|
-
serverResponseSchema,
|
|
6
|
-
RpcId
|
|
7
|
-
} from "@deepseek-ai/dsh-host-apiproxy/api";
|
|
8
3
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
9
4
|
import { boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
10
5
|
import { hasApiRemoteSubagentOwner } from "@deepseek-ai/dsh-api-remotes";
|
|
@@ -826,12 +821,9 @@ defineMethod("transform", [
|
|
|
826
821
|
import WebSocket, { WebSocketServer } from "ws";
|
|
827
822
|
|
|
828
823
|
// src/interconnect/types.ts
|
|
829
|
-
var INTERCONNECT_CHANNEL = "/interconnect";
|
|
830
824
|
var INTERCONNECT_TOKEN_REF = "DSH_INTERCONNECT_TOKEN";
|
|
831
825
|
|
|
832
826
|
// src/interconnect/index.ts
|
|
833
|
-
var INVALID_REQUEST_RPC_ID = RpcId("invalid-request");
|
|
834
|
-
var MAX_REQUEST_BODY_BYTES = 1024 * 1024;
|
|
835
827
|
var PLUGIN_SOURCE = "dsh-interconnect";
|
|
836
828
|
var LINK_CHANNEL = "/interconnect/link";
|
|
837
829
|
var notificationSchema = Schema.union([
|
|
@@ -842,70 +834,78 @@ var notificationSchema = Schema.union([
|
|
|
842
834
|
Schema.object({ kind: Schema.const("session/disposed"), sessionId: Schema.string() }),
|
|
843
835
|
Schema.object({ kind: Schema.const("subagent/end"), provider: Schema.string(), childSessionId: Schema.string(), stopReason: Schema.string() })
|
|
844
836
|
]);
|
|
837
|
+
var senderSchema = Schema.object({ instanceId: Schema.string(), sessionId: Schema.string() });
|
|
838
|
+
var messageSchema = Schema.union([
|
|
839
|
+
Schema.object({
|
|
840
|
+
kind: Schema.const("send"),
|
|
841
|
+
sessionId: Schema.string(),
|
|
842
|
+
text: Schema.string(),
|
|
843
|
+
sender: senderSchema,
|
|
844
|
+
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]),
|
|
845
|
+
resume: Schema.boolean()
|
|
846
|
+
}),
|
|
847
|
+
Schema.object({
|
|
848
|
+
kind: Schema.const("reply"),
|
|
849
|
+
sessionId: Schema.string(),
|
|
850
|
+
text: Schema.string(),
|
|
851
|
+
sender: senderSchema,
|
|
852
|
+
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]),
|
|
853
|
+
resume: Schema.boolean()
|
|
854
|
+
})
|
|
855
|
+
]);
|
|
856
|
+
var querySchema = Schema.union([
|
|
857
|
+
Schema.object({ kind: Schema.const("ping") }),
|
|
858
|
+
Schema.object({ kind: Schema.const("list") }),
|
|
859
|
+
Schema.object({ kind: Schema.const("event"), notification: notificationSchema })
|
|
860
|
+
]);
|
|
845
861
|
var linkFrameSchema = Schema.union([
|
|
846
862
|
Schema.object({ type: Schema.const("hello"), sender: Schema.string() }),
|
|
847
|
-
Schema.object({ type: Schema.const("event"), notification: notificationSchema })
|
|
863
|
+
Schema.object({ type: Schema.const("event"), notification: notificationSchema }),
|
|
864
|
+
Schema.object({ type: Schema.const("msg"), reqId: Schema.string(), message: messageSchema }),
|
|
865
|
+
Schema.object({ type: Schema.const("msg-result"), reqId: Schema.string(), result: Schema.object({
|
|
866
|
+
delivered: Schema.boolean(),
|
|
867
|
+
instance: Schema.string(),
|
|
868
|
+
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]),
|
|
869
|
+
reason: Schema.string()
|
|
870
|
+
}) }),
|
|
871
|
+
Schema.object({ type: Schema.const("query"), reqId: Schema.string(), query: querySchema }),
|
|
872
|
+
Schema.object({ type: Schema.const("query-result"), reqId: Schema.string(), result: Schema.any() })
|
|
848
873
|
]);
|
|
849
|
-
var sendPayloadSchema = Schema.object({
|
|
850
|
-
sessionId: Schema.string(),
|
|
851
|
-
text: Schema.string(),
|
|
852
|
-
sender: Schema.object({
|
|
853
|
-
baseUrl: Schema.string(),
|
|
854
|
-
instanceId: Schema.string(),
|
|
855
|
-
sessionId: Schema.string()
|
|
856
|
-
}),
|
|
857
|
-
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]),
|
|
858
|
-
resume: Schema.boolean()
|
|
859
|
-
});
|
|
860
|
-
var replyPayloadSchema = Schema.object({
|
|
861
|
-
sessionId: Schema.string(),
|
|
862
|
-
text: Schema.string(),
|
|
863
|
-
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]),
|
|
864
|
-
resume: Schema.boolean()
|
|
865
|
-
});
|
|
866
|
-
var eventPayloadSchema = Schema.object({
|
|
867
|
-
sender: Schema.string(),
|
|
868
|
-
notification: notificationSchema
|
|
869
|
-
});
|
|
870
874
|
var InterconnectService = class extends Service {
|
|
871
875
|
static inject = ["webServer", "agents", "credentials"];
|
|
872
876
|
static Config = Schema.object({
|
|
873
877
|
instanceId: Schema.string().default("dsh"),
|
|
874
|
-
selfBaseUrl: Schema.string(),
|
|
875
878
|
requestTimeoutMs: Schema.natural().max(6e4).default(1e4),
|
|
876
|
-
peers: Schema.
|
|
879
|
+
peers: Schema.object({}).default({}),
|
|
877
880
|
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]).default("followup"),
|
|
878
881
|
allowResume: Schema.boolean().default(true)
|
|
879
882
|
});
|
|
880
883
|
instanceId;
|
|
881
|
-
selfBaseUrl;
|
|
882
884
|
requestTimeoutMs;
|
|
883
885
|
delivery;
|
|
884
886
|
allowResume;
|
|
885
|
-
peers = /* @__PURE__ */ new Set();
|
|
886
887
|
subscriptions = [];
|
|
887
888
|
server = new WebSocketServer({ noServer: true });
|
|
888
889
|
sockets = /* @__PURE__ */ new Set();
|
|
890
|
+
/** Outbound peer links keyed by the peer's `instanceId`. */
|
|
889
891
|
linkStates = /* @__PURE__ */ new Map();
|
|
890
892
|
heartbeatTimer;
|
|
891
893
|
/** Peer identity each live socket announced via its `hello` frame, if any. */
|
|
892
894
|
peerOf = /* @__PURE__ */ new WeakMap();
|
|
893
895
|
/** Sender each local session last received a send from, keyed by local session id. */
|
|
894
896
|
senders = /* @__PURE__ */ new Map();
|
|
897
|
+
/** In-flight frames sent over a peer link, keyed by `reqId`, awaiting a correlated result. */
|
|
898
|
+
pendingMessages = /* @__PURE__ */ new Map();
|
|
899
|
+
reqIdCounter = 0;
|
|
895
900
|
constructor(ctx, config) {
|
|
896
901
|
super(ctx, "interconnect");
|
|
897
902
|
this.instanceId = config.instanceId;
|
|
898
|
-
this.selfBaseUrl = config.selfBaseUrl;
|
|
899
903
|
this.requestTimeoutMs = config.requestTimeoutMs;
|
|
900
904
|
this.delivery = config.delivery ?? "followup";
|
|
901
905
|
this.allowResume = config.allowResume ?? true;
|
|
902
|
-
for (const
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
path: INTERCONNECT_CHANNEL,
|
|
906
|
-
handler: (req, res) => this.handle(req, res)
|
|
907
|
-
};
|
|
908
|
-
ctx.effect(() => ctx.webServer.register(route), "interconnect: /interconnect route");
|
|
906
|
+
for (const [peerInstanceId, origin] of Object.entries(config.peers ?? {})) {
|
|
907
|
+
this.link(peerInstanceId, origin);
|
|
908
|
+
}
|
|
909
909
|
const upgrade = {
|
|
910
910
|
path: LINK_CHANNEL,
|
|
911
911
|
handler: (req, socket, head) => {
|
|
@@ -967,13 +967,12 @@ var InterconnectService = class extends Service {
|
|
|
967
967
|
}, "interconnect: websocket teardown");
|
|
968
968
|
}
|
|
969
969
|
/**
|
|
970
|
-
* This instance's identity to attach to outbound messages
|
|
971
|
-
*
|
|
972
|
-
* peer cannot reply to them.
|
|
970
|
+
* This instance's identity to attach to outbound messages. Always present:
|
|
971
|
+
* it needs no address, only this instance's id and the calling session.
|
|
973
972
|
* @param sessionId - the local session that is sending, used as the reply target.
|
|
974
973
|
*/
|
|
975
974
|
selfSender(sessionId) {
|
|
976
|
-
return
|
|
975
|
+
return { instanceId: this.instanceId, sessionId };
|
|
977
976
|
}
|
|
978
977
|
async send(request) {
|
|
979
978
|
const payload = {
|
|
@@ -983,14 +982,14 @@ var InterconnectService = class extends Service {
|
|
|
983
982
|
...request.delivery === void 0 ? {} : { delivery: request.delivery },
|
|
984
983
|
...request.resume === void 0 ? {} : { resume: request.resume }
|
|
985
984
|
};
|
|
986
|
-
const result = await this.
|
|
985
|
+
const result = await this.msgRequest(request.instanceId, "send", payload);
|
|
987
986
|
return result ?? { delivered: false, instance: this.instanceId, reason: "unreachable" };
|
|
988
987
|
}
|
|
989
988
|
/**
|
|
990
989
|
* Deliver one text message back to the peer that a local session last
|
|
991
990
|
* received a send from. `sessionId` names the LOCAL replying session; the
|
|
992
|
-
* outbound target is the `sender` that session recorded,
|
|
993
|
-
*
|
|
991
|
+
* outbound target is the `sender` that session recorded, addressed through
|
|
992
|
+
* this instance's own link to the sender's instance.
|
|
994
993
|
*/
|
|
995
994
|
async reply(request) {
|
|
996
995
|
const sender = this.senders.get(request.sessionId);
|
|
@@ -1003,68 +1002,137 @@ var InterconnectService = class extends Service {
|
|
|
1003
1002
|
...request.delivery === void 0 ? {} : { delivery: request.delivery },
|
|
1004
1003
|
...request.resume === void 0 ? {} : { resume: request.resume },
|
|
1005
1004
|
// Attribute the reply to this instance so the peer, in turn, can reply
|
|
1006
|
-
// back — chaining the conversation.
|
|
1007
|
-
|
|
1005
|
+
// back — chaining the conversation.
|
|
1006
|
+
sender: this.selfSender(request.sessionId)
|
|
1008
1007
|
};
|
|
1009
|
-
const result = await this.
|
|
1008
|
+
const result = await this.msgRequest(sender.instanceId, "send", payload);
|
|
1010
1009
|
return result ?? { delivered: false, instance: this.instanceId, reason: "unreachable" };
|
|
1011
1010
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1011
|
+
/**
|
|
1012
|
+
* Probe a peer instance for liveness and identity over its persistent link.
|
|
1013
|
+
* Returns the peer identity when reachable, or undefined when the link is
|
|
1014
|
+
* not up or no answer arrives.
|
|
1015
|
+
*/
|
|
1016
|
+
async ping(instanceId) {
|
|
1017
|
+
return this.queryRequest(instanceId, { kind: "ping" });
|
|
1014
1018
|
}
|
|
1015
1019
|
/**
|
|
1016
|
-
* List
|
|
1017
|
-
*
|
|
1020
|
+
* List a peer instance's live sessions over its persistent link. Undefined on
|
|
1021
|
+
* transport failure, matching `ping`.
|
|
1018
1022
|
*/
|
|
1019
|
-
async list(
|
|
1020
|
-
return this.
|
|
1023
|
+
async list(instanceId) {
|
|
1024
|
+
return this.queryRequest(instanceId, { kind: "list" });
|
|
1025
|
+
}
|
|
1026
|
+
/** The origin this instance dials to reach a configured peer, or undefined. */
|
|
1027
|
+
originOf(instanceId) {
|
|
1028
|
+
const state = this.linkStates.get(instanceId);
|
|
1029
|
+
return state?.peer;
|
|
1021
1030
|
}
|
|
1022
1031
|
/**
|
|
1023
|
-
*
|
|
1024
|
-
*
|
|
1025
|
-
*
|
|
1026
|
-
*
|
|
1032
|
+
* Send a `msg` frame over the live link to a peer and resolve with its
|
|
1033
|
+
* correlated `msg-result`. Undefined when the peer has no live link here
|
|
1034
|
+
* (not configured, or the link is down) — sends can only address configured+
|
|
1035
|
+
* connected peers by design.
|
|
1027
1036
|
*/
|
|
1028
|
-
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1037
|
+
async msgRequest(instanceId, kind, payload) {
|
|
1038
|
+
const state = this.linkStates.get(instanceId);
|
|
1039
|
+
if (state === void 0 || !state.writable()) return void 0;
|
|
1040
|
+
const reqId = `m${++this.reqIdCounter}-${crypto.randomUUID()}`;
|
|
1041
|
+
const message = {
|
|
1042
|
+
kind,
|
|
1043
|
+
sessionId: payload.sessionId,
|
|
1044
|
+
text: payload.text,
|
|
1045
|
+
...payload.sender === void 0 ? {} : { sender: payload.sender },
|
|
1046
|
+
...payload.delivery === void 0 ? {} : { delivery: payload.delivery },
|
|
1047
|
+
...payload.resume === void 0 ? {} : { resume: payload.resume }
|
|
1048
|
+
};
|
|
1049
|
+
return this.waitForResult(reqId, (failure) => {
|
|
1050
|
+
const wrote = state.sendFrame({ type: "msg", reqId, message });
|
|
1051
|
+
if (!wrote) failure(new Error(`interconnect: peer link to ${instanceId} closed while sending`));
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
/** Send a `query` frame over the live link to a peer and resolve its result. */
|
|
1055
|
+
async queryRequest(instanceId, query) {
|
|
1056
|
+
const state = this.linkStates.get(instanceId);
|
|
1057
|
+
if (state === void 0 || !state.writable()) return void 0;
|
|
1058
|
+
const reqId = `q${++this.reqIdCounter}-${crypto.randomUUID()}`;
|
|
1059
|
+
return this.waitForResult(reqId, (failure) => {
|
|
1060
|
+
const wrote = state.sendFrame({ type: "query", reqId, query });
|
|
1061
|
+
if (!wrote) failure(new Error(`interconnect: peer link to ${instanceId} closed while querying`));
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
/** Settle a `reqId` frame against its `*-result`, timing out at `requestTimeoutMs`. */
|
|
1065
|
+
waitForResult(reqId, send) {
|
|
1066
|
+
return new Promise((resolve2, reject) => {
|
|
1067
|
+
const timer = setTimeout(() => {
|
|
1068
|
+
this.pendingMessages.delete(reqId);
|
|
1069
|
+
reject(new Error(`interconnect: no result for ${reqId} within ${this.requestTimeoutMs}ms`));
|
|
1070
|
+
}, this.requestTimeoutMs);
|
|
1071
|
+
this.pendingMessages.set(reqId, { timer });
|
|
1072
|
+
const pending = this.pendingMessages.get(reqId);
|
|
1073
|
+
if (pending === void 0) return;
|
|
1074
|
+
pending.resolve = (result) => {
|
|
1075
|
+
clearTimeout(timer);
|
|
1076
|
+
this.pendingMessages.delete(reqId);
|
|
1077
|
+
resolve2(result);
|
|
1078
|
+
};
|
|
1079
|
+
pending.reject = (error) => {
|
|
1080
|
+
clearTimeout(timer);
|
|
1081
|
+
this.pendingMessages.delete(reqId);
|
|
1082
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
1083
|
+
};
|
|
1084
|
+
send((error) => {
|
|
1085
|
+
clearTimeout(timer);
|
|
1086
|
+
this.pendingMessages.delete(reqId);
|
|
1087
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
1088
|
+
});
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Add a peer route at runtime. Returns a disposer that removes it. Re-adding
|
|
1093
|
+
* an existing instanceId re-routes it to the new origin.
|
|
1094
|
+
* @param instanceId - the peer's `instanceId`.
|
|
1095
|
+
* @param origin - origin this instance dials to reach that peer.
|
|
1096
|
+
* @returns disposer removing the peer route.
|
|
1097
|
+
*/
|
|
1098
|
+
subscribe(instanceId, origin) {
|
|
1099
|
+
this.link(instanceId, origin);
|
|
1031
1100
|
return () => {
|
|
1032
|
-
this.
|
|
1101
|
+
this.forgetLink(instanceId);
|
|
1033
1102
|
};
|
|
1034
1103
|
}
|
|
1035
|
-
/** Remove a peer
|
|
1036
|
-
unsubscribe(
|
|
1037
|
-
this.
|
|
1104
|
+
/** Remove a peer route, closing its outbound link. */
|
|
1105
|
+
unsubscribe(instanceId) {
|
|
1106
|
+
const state = this.linkStates.get(instanceId);
|
|
1107
|
+
if (state !== void 0) state.close();
|
|
1108
|
+
this.linkStates.delete(instanceId);
|
|
1038
1109
|
}
|
|
1039
1110
|
/**
|
|
1040
1111
|
* Open (and, on drop, re-open) a persistent WebSocket link to a peer. Local
|
|
1041
1112
|
* events stream over the link in real time, and events the peer pushes are
|
|
1042
|
-
* surfaced as `interconnect/event`. Repeating for the same
|
|
1043
|
-
*
|
|
1044
|
-
* @param
|
|
1113
|
+
* surfaced as `interconnect/event`. Repeating for the same instanceId
|
|
1114
|
+
* re-routes the link to the new origin.
|
|
1115
|
+
* @param instanceId - the peer's `instanceId`.
|
|
1116
|
+
* @param origin - receiver origin this instance dials, e.g. `http://127.0.0.1:13080`.
|
|
1045
1117
|
* @returns a handle closing the link and cancelling reconnection.
|
|
1046
1118
|
*/
|
|
1047
|
-
link(
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1119
|
+
link(instanceId, origin) {
|
|
1120
|
+
const existing = this.linkStates.get(instanceId);
|
|
1121
|
+
if (existing !== void 0) {
|
|
1122
|
+
existing.reroute(trimBase(origin));
|
|
1123
|
+
return existing;
|
|
1124
|
+
}
|
|
1125
|
+
const state = new LinkState(this, instanceId, trimBase(origin));
|
|
1126
|
+
this.linkStates.set(instanceId, state);
|
|
1053
1127
|
state.dial();
|
|
1054
1128
|
return state;
|
|
1055
1129
|
}
|
|
1056
1130
|
/** Remove a closed outbound link's state so a later `link` re-dials fresh. */
|
|
1057
|
-
forgetLink(
|
|
1058
|
-
this.linkStates.delete(
|
|
1131
|
+
forgetLink(instanceId) {
|
|
1132
|
+
this.linkStates.delete(instanceId);
|
|
1059
1133
|
}
|
|
1060
|
-
/**
|
|
1134
|
+
/** Push one serialized lifecycle fact out to every linked peer over WS. */
|
|
1061
1135
|
fanout(notification) {
|
|
1062
|
-
const payload = { sender: this.instanceId, notification };
|
|
1063
|
-
for (const peer of this.peers) {
|
|
1064
|
-
void this.post(peer, "event", payload).catch((error) => {
|
|
1065
|
-
this.ctx.logger.warn(`interconnect: event fan-out to ${peer} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1066
|
-
});
|
|
1067
|
-
}
|
|
1068
1136
|
this.broadcast(notification);
|
|
1069
1137
|
}
|
|
1070
1138
|
/** Push one fact over every live WebSocket link, dropping closed sockets. */
|
|
@@ -1080,132 +1148,47 @@ var InterconnectService = class extends Service {
|
|
|
1080
1148
|
socket.send(encoded);
|
|
1081
1149
|
}
|
|
1082
1150
|
}
|
|
1083
|
-
/**
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
return;
|
|
1097
|
-
}
|
|
1098
|
-
await this.dispatch(req, res);
|
|
1099
|
-
}
|
|
1100
|
-
/** Parse the ClientRequest envelope and route to the named endpoint. */
|
|
1101
|
-
async dispatch(req, res) {
|
|
1102
|
-
const pathname = new URL(req.url ?? "/", "http://dsh.internal").pathname;
|
|
1103
|
-
const endpoint = endpointFromPath(INTERCONNECT_CHANNEL, pathname);
|
|
1104
|
-
if (req.method !== "POST" || endpoint === void 0) {
|
|
1105
|
-
res.writeHead(404);
|
|
1106
|
-
res.end("not found");
|
|
1107
|
-
return;
|
|
1108
|
-
}
|
|
1109
|
-
const mediaType = (req.headers["content-type"] ?? "").split(";", 1)[0]?.trim().toLowerCase();
|
|
1110
|
-
if (mediaType !== "application/json") {
|
|
1111
|
-
res.writeHead(415);
|
|
1112
|
-
res.end("content type must be application/json");
|
|
1113
|
-
return;
|
|
1114
|
-
}
|
|
1115
|
-
const body = await readBody(req);
|
|
1116
|
-
if (body === void 0) {
|
|
1117
|
-
res.writeHead(413);
|
|
1118
|
-
res.end("body too large");
|
|
1119
|
-
return;
|
|
1120
|
-
}
|
|
1121
|
-
let envelope;
|
|
1122
|
-
try {
|
|
1123
|
-
envelope = JSON.parse(body);
|
|
1124
|
-
} catch {
|
|
1125
|
-
res.writeHead(400);
|
|
1126
|
-
res.end("body is not JSON");
|
|
1127
|
-
return;
|
|
1128
|
-
}
|
|
1129
|
-
const parsed = clientRequestSchema.safeParse(envelope);
|
|
1130
|
-
const rpcId = typeof envelope.rpcId === "string" ? RpcId(envelope.rpcId) : INVALID_REQUEST_RPC_ID;
|
|
1131
|
-
if (!parsed.success) {
|
|
1132
|
-
writeError(res, rpcId, {
|
|
1133
|
-
code: "bad-request",
|
|
1134
|
-
message: "invalid client-request message",
|
|
1135
|
-
details: { issues: parsed.error.issues }
|
|
1136
|
-
});
|
|
1137
|
-
return;
|
|
1138
|
-
}
|
|
1139
|
-
if (parsed.data.method !== endpoint) {
|
|
1140
|
-
writeError(res, rpcId, {
|
|
1141
|
-
code: "bad-request",
|
|
1142
|
-
message: `method ${JSON.stringify(parsed.data.method)} does not match endpoint ${JSON.stringify(endpoint)}`,
|
|
1143
|
-
details: { issues: [] }
|
|
1151
|
+
/**
|
|
1152
|
+
* Dispatch one inbound `msg` frame: deliver a `send`/`reply` and answer the
|
|
1153
|
+
* correlated `msg-result` on the same socket. Errors are surfaced as the
|
|
1154
|
+
* result so a throwing handler cannot escape the socket's message callback.
|
|
1155
|
+
*/
|
|
1156
|
+
async handleMsgFrame(socket, reqId, message) {
|
|
1157
|
+
let result;
|
|
1158
|
+
if (message.kind === "reply") {
|
|
1159
|
+
result = await this.replyForSession({
|
|
1160
|
+
sessionId: message.sessionId,
|
|
1161
|
+
text: message.text,
|
|
1162
|
+
...message.delivery === void 0 ? {} : { delivery: message.delivery },
|
|
1163
|
+
...message.resume === void 0 ? {} : { resume: message.resume }
|
|
1144
1164
|
});
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
res.writeHead(404);
|
|
1153
|
-
res.end("not found");
|
|
1154
|
-
return;
|
|
1155
|
-
}
|
|
1156
|
-
if (error instanceof InvalidPayloadError) {
|
|
1157
|
-
writeError(res, rpcId, {
|
|
1158
|
-
code: "bad-request",
|
|
1159
|
-
message: error.message,
|
|
1160
|
-
details: { issues: [] }
|
|
1161
|
-
});
|
|
1162
|
-
return;
|
|
1163
|
-
}
|
|
1164
|
-
this.ctx.logger.warn(error instanceof Error ? error : new Error(String(error)));
|
|
1165
|
-
writeError(res, rpcId, {
|
|
1166
|
-
code: "internal",
|
|
1167
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1168
|
-
details: {}
|
|
1165
|
+
} else {
|
|
1166
|
+
result = await this.deliver({
|
|
1167
|
+
sessionId: message.sessionId,
|
|
1168
|
+
text: message.text,
|
|
1169
|
+
...message.sender === void 0 ? {} : { sender: message.sender },
|
|
1170
|
+
...message.delivery === void 0 ? {} : { delivery: message.delivery },
|
|
1171
|
+
...message.resume === void 0 ? {} : { resume: message.resume }
|
|
1169
1172
|
});
|
|
1170
1173
|
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
let parsed;
|
|
1188
|
-
try {
|
|
1189
|
-
parsed = Schema.resolve(payload, replyPayloadSchema, {})[0];
|
|
1190
|
-
} catch (error) {
|
|
1191
|
-
throw new InvalidPayloadError(`reply payload invalid: ${error instanceof Error ? error.message : String(error)}`);
|
|
1192
|
-
}
|
|
1193
|
-
return this.replyForSession(parsed);
|
|
1194
|
-
}
|
|
1195
|
-
if (endpoint === "list") {
|
|
1196
|
-
return this.listSessions();
|
|
1197
|
-
}
|
|
1198
|
-
if (endpoint === "event") {
|
|
1199
|
-
let eventPayload;
|
|
1200
|
-
try {
|
|
1201
|
-
eventPayload = Schema.resolve(payload, eventPayloadSchema, {})[0];
|
|
1202
|
-
} catch (error) {
|
|
1203
|
-
throw new InvalidPayloadError(`event payload invalid: ${error instanceof Error ? error.message : String(error)}`);
|
|
1204
|
-
}
|
|
1205
|
-
this.receiveEvent(eventPayload);
|
|
1206
|
-
return { accepted: true };
|
|
1174
|
+
this.sendFrame(socket, { type: "msg-result", reqId, result });
|
|
1175
|
+
}
|
|
1176
|
+
/** Dispatch one inbound `query` frame (ping/list/event) and answer on the socket. */
|
|
1177
|
+
async handleQueryFrame(socket, reqId, query) {
|
|
1178
|
+
let result;
|
|
1179
|
+
if (query.kind === "ping") {
|
|
1180
|
+
result = { pong: true, instance: this.instanceId };
|
|
1181
|
+
} else if (query.kind === "list") {
|
|
1182
|
+
result = this.listSessions();
|
|
1183
|
+
} else {
|
|
1184
|
+
const payload = {
|
|
1185
|
+
sender: this.peerOf.get(socket) ?? "unknown-peer",
|
|
1186
|
+
notification: query.notification
|
|
1187
|
+
};
|
|
1188
|
+
this.receiveEvent(payload);
|
|
1189
|
+
result = { accepted: true };
|
|
1207
1190
|
}
|
|
1208
|
-
|
|
1191
|
+
this.sendFrame(socket, { type: "query-result", reqId, result });
|
|
1209
1192
|
}
|
|
1210
1193
|
/** Surface one remote notification to local listeners and the log. */
|
|
1211
1194
|
receiveEvent(eventPayload) {
|
|
@@ -1286,7 +1269,7 @@ var InterconnectService = class extends Service {
|
|
|
1286
1269
|
}
|
|
1287
1270
|
const sender = (() => {
|
|
1288
1271
|
const candidate = payload.sender;
|
|
1289
|
-
return candidate === void 0 || typeof candidate.
|
|
1272
|
+
return candidate === void 0 || typeof candidate.instanceId !== "string" ? void 0 : candidate;
|
|
1290
1273
|
})();
|
|
1291
1274
|
if (sender !== void 0) {
|
|
1292
1275
|
this.senders.set(payload.sessionId, sender);
|
|
@@ -1329,35 +1312,6 @@ var InterconnectService = class extends Service {
|
|
|
1329
1312
|
...payload.resume === void 0 ? {} : { resume: payload.resume }
|
|
1330
1313
|
});
|
|
1331
1314
|
}
|
|
1332
|
-
async post(baseUrl, endpoint, payload) {
|
|
1333
|
-
const token = await this.resolveToken();
|
|
1334
|
-
if (token === void 0) return void 0;
|
|
1335
|
-
const url = `${trimBase(baseUrl)}${INTERCONNECT_CHANNEL}/${endpoint}`;
|
|
1336
|
-
const envelope = {
|
|
1337
|
-
type: "client-request",
|
|
1338
|
-
rpcId: crypto.randomUUID(),
|
|
1339
|
-
method: endpoint,
|
|
1340
|
-
payload
|
|
1341
|
-
};
|
|
1342
|
-
try {
|
|
1343
|
-
const response = await fetch(url, {
|
|
1344
|
-
method: "POST",
|
|
1345
|
-
headers: {
|
|
1346
|
-
"content-type": "application/json",
|
|
1347
|
-
authorization: `Bearer ${token}`
|
|
1348
|
-
},
|
|
1349
|
-
body: JSON.stringify(envelope),
|
|
1350
|
-
signal: AbortSignal.timeout(this.requestTimeoutMs)
|
|
1351
|
-
});
|
|
1352
|
-
if (!response.ok) return void 0;
|
|
1353
|
-
const body = await response.json();
|
|
1354
|
-
if (body.result?.ok !== true) return void 0;
|
|
1355
|
-
return body.result.value;
|
|
1356
|
-
} catch (error) {
|
|
1357
|
-
this.ctx.logger.warn(`interconnect: ${endpoint} to ${baseUrl} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1358
|
-
return void 0;
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
1315
|
async resolveToken() {
|
|
1362
1316
|
const credential = await this.ctx.credentials.resolve(credentialRef(INTERCONNECT_TOKEN_REF));
|
|
1363
1317
|
return credential === void 0 || credential.value.length === 0 ? void 0 : credential.value;
|
|
@@ -1429,36 +1383,82 @@ var InterconnectService = class extends Service {
|
|
|
1429
1383
|
this.peerOf.set(socket, frame.sender);
|
|
1430
1384
|
return;
|
|
1431
1385
|
}
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1386
|
+
if (frame.type === "event") {
|
|
1387
|
+
const sender = this.peerOf.get(socket) ?? "unknown-peer";
|
|
1388
|
+
try {
|
|
1389
|
+
this.receiveEvent({ sender, notification: frame.notification });
|
|
1390
|
+
} catch (error) {
|
|
1391
|
+
this.ctx.logger.warn(`interconnect: listener for a ${frame.notification.kind} event from ${sender} threw: ${error instanceof Error ? error.message : String(error)}`);
|
|
1392
|
+
}
|
|
1393
|
+
return;
|
|
1437
1394
|
}
|
|
1395
|
+
if (frame.type === "msg-result") {
|
|
1396
|
+
const pending = this.pendingMessages.get(frame.reqId);
|
|
1397
|
+
if (pending !== void 0) {
|
|
1398
|
+
;
|
|
1399
|
+
pending.resolve?.(frame.result);
|
|
1400
|
+
}
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
if (frame.type === "query-result") {
|
|
1404
|
+
const pending = this.pendingMessages.get(frame.reqId);
|
|
1405
|
+
if (pending !== void 0) {
|
|
1406
|
+
;
|
|
1407
|
+
pending.resolve?.(frame.result);
|
|
1408
|
+
}
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
if (frame.type === "msg") {
|
|
1412
|
+
void Promise.resolve().then(async () => {
|
|
1413
|
+
await this.handleMsgFrame(socket, frame.reqId, frame.message);
|
|
1414
|
+
}).catch((error) => {
|
|
1415
|
+
this.ctx.logger.warn(`interconnect: msg ${frame.reqId} handler threw: ${error instanceof Error ? error.message : String(error)}`);
|
|
1416
|
+
void this.sendFrame(socket, {
|
|
1417
|
+
type: "msg-result",
|
|
1418
|
+
reqId: frame.reqId,
|
|
1419
|
+
result: { delivered: false, instance: this.instanceId, reason: "unreachable" }
|
|
1420
|
+
});
|
|
1421
|
+
});
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
void Promise.resolve().then(async () => {
|
|
1425
|
+
await this.handleQueryFrame(socket, frame.reqId, frame.query);
|
|
1426
|
+
}).catch((error) => {
|
|
1427
|
+
this.ctx.logger.warn(`interconnect: query ${frame.reqId} handler threw: ${error instanceof Error ? error.message : String(error)}`);
|
|
1428
|
+
});
|
|
1438
1429
|
}
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
}
|
|
1445
|
-
};
|
|
1446
|
-
var InvalidPayloadError = class extends Error {
|
|
1447
|
-
constructor(message) {
|
|
1448
|
-
super(message);
|
|
1449
|
-
this.name = "InvalidPayloadError";
|
|
1430
|
+
/** Write one frame to a socket, returning false when the socket cannot take it. */
|
|
1431
|
+
sendFrame(socket, frame) {
|
|
1432
|
+
if (socket.readyState !== WebSocket.OPEN) return false;
|
|
1433
|
+
socket.send(JSON.stringify(frame));
|
|
1434
|
+
return true;
|
|
1450
1435
|
}
|
|
1451
1436
|
};
|
|
1452
1437
|
var LinkState = class {
|
|
1453
|
-
constructor(owner,
|
|
1438
|
+
constructor(owner, instanceId, origin) {
|
|
1454
1439
|
this.owner = owner;
|
|
1455
|
-
this.
|
|
1440
|
+
this.instanceId = instanceId;
|
|
1441
|
+
this.peer = origin;
|
|
1456
1442
|
}
|
|
1457
1443
|
socket;
|
|
1458
1444
|
closed = false;
|
|
1459
1445
|
retry = 0;
|
|
1460
1446
|
reconnectTimer;
|
|
1447
|
+
// `peer` is mutable so `reroute` can point the link at a new origin.
|
|
1461
1448
|
peer;
|
|
1449
|
+
instanceId;
|
|
1450
|
+
/** Point this link at a different origin; re-dials immediately. */
|
|
1451
|
+
reroute(origin) {
|
|
1452
|
+
if (origin === this.peer) return;
|
|
1453
|
+
this.peer = origin;
|
|
1454
|
+
const socket = this.socket;
|
|
1455
|
+
if (socket !== void 0) {
|
|
1456
|
+
socket.removeAllListeners();
|
|
1457
|
+
socket.terminate();
|
|
1458
|
+
this.socket = void 0;
|
|
1459
|
+
}
|
|
1460
|
+
this.dial();
|
|
1461
|
+
}
|
|
1462
1462
|
/** Open the socket; reconnect is scheduled by the close handler. */
|
|
1463
1463
|
dial() {
|
|
1464
1464
|
if (this.closed) return;
|
|
@@ -1485,6 +1485,17 @@ var LinkState = class {
|
|
|
1485
1485
|
this.scheduleReconnect();
|
|
1486
1486
|
});
|
|
1487
1487
|
}
|
|
1488
|
+
/** Whether this link currently holds an open socket that can carry frames. */
|
|
1489
|
+
writable() {
|
|
1490
|
+
return this.socket !== void 0 && this.socket.readyState === WebSocket.OPEN;
|
|
1491
|
+
}
|
|
1492
|
+
/** Write one frame over this link's outbound socket; false when not open. */
|
|
1493
|
+
sendFrame(frame) {
|
|
1494
|
+
const socket = this.socket;
|
|
1495
|
+
if (socket === void 0 || socket.readyState !== WebSocket.OPEN) return false;
|
|
1496
|
+
socket.send(JSON.stringify(frame));
|
|
1497
|
+
return true;
|
|
1498
|
+
}
|
|
1488
1499
|
close() {
|
|
1489
1500
|
this.closed = true;
|
|
1490
1501
|
this.owner.forgetLink(this.peer);
|
|
@@ -1504,21 +1515,6 @@ var LinkState = class {
|
|
|
1504
1515
|
}, delay);
|
|
1505
1516
|
}
|
|
1506
1517
|
};
|
|
1507
|
-
function endpointFromPath(channel, pathname) {
|
|
1508
|
-
if (!pathname.startsWith(`${channel}/`)) return void 0;
|
|
1509
|
-
return pathname.slice(channel.length + 1);
|
|
1510
|
-
}
|
|
1511
|
-
async function readBody(req) {
|
|
1512
|
-
let size = 0;
|
|
1513
|
-
const chunks = [];
|
|
1514
|
-
for await (const chunk of req) {
|
|
1515
|
-
const buffer = chunk;
|
|
1516
|
-
size += buffer.byteLength;
|
|
1517
|
-
if (size > MAX_REQUEST_BODY_BYTES) return void 0;
|
|
1518
|
-
chunks.push(buffer);
|
|
1519
|
-
}
|
|
1520
|
-
return Buffer.concat(chunks).toString();
|
|
1521
|
-
}
|
|
1522
1518
|
function timingSafeEqual(a, b) {
|
|
1523
1519
|
if (a.length !== b.length) return false;
|
|
1524
1520
|
let diff = 0;
|
|
@@ -1530,21 +1526,8 @@ function timingSafeEqual(a, b) {
|
|
|
1530
1526
|
function trimBase(baseUrl) {
|
|
1531
1527
|
return baseUrl.replace(/\/+$/, "");
|
|
1532
1528
|
}
|
|
1533
|
-
function writeFull(res, rpcId, result) {
|
|
1534
|
-
const body = serverResponseSchema.parse({
|
|
1535
|
-
type: "server-response",
|
|
1536
|
-
rpcId,
|
|
1537
|
-
result
|
|
1538
|
-
});
|
|
1539
|
-
res.writeHead(200, { "content-type": "application/json" });
|
|
1540
|
-
res.end(JSON.stringify(body));
|
|
1541
|
-
}
|
|
1542
|
-
function writeError(res, rpcId, error) {
|
|
1543
|
-
writeFull(res, rpcId, { ok: false, error });
|
|
1544
|
-
}
|
|
1545
1529
|
var index_default = InterconnectService;
|
|
1546
1530
|
export {
|
|
1547
|
-
INTERCONNECT_CHANNEL,
|
|
1548
1531
|
INTERCONNECT_TOKEN_REF,
|
|
1549
1532
|
InterconnectService,
|
|
1550
1533
|
index_default as default
|