@nextclaw/remote 0.1.25 → 0.1.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.d.ts CHANGED
@@ -163,8 +163,14 @@ declare class RemoteConnector {
163
163
  platformClient: RemotePlatformClient;
164
164
  relayBridgeFactory?: (localOrigin: string) => RemoteRelayBridge;
165
165
  logger?: RemoteLogger;
166
+ createSocket?: (wsUrl: string) => WebSocket;
167
+ delayFn?: typeof delay;
168
+ random?: () => number;
166
169
  });
167
170
  private get logger();
171
+ private get delayFn();
172
+ private get random();
173
+ private createSocket;
168
174
  private connectOnce;
169
175
  private handleSocketMessage;
170
176
  private parseRelayFrame;
package/dist/index.js CHANGED
@@ -707,6 +707,74 @@ var RemoteAppAdapter = class {
707
707
  }
708
708
  };
709
709
 
710
+ // src/remote-connector-error.ts
711
+ var TERMINAL_REMOTE_ERROR_PATTERNS = [
712
+ /invalid or expired token/i,
713
+ /missing bearer token/i,
714
+ /token expired/i,
715
+ /token is invalid/i,
716
+ /run "nextclaw login"/i,
717
+ /unexpected server response:\s*400/i,
718
+ /unexpected server response:\s*401/i,
719
+ /unexpected server response:\s*403/i,
720
+ /unexpected server response:\s*404/i,
721
+ /invalid url/i,
722
+ /unsupported protocol/i
723
+ ];
724
+ function isTerminalRemoteConnectorError(error) {
725
+ const message = error instanceof Error ? error.message : String(error);
726
+ return TERMINAL_REMOTE_ERROR_PATTERNS.some((pattern) => pattern.test(message));
727
+ }
728
+
729
+ // src/remote-connector-retry.utils.ts
730
+ var BASE_RECONNECT_DELAY_MS = 3e3;
731
+ var MAX_RECONNECT_DELAY_MS = 6e4;
732
+ var RECONNECT_JITTER_RATIO = 0.2;
733
+ var MAX_CONSECUTIVE_RECONNECT_FAILURES = 6;
734
+ function resolveReconnectDelayMs(attempt, random) {
735
+ const exponentialDelayMs = Math.min(
736
+ BASE_RECONNECT_DELAY_MS * 2 ** Math.max(0, attempt - 1),
737
+ MAX_RECONNECT_DELAY_MS
738
+ );
739
+ const jitterRatio = (random() * 2 - 1) * RECONNECT_JITTER_RATIO;
740
+ return Math.max(
741
+ BASE_RECONNECT_DELAY_MS,
742
+ Math.round(exponentialDelayMs * (1 + jitterRatio))
743
+ );
744
+ }
745
+ function formatReconnectDelay(delayMs) {
746
+ const seconds = delayMs / 1e3;
747
+ return Number.isInteger(seconds) ? `${seconds}s` : `${seconds.toFixed(1)}s`;
748
+ }
749
+ function buildReconnectHaltedMessage(message) {
750
+ return `${message} Auto-reconnect stopped after ${MAX_CONSECUTIVE_RECONNECT_FAILURES} consecutive failures to avoid wasting remote requests. Use Remote Access repair or restart the service after checking platform/network availability.`;
751
+ }
752
+
753
+ // src/remote-connector-websocket-error.utils.ts
754
+ function readRemoteConnectorSocketErrorMessage(event) {
755
+ const typedEvent = event;
756
+ if (typeof typedEvent.message === "string") {
757
+ const directMessage = typedEvent.message.trim();
758
+ if (directMessage.length > 0) {
759
+ return directMessage;
760
+ }
761
+ }
762
+ const nestedError = typedEvent.error;
763
+ if (nestedError instanceof Error && nestedError.message.trim().length > 0) {
764
+ return nestedError.message.trim();
765
+ }
766
+ if (typeof nestedError === "string" && nestedError.trim().length > 0) {
767
+ return nestedError.trim();
768
+ }
769
+ if (typeof nestedError === "object" && nestedError && typeof nestedError.message === "string") {
770
+ const nestedMessage = nestedError.message.trim();
771
+ if (nestedMessage.length > 0) {
772
+ return nestedMessage;
773
+ }
774
+ }
775
+ return "Remote connector websocket failed.";
776
+ }
777
+
710
778
  // src/remote-connector.ts
711
779
  var RemoteConnector = class {
712
780
  constructor(deps) {
@@ -715,9 +783,18 @@ var RemoteConnector = class {
715
783
  get logger() {
716
784
  return this.deps.logger ?? console;
717
785
  }
786
+ get delayFn() {
787
+ return this.deps.delayFn ?? delay;
788
+ }
789
+ get random() {
790
+ return this.deps.random ?? Math.random;
791
+ }
792
+ createSocket(wsUrl) {
793
+ return this.deps.createSocket?.(wsUrl) ?? new WebSocket(wsUrl);
794
+ }
718
795
  async connectOnce(params) {
719
796
  return await new Promise((resolve, reject) => {
720
- const socket = new WebSocket(params.wsUrl);
797
+ const socket = this.createSocket(params.wsUrl);
721
798
  const appAdapter = new RemoteAppAdapter(params.localOrigin, socket);
722
799
  let settled = false;
723
800
  let aborted = false;
@@ -783,13 +860,13 @@ var RemoteConnector = class {
783
860
  appAdapter.stop();
784
861
  finishResolve(aborted ? "aborted" : "closed");
785
862
  });
786
- socket.addEventListener("error", () => {
863
+ socket.addEventListener("error", (event) => {
787
864
  appAdapter.stop();
788
865
  if (aborted) {
789
866
  finishResolve("aborted");
790
867
  return;
791
868
  }
792
- finishReject(new Error("Remote connector websocket failed."));
869
+ finishReject(new Error(readRemoteConnectorSocketErrorMessage(event)));
793
870
  });
794
871
  });
795
872
  }
@@ -869,6 +946,7 @@ var RemoteConnector = class {
869
946
  statusStore?.write(next);
870
947
  }
871
948
  async runCycle(params) {
949
+ let device = params.device;
872
950
  try {
873
951
  this.writeRemoteState(params.opts.statusStore, {
874
952
  enabled: true,
@@ -879,7 +957,7 @@ var RemoteConnector = class {
879
957
  localOrigin: params.context.localOrigin,
880
958
  lastError: null
881
959
  });
882
- const device = await this.ensureDevice({ device: params.device, context: params.context });
960
+ device = await this.ensureDevice({ device, context: params.context });
883
961
  const wsUrl = `${params.context.platformBase.replace(/^http/i, "ws")}/platform/remote/connect?instanceId=${encodeURIComponent(device.id)}&token=${encodeURIComponent(params.context.token)}`;
884
962
  const outcome = await this.connectOnce({
885
963
  wsUrl,
@@ -902,7 +980,12 @@ var RemoteConnector = class {
902
980
  lastError: null
903
981
  });
904
982
  }
905
- return { device, aborted: outcome === "aborted" };
983
+ return {
984
+ device,
985
+ outcome: outcome === "aborted" ? "aborted" : "retry",
986
+ retryFailure: false,
987
+ lastError: null
988
+ };
906
989
  } catch (error) {
907
990
  const message = error instanceof Error ? error.message : String(error);
908
991
  this.writeRemoteState(params.opts.statusStore, {
@@ -915,7 +998,12 @@ var RemoteConnector = class {
915
998
  lastError: message
916
999
  });
917
1000
  this.logger.error(`Remote connector error: ${message}`);
918
- return { device: params.device, aborted: false };
1001
+ return {
1002
+ device,
1003
+ outcome: isTerminalRemoteConnectorError(error) ? "stop" : "retry",
1004
+ retryFailure: true,
1005
+ lastError: message
1006
+ };
919
1007
  }
920
1008
  }
921
1009
  async run(opts = {}) {
@@ -925,19 +1013,49 @@ var RemoteConnector = class {
925
1013
  );
926
1014
  await relayBridge.ensureLocalUiHealthy();
927
1015
  let device = null;
1016
+ let preserveRuntimeError = false;
1017
+ let consecutiveReconnectFailures = 0;
928
1018
  while (!opts.signal?.aborted) {
929
1019
  const cycle = await this.runCycle({ device, context, relayBridge, opts });
930
1020
  device = cycle.device;
931
- if (cycle.aborted || !context.autoReconnect || opts.signal?.aborted) {
1021
+ consecutiveReconnectFailures = cycle.retryFailure ? consecutiveReconnectFailures + 1 : 0;
1022
+ if (cycle.outcome === "stop") {
1023
+ preserveRuntimeError = true;
932
1024
  break;
933
1025
  }
934
- this.logger.warn("Remote connector disconnected. Reconnecting in 3s...");
1026
+ if (cycle.outcome === "aborted" || !context.autoReconnect || opts.signal?.aborted) {
1027
+ break;
1028
+ }
1029
+ if (cycle.retryFailure && consecutiveReconnectFailures >= MAX_CONSECUTIVE_RECONNECT_FAILURES) {
1030
+ const haltedMessage = buildReconnectHaltedMessage(
1031
+ cycle.lastError ?? "Remote connector websocket failed."
1032
+ );
1033
+ this.writeRemoteState(opts.statusStore, {
1034
+ enabled: true,
1035
+ state: "error",
1036
+ deviceId: device?.id,
1037
+ deviceName: context.displayName,
1038
+ platformBase: context.platformBase,
1039
+ localOrigin: context.localOrigin,
1040
+ lastError: haltedMessage
1041
+ });
1042
+ this.logger.error(`Remote connector error: ${haltedMessage}`);
1043
+ preserveRuntimeError = true;
1044
+ break;
1045
+ }
1046
+ const reconnectDelayMs = resolveReconnectDelayMs(cycle.retryFailure ? consecutiveReconnectFailures : 1, this.random);
1047
+ this.logger.warn(
1048
+ `Remote connector disconnected. Reconnecting in ${formatReconnectDelay(reconnectDelayMs)}...`
1049
+ );
935
1050
  try {
936
- await delay(3e3, opts.signal);
1051
+ await this.delayFn(reconnectDelayMs, opts.signal);
937
1052
  } catch {
938
1053
  break;
939
1054
  }
940
1055
  }
1056
+ if (preserveRuntimeError) {
1057
+ return;
1058
+ }
941
1059
  this.writeRemoteState(opts.statusStore, {
942
1060
  enabled: opts.mode === "service" ? true : Boolean(context.config.remote.enabled),
943
1061
  state: opts.signal?.aborted ? "disconnected" : "disabled",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/remote",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "private": false,
5
5
  "description": "Remote access runtime for NextClaw device registration, relay bridging, and service-managed connectivity.",
6
6
  "type": "module",
@@ -30,8 +30,8 @@
30
30
  "dependencies": {
31
31
  "commander": "^12.1.0",
32
32
  "ws": "^8.18.0",
33
- "@nextclaw/core": "0.9.11",
34
- "@nextclaw/server": "0.10.31"
33
+ "@nextclaw/server": "0.10.33",
34
+ "@nextclaw/core": "0.9.11"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^20.17.6",