@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 +6 -0
- package/dist/index.js +127 -9
- package/package.json +3 -3
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 =
|
|
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(
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
1021
|
+
consecutiveReconnectFailures = cycle.retryFailure ? consecutiveReconnectFailures + 1 : 0;
|
|
1022
|
+
if (cycle.outcome === "stop") {
|
|
1023
|
+
preserveRuntimeError = true;
|
|
932
1024
|
break;
|
|
933
1025
|
}
|
|
934
|
-
|
|
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
|
|
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.
|
|
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/
|
|
34
|
-
"@nextclaw/
|
|
33
|
+
"@nextclaw/server": "0.10.33",
|
|
34
|
+
"@nextclaw/core": "0.9.11"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^20.17.6",
|