@adhdev/session-host-core 1.0.82-rc.25 → 1.0.82-rc.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -733,16 +733,59 @@ function createLineParser(onEnvelope) {
733
733
  return parser;
734
734
  }
735
735
  var establishedSockets = /* @__PURE__ */ new WeakSet();
736
+ var activeClientRefs = /* @__PURE__ */ new Set();
737
+ function disconnectSessionHostClients(endpoint, error) {
738
+ for (const ref of activeClientRefs) {
739
+ const client = ref.deref();
740
+ if (!client) {
741
+ activeClientRefs.delete(ref);
742
+ continue;
743
+ }
744
+ if (!endpoint || client.endpoint.path === endpoint.path) {
745
+ client.disconnect(error);
746
+ }
747
+ }
748
+ }
749
+ function findConnectedSessionHostClient(endpoint) {
750
+ for (const ref of activeClientRefs) {
751
+ const client = ref.deref();
752
+ if (!client) {
753
+ activeClientRefs.delete(ref);
754
+ continue;
755
+ }
756
+ if ((!endpoint || client.endpoint.path === endpoint.path) && client.isConnected()) {
757
+ return client;
758
+ }
759
+ }
760
+ return null;
761
+ }
736
762
  var SessionHostClient = class {
737
763
  endpoint;
738
764
  socket = null;
765
+ connectPromise = null;
766
+ requestTimeoutMs = 3e4;
739
767
  requestWaiters = /* @__PURE__ */ new Map();
740
768
  eventListeners = /* @__PURE__ */ new Set();
741
769
  disconnectListeners = /* @__PURE__ */ new Set();
742
770
  /** Guards against emitting both an 'error'-path and a 'close'-path disconnect for one socket. */
743
771
  disconnectedSockets = /* @__PURE__ */ new WeakSet();
772
+ /** Session subscriptions established via create_session/attach_session, replayed automatically on reconnect. */
773
+ attachedSessions = /* @__PURE__ */ new Map();
744
774
  constructor(options = {}) {
745
775
  this.endpoint = options.endpoint || getDefaultSessionHostEndpoint(options.appName || "adhdev");
776
+ if (typeof options.requestTimeoutMs === "number" && options.requestTimeoutMs > 0) {
777
+ this.requestTimeoutMs = options.requestTimeoutMs;
778
+ }
779
+ if (options.registerAsShared !== false) activeClientRefs.add(new WeakRef(this));
780
+ }
781
+ static disconnectClients(endpoint, error) {
782
+ disconnectSessionHostClients(endpoint, error);
783
+ }
784
+ static findConnectedClient(endpoint) {
785
+ return findConnectedSessionHostClient(endpoint);
786
+ }
787
+ isConnected() {
788
+ return Boolean(this.socket && !this.socket.destroyed && establishedSockets.has(this.socket));
746
789
  }
747
790
  /**
748
791
  * Fires when an established connection to the session host goes away.
@@ -785,6 +828,7 @@ var SessionHostClient = class {
785
828
  this.requestWaiters.clear();
786
829
  if (this.socket === socket) {
787
830
  this.socket = null;
831
+ this.connectPromise = null;
788
832
  }
789
833
  try {
790
834
  socket.destroy();
@@ -804,8 +848,47 @@ var SessionHostClient = class {
804
848
  }
805
849
  }
806
850
  }
807
- async connect() {
808
- if (this.socket && !this.socket.destroyed) return;
851
+ sendRawRequestOnSocket(socket, request, timeoutMs = this.requestTimeoutMs) {
852
+ const requestId = generateUUID();
853
+ const envelope = {
854
+ kind: "request",
855
+ requestId,
856
+ request
857
+ };
858
+ const timeoutLabel = timeoutMs % 1e3 === 0 ? `${timeoutMs / 1e3}s` : `${timeoutMs}ms`;
859
+ return new Promise((resolve3, reject) => {
860
+ const timeout = setTimeout(() => {
861
+ const timeoutError = Object.assign(new Error(`Session host request timed out after ${timeoutLabel} (${request.type})`), { code: "SESSION_HOST_REQUEST_TIMEOUT", requestType: request.type });
862
+ if (this.socket === socket) {
863
+ this.disconnect(timeoutError);
864
+ } else {
865
+ this.requestWaiters.delete(requestId);
866
+ this.disconnectedSockets.add(socket);
867
+ try {
868
+ socket.destroy();
869
+ } catch {
870
+ }
871
+ reject(timeoutError);
872
+ }
873
+ }, timeoutMs);
874
+ this.requestWaiters.set(requestId, {
875
+ resolve: (value) => {
876
+ clearTimeout(timeout);
877
+ resolve3(value);
878
+ },
879
+ reject: (error) => {
880
+ clearTimeout(timeout);
881
+ reject(error);
882
+ }
883
+ });
884
+ socket.write(serializeEnvelope(envelope));
885
+ });
886
+ }
887
+ async connect(timeoutMs = this.requestTimeoutMs) {
888
+ if (this.socket && !this.socket.destroyed) {
889
+ if (this.connectPromise) await this.connectPromise;
890
+ return;
891
+ }
809
892
  if (this.socket) {
810
893
  try {
811
894
  this.socket.destroy();
@@ -813,6 +896,7 @@ var SessionHostClient = class {
813
896
  }
814
897
  this.socket = null;
815
898
  }
899
+ this.connectPromise = null;
816
900
  const socket = net.createConnection(this.endpoint.path);
817
901
  this.socket = socket;
818
902
  const parser = createLineParser((envelope) => {
@@ -846,13 +930,54 @@ var SessionHostClient = class {
846
930
  socket.on("end", () => {
847
931
  this.handleDisconnect(socket, "ended", void 0, flushParser().length);
848
932
  });
849
- await new Promise((resolve3, reject) => {
850
- socket.once("connect", () => {
851
- establishedSockets.add(socket);
852
- resolve3();
933
+ const connectTask = (async () => {
934
+ await new Promise((resolve3, reject) => {
935
+ const cleanup = () => {
936
+ clearTimeout(timer);
937
+ socket.off("connect", connected);
938
+ socket.off("error", failed);
939
+ socket.off("close", closed);
940
+ };
941
+ const connected = () => {
942
+ cleanup();
943
+ establishedSockets.add(socket);
944
+ resolve3();
945
+ };
946
+ const failed = (error) => {
947
+ cleanup();
948
+ reject(error);
949
+ };
950
+ const closed = () => failed(new Error("Session host connection closed before connect"));
951
+ const timer = setTimeout(() => {
952
+ const error = Object.assign(new Error(`Session host connect timed out after ${timeoutMs}ms`), { code: "SESSION_HOST_CONNECT_TIMEOUT" });
953
+ failed(error);
954
+ if (this.socket === socket) this.disconnect(error);
955
+ }, timeoutMs);
956
+ socket.once("connect", connected);
957
+ socket.once("error", failed);
958
+ socket.once("close", closed);
853
959
  });
854
- socket.once("error", reject);
855
- });
960
+ if (this.attachedSessions.size > 0 && this.socket === socket && !socket.destroyed) {
961
+ for (const [sessionId, sub] of [...this.attachedSessions.entries()]) {
962
+ if (this.socket !== socket || socket.destroyed) break;
963
+ const replayResponse = await this.sendRawRequestOnSocket(socket, {
964
+ type: "attach_session",
965
+ payload: sub
966
+ }).catch(() => null);
967
+ if (replayResponse && !replayResponse.success) {
968
+ this.attachedSessions.delete(sessionId);
969
+ }
970
+ }
971
+ }
972
+ })();
973
+ this.connectPromise = connectTask;
974
+ try {
975
+ await connectTask;
976
+ } finally {
977
+ if (this.connectPromise === connectTask) {
978
+ this.connectPromise = null;
979
+ }
980
+ }
856
981
  }
857
982
  onEvent(listener) {
858
983
  this.eventListeners.add(listener);
@@ -860,35 +985,73 @@ var SessionHostClient = class {
860
985
  this.eventListeners.delete(listener);
861
986
  };
862
987
  }
863
- async request(request) {
864
- await this.connect();
865
- if (!this.socket) throw new Error("Session host socket unavailable");
866
- const requestId = generateUUID();
867
- const envelope = {
868
- kind: "request",
869
- requestId,
870
- request
871
- };
872
- const response = await new Promise((resolve3, reject) => {
873
- const timeout = setTimeout(() => {
874
- this.requestWaiters.delete(requestId);
875
- reject(new Error(`Session host request timed out after 30s (${request.type})`));
876
- }, 3e4);
877
- this.requestWaiters.set(requestId, {
878
- resolve: (value) => {
879
- clearTimeout(timeout);
880
- resolve3(value);
881
- },
882
- reject: (error) => {
883
- clearTimeout(timeout);
884
- reject(error);
988
+ async request(request, options = {}) {
989
+ const timeoutMs = Math.max(1, options.timeoutMs ?? this.requestTimeoutMs);
990
+ const deadline = Date.now() + timeoutMs;
991
+ const alreadyConnected = this.isConnected() && !this.connectPromise;
992
+ if (options.timeoutMs !== void 0) {
993
+ let timer;
994
+ try {
995
+ await Promise.race([
996
+ this.connect(timeoutMs),
997
+ new Promise((_resolve, reject) => {
998
+ timer = setTimeout(() => {
999
+ const error = Object.assign(new Error(`Session host connect timed out after ${timeoutMs}ms`), { code: "SESSION_HOST_CONNECT_TIMEOUT" });
1000
+ reject(error);
1001
+ this.disconnect(error);
1002
+ }, timeoutMs);
1003
+ })
1004
+ ]);
1005
+ } finally {
1006
+ if (timer) clearTimeout(timer);
1007
+ }
1008
+ } else {
1009
+ await this.connect(timeoutMs);
1010
+ }
1011
+ const requestSocket = this.socket;
1012
+ if (!requestSocket) throw new Error("Session host socket unavailable");
1013
+ const response = await this.sendRawRequestOnSocket(requestSocket, request, options.timeoutMs === void 0 || alreadyConnected ? timeoutMs : Math.max(1, deadline - Date.now()));
1014
+ if (response.success) {
1015
+ if (request.type === "attach_session") {
1016
+ this.attachedSessions.set(request.payload.sessionId, {
1017
+ sessionId: request.payload.sessionId,
1018
+ clientId: request.payload.clientId,
1019
+ clientType: request.payload.clientType,
1020
+ readOnly: request.payload.readOnly === true
1021
+ });
1022
+ } else if (request.type === "create_session") {
1023
+ const createdId = response.result?.sessionId || request.payload.sessionId;
1024
+ const clientId = typeof request.payload.clientId === "string" ? request.payload.clientId.trim() : "";
1025
+ if (createdId && clientId) {
1026
+ this.attachedSessions.set(createdId, {
1027
+ sessionId: createdId,
1028
+ clientId,
1029
+ clientType: request.payload.clientType || "daemon",
1030
+ readOnly: false
1031
+ });
885
1032
  }
886
- });
887
- this.socket?.write(serializeEnvelope(envelope));
888
- });
1033
+ } else if (request.type === "detach_session" || request.type === "delete_session") {
1034
+ this.attachedSessions.delete(request.payload.sessionId);
1035
+ }
1036
+ }
889
1037
  return response;
890
1038
  }
1039
+ disconnect(error) {
1040
+ const socket = this.socket;
1041
+ if (socket) {
1042
+ this.handleDisconnect(socket, error ? "error" : "closed", error, 0);
1043
+ return;
1044
+ }
1045
+ this.connectPromise = null;
1046
+ const failure = error || new Error(`Session host client disconnected (${this.endpoint.path})`);
1047
+ for (const waiter of this.requestWaiters.values()) {
1048
+ waiter.reject(failure);
1049
+ }
1050
+ this.requestWaiters.clear();
1051
+ }
891
1052
  async close() {
1053
+ this.attachedSessions.clear();
1054
+ this.connectPromise = null;
892
1055
  if (!this.socket) return;
893
1056
  const socket = this.socket;
894
1057
  this.socket = null;
@@ -1105,7 +1268,9 @@ export {
1105
1268
  createLineParser,
1106
1269
  createResponseEnvelope,
1107
1270
  createSessionHostControlPlane,
1271
+ disconnectSessionHostClients,
1108
1272
  ensureNodePtySpawnHelperPermissions,
1273
+ findConnectedSessionHostClient,
1109
1274
  formatRuntimeOwner,
1110
1275
  getDefaultSessionHostEndpoint,
1111
1276
  getSessionHostRecoveryLabel,