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

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,40 @@ 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
+ }
736
749
  var SessionHostClient = class {
737
750
  endpoint;
738
751
  socket = null;
752
+ connectPromise = null;
753
+ requestTimeoutMs = 3e4;
739
754
  requestWaiters = /* @__PURE__ */ new Map();
740
755
  eventListeners = /* @__PURE__ */ new Set();
741
756
  disconnectListeners = /* @__PURE__ */ new Set();
742
757
  /** Guards against emitting both an 'error'-path and a 'close'-path disconnect for one socket. */
743
758
  disconnectedSockets = /* @__PURE__ */ new WeakSet();
759
+ /** Session subscriptions established via create_session/attach_session, replayed automatically on reconnect. */
760
+ attachedSessions = /* @__PURE__ */ new Map();
744
761
  constructor(options = {}) {
745
762
  this.endpoint = options.endpoint || getDefaultSessionHostEndpoint(options.appName || "adhdev");
763
+ if (typeof options.requestTimeoutMs === "number" && options.requestTimeoutMs > 0) {
764
+ this.requestTimeoutMs = options.requestTimeoutMs;
765
+ }
766
+ activeClientRefs.add(new WeakRef(this));
767
+ }
768
+ static disconnectClients(endpoint, error) {
769
+ disconnectSessionHostClients(endpoint, error);
746
770
  }
747
771
  /**
748
772
  * Fires when an established connection to the session host goes away.
@@ -785,6 +809,7 @@ var SessionHostClient = class {
785
809
  this.requestWaiters.clear();
786
810
  if (this.socket === socket) {
787
811
  this.socket = null;
812
+ this.connectPromise = null;
788
813
  }
789
814
  try {
790
815
  socket.destroy();
@@ -804,8 +829,48 @@ var SessionHostClient = class {
804
829
  }
805
830
  }
806
831
  }
832
+ sendRawRequestOnSocket(socket, request) {
833
+ const requestId = generateUUID();
834
+ const envelope = {
835
+ kind: "request",
836
+ requestId,
837
+ request
838
+ };
839
+ const timeoutMs = this.requestTimeoutMs > 0 ? this.requestTimeoutMs : 3e4;
840
+ const timeoutLabel = timeoutMs % 1e3 === 0 ? `${timeoutMs / 1e3}s` : `${timeoutMs}ms`;
841
+ return new Promise((resolve3, reject) => {
842
+ const timeout = setTimeout(() => {
843
+ const timeoutError = new Error(`Session host request timed out after ${timeoutLabel} (${request.type})`);
844
+ if (this.socket === socket) {
845
+ this.disconnect(timeoutError);
846
+ } else {
847
+ this.requestWaiters.delete(requestId);
848
+ this.disconnectedSockets.add(socket);
849
+ try {
850
+ socket.destroy();
851
+ } catch {
852
+ }
853
+ reject(timeoutError);
854
+ }
855
+ }, timeoutMs);
856
+ this.requestWaiters.set(requestId, {
857
+ resolve: (value) => {
858
+ clearTimeout(timeout);
859
+ resolve3(value);
860
+ },
861
+ reject: (error) => {
862
+ clearTimeout(timeout);
863
+ reject(error);
864
+ }
865
+ });
866
+ socket.write(serializeEnvelope(envelope));
867
+ });
868
+ }
807
869
  async connect() {
808
- if (this.socket && !this.socket.destroyed) return;
870
+ if (this.socket && !this.socket.destroyed) {
871
+ if (this.connectPromise) await this.connectPromise;
872
+ return;
873
+ }
809
874
  if (this.socket) {
810
875
  try {
811
876
  this.socket.destroy();
@@ -813,6 +878,7 @@ var SessionHostClient = class {
813
878
  }
814
879
  this.socket = null;
815
880
  }
881
+ this.connectPromise = null;
816
882
  const socket = net.createConnection(this.endpoint.path);
817
883
  this.socket = socket;
818
884
  const parser = createLineParser((envelope) => {
@@ -846,13 +912,35 @@ var SessionHostClient = class {
846
912
  socket.on("end", () => {
847
913
  this.handleDisconnect(socket, "ended", void 0, flushParser().length);
848
914
  });
849
- await new Promise((resolve3, reject) => {
850
- socket.once("connect", () => {
851
- establishedSockets.add(socket);
852
- resolve3();
915
+ const connectTask = (async () => {
916
+ await new Promise((resolve3, reject) => {
917
+ socket.once("connect", () => {
918
+ establishedSockets.add(socket);
919
+ resolve3();
920
+ });
921
+ socket.once("error", reject);
853
922
  });
854
- socket.once("error", reject);
855
- });
923
+ if (this.attachedSessions.size > 0 && this.socket === socket && !socket.destroyed) {
924
+ for (const [sessionId, sub] of [...this.attachedSessions.entries()]) {
925
+ if (this.socket !== socket || socket.destroyed) break;
926
+ const replayResponse = await this.sendRawRequestOnSocket(socket, {
927
+ type: "attach_session",
928
+ payload: sub
929
+ }).catch(() => null);
930
+ if (replayResponse && !replayResponse.success) {
931
+ this.attachedSessions.delete(sessionId);
932
+ }
933
+ }
934
+ }
935
+ })();
936
+ this.connectPromise = connectTask;
937
+ try {
938
+ await connectTask;
939
+ } finally {
940
+ if (this.connectPromise === connectTask) {
941
+ this.connectPromise = null;
942
+ }
943
+ }
856
944
  }
857
945
  onEvent(listener) {
858
946
  this.eventListeners.add(listener);
@@ -862,33 +950,50 @@ var SessionHostClient = class {
862
950
  }
863
951
  async request(request) {
864
952
  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);
953
+ const requestSocket = this.socket;
954
+ if (!requestSocket) throw new Error("Session host socket unavailable");
955
+ const response = await this.sendRawRequestOnSocket(requestSocket, request);
956
+ if (response.success) {
957
+ if (request.type === "attach_session") {
958
+ this.attachedSessions.set(request.payload.sessionId, {
959
+ sessionId: request.payload.sessionId,
960
+ clientId: request.payload.clientId,
961
+ clientType: request.payload.clientType,
962
+ readOnly: request.payload.readOnly === true
963
+ });
964
+ } else if (request.type === "create_session") {
965
+ const createdId = response.result?.sessionId || request.payload.sessionId;
966
+ const clientId = typeof request.payload.clientId === "string" ? request.payload.clientId.trim() : "";
967
+ if (createdId && clientId) {
968
+ this.attachedSessions.set(createdId, {
969
+ sessionId: createdId,
970
+ clientId,
971
+ clientType: request.payload.clientType || "daemon",
972
+ readOnly: false
973
+ });
885
974
  }
886
- });
887
- this.socket?.write(serializeEnvelope(envelope));
888
- });
975
+ } else if (request.type === "detach_session" || request.type === "delete_session") {
976
+ this.attachedSessions.delete(request.payload.sessionId);
977
+ }
978
+ }
889
979
  return response;
890
980
  }
981
+ disconnect(error) {
982
+ const socket = this.socket;
983
+ if (socket) {
984
+ this.handleDisconnect(socket, error ? "error" : "closed", error, 0);
985
+ return;
986
+ }
987
+ this.connectPromise = null;
988
+ const failure = error || new Error(`Session host client disconnected (${this.endpoint.path})`);
989
+ for (const waiter of this.requestWaiters.values()) {
990
+ waiter.reject(failure);
991
+ }
992
+ this.requestWaiters.clear();
993
+ }
891
994
  async close() {
995
+ this.attachedSessions.clear();
996
+ this.connectPromise = null;
892
997
  if (!this.socket) return;
893
998
  const socket = this.socket;
894
999
  this.socket = null;
@@ -1105,6 +1210,7 @@ export {
1105
1210
  createLineParser,
1106
1211
  createResponseEnvelope,
1107
1212
  createSessionHostControlPlane,
1213
+ disconnectSessionHostClients,
1108
1214
  ensureNodePtySpawnHelperPermissions,
1109
1215
  formatRuntimeOwner,
1110
1216
  getDefaultSessionHostEndpoint,