@nextclaw/remote 0.1.26 → 0.1.28

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
@@ -713,13 +713,68 @@ var TERMINAL_REMOTE_ERROR_PATTERNS = [
713
713
  /missing bearer token/i,
714
714
  /token expired/i,
715
715
  /token is invalid/i,
716
- /run "nextclaw login"/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
717
723
  ];
718
724
  function isTerminalRemoteConnectorError(error) {
719
725
  const message = error instanceof Error ? error.message : String(error);
720
726
  return TERMINAL_REMOTE_ERROR_PATTERNS.some((pattern) => pattern.test(message));
721
727
  }
722
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
+
723
778
  // src/remote-connector.ts
724
779
  var RemoteConnector = class {
725
780
  constructor(deps) {
@@ -728,9 +783,18 @@ var RemoteConnector = class {
728
783
  get logger() {
729
784
  return this.deps.logger ?? console;
730
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
+ }
731
795
  async connectOnce(params) {
732
796
  return await new Promise((resolve, reject) => {
733
- const socket = new WebSocket(params.wsUrl);
797
+ const socket = this.createSocket(params.wsUrl);
734
798
  const appAdapter = new RemoteAppAdapter(params.localOrigin, socket);
735
799
  let settled = false;
736
800
  let aborted = false;
@@ -796,13 +860,13 @@ var RemoteConnector = class {
796
860
  appAdapter.stop();
797
861
  finishResolve(aborted ? "aborted" : "closed");
798
862
  });
799
- socket.addEventListener("error", () => {
863
+ socket.addEventListener("error", (event) => {
800
864
  appAdapter.stop();
801
865
  if (aborted) {
802
866
  finishResolve("aborted");
803
867
  return;
804
868
  }
805
- finishReject(new Error("Remote connector websocket failed."));
869
+ finishReject(new Error(readRemoteConnectorSocketErrorMessage(event)));
806
870
  });
807
871
  });
808
872
  }
@@ -882,6 +946,7 @@ var RemoteConnector = class {
882
946
  statusStore?.write(next);
883
947
  }
884
948
  async runCycle(params) {
949
+ let device = params.device;
885
950
  try {
886
951
  this.writeRemoteState(params.opts.statusStore, {
887
952
  enabled: true,
@@ -892,7 +957,7 @@ var RemoteConnector = class {
892
957
  localOrigin: params.context.localOrigin,
893
958
  lastError: null
894
959
  });
895
- const device = await this.ensureDevice({ device: params.device, context: params.context });
960
+ device = await this.ensureDevice({ device, context: params.context });
896
961
  const wsUrl = `${params.context.platformBase.replace(/^http/i, "ws")}/platform/remote/connect?instanceId=${encodeURIComponent(device.id)}&token=${encodeURIComponent(params.context.token)}`;
897
962
  const outcome = await this.connectOnce({
898
963
  wsUrl,
@@ -915,7 +980,12 @@ var RemoteConnector = class {
915
980
  lastError: null
916
981
  });
917
982
  }
918
- return { device, outcome: outcome === "aborted" ? "aborted" : "retry" };
983
+ return {
984
+ device,
985
+ outcome: outcome === "aborted" ? "aborted" : "retry",
986
+ retryFailure: false,
987
+ lastError: null
988
+ };
919
989
  } catch (error) {
920
990
  const message = error instanceof Error ? error.message : String(error);
921
991
  this.writeRemoteState(params.opts.statusStore, {
@@ -929,8 +999,10 @@ var RemoteConnector = class {
929
999
  });
930
1000
  this.logger.error(`Remote connector error: ${message}`);
931
1001
  return {
932
- device: params.device,
933
- outcome: isTerminalRemoteConnectorError(error) ? "stop" : "retry"
1002
+ device,
1003
+ outcome: isTerminalRemoteConnectorError(error) ? "stop" : "retry",
1004
+ retryFailure: true,
1005
+ lastError: message
934
1006
  };
935
1007
  }
936
1008
  }
@@ -942,9 +1014,11 @@ var RemoteConnector = class {
942
1014
  await relayBridge.ensureLocalUiHealthy();
943
1015
  let device = null;
944
1016
  let preserveRuntimeError = false;
1017
+ let consecutiveReconnectFailures = 0;
945
1018
  while (!opts.signal?.aborted) {
946
1019
  const cycle = await this.runCycle({ device, context, relayBridge, opts });
947
1020
  device = cycle.device;
1021
+ consecutiveReconnectFailures = cycle.retryFailure ? consecutiveReconnectFailures + 1 : 0;
948
1022
  if (cycle.outcome === "stop") {
949
1023
  preserveRuntimeError = true;
950
1024
  break;
@@ -952,9 +1026,29 @@ var RemoteConnector = class {
952
1026
  if (cycle.outcome === "aborted" || !context.autoReconnect || opts.signal?.aborted) {
953
1027
  break;
954
1028
  }
955
- this.logger.warn("Remote connector disconnected. Reconnecting in 3s...");
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
+ );
956
1050
  try {
957
- await delay(3e3, opts.signal);
1051
+ await this.delayFn(reconnectDelayMs, opts.signal);
958
1052
  } catch {
959
1053
  break;
960
1054
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/remote",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "private": false,
5
5
  "description": "Remote access runtime for NextClaw device registration, relay bridging, and service-managed connectivity.",
6
6
  "type": "module",
@@ -27,11 +27,17 @@
27
27
  "files": [
28
28
  "dist"
29
29
  ],
30
+ "scripts": {
31
+ "build": "tsup src/index.ts --format esm --dts --out-dir dist",
32
+ "prepack": "pnpm run build",
33
+ "lint": "eslint .",
34
+ "tsc": "tsc -p tsconfig.json"
35
+ },
30
36
  "dependencies": {
37
+ "@nextclaw/core": "workspace:*",
38
+ "@nextclaw/server": "workspace:*",
31
39
  "commander": "^12.1.0",
32
- "ws": "^8.18.0",
33
- "@nextclaw/server": "0.10.32",
34
- "@nextclaw/core": "0.9.11"
40
+ "ws": "^8.18.0"
35
41
  },
36
42
  "devDependencies": {
37
43
  "@types/node": "^20.17.6",
@@ -39,10 +45,5 @@
39
45
  "prettier": "^3.3.3",
40
46
  "tsup": "^8.3.5",
41
47
  "typescript": "^5.6.3"
42
- },
43
- "scripts": {
44
- "build": "tsup src/index.ts --format esm --dts --out-dir dist",
45
- "lint": "eslint .",
46
- "tsc": "tsc -p tsconfig.json"
47
48
  }
48
- }
49
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 NextClaw contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.