@limrun/api 0.29.0 → 0.31.0
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/CHANGELOG.md +35 -0
- package/build-settings.d.mts +4 -0
- package/build-settings.d.mts.map +1 -0
- package/build-settings.d.ts +4 -0
- package/build-settings.d.ts.map +1 -0
- package/build-settings.js +61 -0
- package/build-settings.js.map +1 -0
- package/build-settings.mjs +56 -0
- package/build-settings.mjs.map +1 -0
- package/exec-client.d.mts +1 -0
- package/exec-client.d.mts.map +1 -1
- package/exec-client.d.ts +1 -0
- package/exec-client.d.ts.map +1 -1
- package/exec-client.js.map +1 -1
- package/exec-client.mjs.map +1 -1
- package/index.d.mts +2 -1
- package/index.d.mts.map +1 -1
- package/index.d.ts +2 -1
- package/index.d.ts.map +1 -1
- package/index.js +9 -1
- package/index.js.map +1 -1
- package/index.mjs +2 -0
- package/index.mjs.map +1 -1
- package/package.json +11 -1
- package/resources/index.d.mts +1 -1
- package/resources/index.d.mts.map +1 -1
- package/resources/index.d.ts +1 -1
- package/resources/index.d.ts.map +1 -1
- package/resources/index.js +3 -1
- package/resources/index.js.map +1 -1
- package/resources/index.mjs +1 -1
- package/resources/index.mjs.map +1 -1
- package/resources/xcode-instances-helpers.d.mts +67 -0
- package/resources/xcode-instances-helpers.d.mts.map +1 -1
- package/resources/xcode-instances-helpers.d.ts +67 -0
- package/resources/xcode-instances-helpers.d.ts.map +1 -1
- package/resources/xcode-instances-helpers.js +95 -1
- package/resources/xcode-instances-helpers.js.map +1 -1
- package/resources/xcode-instances-helpers.mjs +92 -0
- package/resources/xcode-instances-helpers.mjs.map +1 -1
- package/src/build-settings.ts +61 -0
- package/src/exec-client.ts +1 -0
- package/src/index.ts +8 -0
- package/src/resources/index.ts +2 -0
- package/src/resources/xcode-instances-helpers.ts +162 -0
- package/src/tunnel.ts +62 -3
- package/src/version.ts +1 -1
- package/tunnel.d.mts.map +1 -1
- package/tunnel.d.ts.map +1 -1
- package/tunnel.js +59 -2
- package/tunnel.js.map +1 -1
- package/tunnel.mjs +59 -2
- package/tunnel.mjs.map +1 -1
- package/version.d.mts +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.mjs +1 -1
package/src/exec-client.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export { PagePromise } from './core/pagination';
|
|
|
9
9
|
export * from './instance-client';
|
|
10
10
|
export * as Ios from './ios-client';
|
|
11
11
|
export { startHttpProxy, type HttpProxy, type StartHttpProxyOptions } from './http-proxy';
|
|
12
|
+
export { buildSettingKeyPattern, parseBuildSettingEntries, validateBuildSettings } from './build-settings';
|
|
12
13
|
export {
|
|
13
14
|
exec,
|
|
14
15
|
type ExecRequest,
|
|
@@ -19,6 +20,13 @@ export {
|
|
|
19
20
|
export {
|
|
20
21
|
type XcodeCreateClientParams,
|
|
21
22
|
type XcodeClient,
|
|
23
|
+
type RbeStatus,
|
|
24
|
+
type RbeStartOptions,
|
|
25
|
+
type RbeTunnelOptions,
|
|
26
|
+
type Tunnel,
|
|
27
|
+
RbeUnsupportedError,
|
|
28
|
+
deriveRbeTunnelUrl,
|
|
29
|
+
DEFAULT_RBE_TUNNEL_PORT,
|
|
22
30
|
type XcodeProjectConfig,
|
|
23
31
|
type XcodeBuildOptions,
|
|
24
32
|
type ReactNativeBuildConfig,
|
package/src/resources/index.ts
CHANGED
|
@@ -32,6 +32,8 @@ export {
|
|
|
32
32
|
export { Assets, AssetGetOrUploadParams, AssetGetOrUploadResponse } from './assets-helpers';
|
|
33
33
|
export {
|
|
34
34
|
XcodeInstances,
|
|
35
|
+
RbeUnsupportedError,
|
|
36
|
+
DEFAULT_RBE_TUNNEL_PORT,
|
|
35
37
|
type XcodeCreateClientParams,
|
|
36
38
|
type XcodeClient,
|
|
37
39
|
type XcodeProjectConfig,
|
|
@@ -13,6 +13,11 @@ import {
|
|
|
13
13
|
import { createIgnoreFn } from '../folder-sync-ignore';
|
|
14
14
|
import { nodeProxyTransport } from '../internal/proxy-transport';
|
|
15
15
|
import { directInstanceHttpError } from '../internal/direct-instance-errors';
|
|
16
|
+
import { LimrunError } from '../core/error';
|
|
17
|
+
import { validateBuildSettings } from '../build-settings';
|
|
18
|
+
import { startTcpTunnel, type Tunnel } from '../tunnel';
|
|
19
|
+
|
|
20
|
+
export type { Tunnel } from '../tunnel';
|
|
16
21
|
|
|
17
22
|
export type LogLevel = 'none' | 'error' | 'warn' | 'info' | 'debug';
|
|
18
23
|
|
|
@@ -88,6 +93,7 @@ export type XcodeBuildOptions = {
|
|
|
88
93
|
upload?: { assetName: string } | { signedUploadUrl: string };
|
|
89
94
|
signing?: XcodeSigningConfig;
|
|
90
95
|
reactNative?: ReactNativeBuildConfig;
|
|
96
|
+
buildSettings?: Record<string, string>;
|
|
91
97
|
};
|
|
92
98
|
|
|
93
99
|
export type SimulatorInstallState =
|
|
@@ -136,6 +142,43 @@ export type SimulatorAttachResult = {
|
|
|
136
142
|
installError?: string;
|
|
137
143
|
};
|
|
138
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Status of the instance's embedded Bazel Remote Build Execution stack, as
|
|
147
|
+
* reported by limbuild's /rbe endpoints.
|
|
148
|
+
*/
|
|
149
|
+
export type RbeStatus = {
|
|
150
|
+
state: 'stopped' | 'starting' | 'running' | 'failed';
|
|
151
|
+
/** Loopback port of the RBE gRPC frontend inside the instance, when running. */
|
|
152
|
+
frontendPort?: number;
|
|
153
|
+
/**
|
|
154
|
+
* The Xcode version key remote actions must declare via
|
|
155
|
+
* XCODE_VERSION_OVERRIDE (e.g. 26.4.0.17E192), when running. Clients
|
|
156
|
+
* generate their xcode_version_config from it.
|
|
157
|
+
*/
|
|
158
|
+
xcodeVersion?: string;
|
|
159
|
+
error?: string;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/** Default local TCP port the RBE tunnel listens on. */
|
|
163
|
+
export const DEFAULT_RBE_TUNNEL_PORT = 8980;
|
|
164
|
+
|
|
165
|
+
export type RbeStartOptions = {
|
|
166
|
+
/** Max size of the content-addressable store (CAS), in bytes. */
|
|
167
|
+
casMaxBytes?: number;
|
|
168
|
+
/** Max size of the action cache (AC), in bytes. */
|
|
169
|
+
acMaxBytes?: number;
|
|
170
|
+
/** Number of concurrent build actions the embedded worker runs. */
|
|
171
|
+
workerConcurrency?: number;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export type RbeTunnelOptions = {
|
|
175
|
+
/** Local port to listen on. Defaults to DEFAULT_RBE_TUNNEL_PORT (8980). */
|
|
176
|
+
port?: number;
|
|
177
|
+
/** Local address to listen on. Defaults to 127.0.0.1. */
|
|
178
|
+
host?: string;
|
|
179
|
+
logLevel?: LogLevel;
|
|
180
|
+
};
|
|
181
|
+
|
|
139
182
|
export type XcodeClient = {
|
|
140
183
|
/**
|
|
141
184
|
* Sync source code to the xcode instance. In watch mode, keeps syncing on changes.
|
|
@@ -165,6 +208,25 @@ export type XcodeClient = {
|
|
|
165
208
|
* Return the currently attached simulator and latest installable build state.
|
|
166
209
|
*/
|
|
167
210
|
getSimulator: () => Promise<SimulatorStatus>;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Start the instance's embedded Bazel Remote Build Execution stack.
|
|
214
|
+
* Idempotent: returns the running status when already up.
|
|
215
|
+
*/
|
|
216
|
+
startRbe: (opts?: RbeStartOptions) => Promise<RbeStatus>;
|
|
217
|
+
|
|
218
|
+
/** Return the current RBE stack status. */
|
|
219
|
+
getRbe: () => Promise<RbeStatus>;
|
|
220
|
+
|
|
221
|
+
/** Stop the RBE stack. */
|
|
222
|
+
stopRbe: () => Promise<RbeStatus>;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Open a local TCP listener bridged to the instance's RBE gRPC frontend
|
|
226
|
+
* over a multiplexed websocket. Point bazel at it with
|
|
227
|
+
* --remote_executor=grpc://127.0.0.1:<port>.
|
|
228
|
+
*/
|
|
229
|
+
startRbeTunnel: (opts?: RbeTunnelOptions) => Promise<Tunnel>;
|
|
168
230
|
};
|
|
169
231
|
|
|
170
232
|
export type XcodeCreateClientParams =
|
|
@@ -219,6 +281,57 @@ async function fetchSandboxInfo(apiUrl: string, token: string): Promise<{ homeDi
|
|
|
219
281
|
};
|
|
220
282
|
}
|
|
221
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Derives the websocket URL of limbuild's /rbe/tunnel endpoint from the
|
|
286
|
+
* instance apiUrl, mirroring deriveReverseTunnelUrl in ios-client.ts.
|
|
287
|
+
*/
|
|
288
|
+
export function deriveRbeTunnelUrl(apiUrl: string): string {
|
|
289
|
+
const url = new URL(apiUrl);
|
|
290
|
+
if (url.protocol === 'https:') {
|
|
291
|
+
url.protocol = 'wss:';
|
|
292
|
+
} else if (url.protocol === 'http:') {
|
|
293
|
+
url.protocol = 'ws:';
|
|
294
|
+
} else {
|
|
295
|
+
throw new Error(`Unsupported apiUrl protocol for rbe tunnel: ${url.protocol}`);
|
|
296
|
+
}
|
|
297
|
+
url.pathname = `${url.pathname.replace(/\/+$/, '')}/rbe/tunnel`;
|
|
298
|
+
url.search = '';
|
|
299
|
+
url.hash = '';
|
|
300
|
+
// The tunnel layer (startMultiplexedTcpTunnel) appends mode=multiplexed to
|
|
301
|
+
// the URL itself, so we don't set it here.
|
|
302
|
+
return url.toString();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Raised when an `/rbe` request returns 404. limbuild's `/rbe` routes only
|
|
307
|
+
* exist on builds with remote-execution support, so a 404 there means RBE is
|
|
308
|
+
* unavailable on this instance (an older limbuild, or the wrong environment) —
|
|
309
|
+
* NOT that the instance is missing. Kept distinct from NotFoundError so the CLI
|
|
310
|
+
* does not mistake it for a vanished instance and spin up replacements.
|
|
311
|
+
*/
|
|
312
|
+
export class RbeUnsupportedError extends LimrunError {
|
|
313
|
+
constructor(operation: string) {
|
|
314
|
+
super(
|
|
315
|
+
`Remote build execution is not available on this Xcode instance (${operation} returned 404). ` +
|
|
316
|
+
'Its limbuild may predate RBE support, or you may be targeting the wrong environment ' +
|
|
317
|
+
'(for example production instead of staging).',
|
|
318
|
+
);
|
|
319
|
+
this.name = 'RbeUnsupportedError';
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Reads an `/rbe` JSON response, mapping a 404 to RbeUnsupportedError (a route
|
|
325
|
+
* 404 means RBE is not supported here, never a missing instance).
|
|
326
|
+
*/
|
|
327
|
+
async function readRbeResponse<T>(res: Response, operation: string): Promise<T> {
|
|
328
|
+
if (res.status === 404) {
|
|
329
|
+
await res.text().catch(() => undefined);
|
|
330
|
+
throw new RbeUnsupportedError(operation);
|
|
331
|
+
}
|
|
332
|
+
return readJsonResponse<T>(res, operation);
|
|
333
|
+
}
|
|
334
|
+
|
|
222
335
|
async function readJsonResponse<T>(res: Response, operation: string): Promise<T> {
|
|
223
336
|
const text = await res.text();
|
|
224
337
|
if (!res.ok) {
|
|
@@ -326,11 +439,15 @@ export class XcodeInstances extends GeneratedXcodeInstances {
|
|
|
326
439
|
if (options?.reactNative?.devServerURL && settings?.configuration === 'Release') {
|
|
327
440
|
throw new Error('reactNative.devServerURL is only supported for Debug builds');
|
|
328
441
|
}
|
|
442
|
+
if (options?.buildSettings) {
|
|
443
|
+
validateBuildSettings(options.buildSettings);
|
|
444
|
+
}
|
|
329
445
|
const request: ExecRequest = {
|
|
330
446
|
command: 'xcodebuild',
|
|
331
447
|
...(settings && { xcodebuild: settings }),
|
|
332
448
|
...(options?.reactNative && { reactNative: options.reactNative }),
|
|
333
449
|
...(options?.signing && { signing: options.signing }),
|
|
450
|
+
...(options?.buildSettings && { buildSettings: options.buildSettings }),
|
|
334
451
|
};
|
|
335
452
|
|
|
336
453
|
if (options?.upload && 'assetName' in options.upload) {
|
|
@@ -395,6 +512,51 @@ export class XcodeInstances extends GeneratedXcodeInstances {
|
|
|
395
512
|
});
|
|
396
513
|
return readJsonResponse<SimulatorAttachResult>(res, 'POST /simulator');
|
|
397
514
|
},
|
|
515
|
+
|
|
516
|
+
async startRbe(opts?: RbeStartOptions): Promise<RbeStatus> {
|
|
517
|
+
const res = await nodeProxyTransport.fetch(`${apiUrl}/rbe`, {
|
|
518
|
+
method: 'POST',
|
|
519
|
+
headers: {
|
|
520
|
+
'Content-Type': 'application/json',
|
|
521
|
+
Authorization: `Bearer ${token}`,
|
|
522
|
+
},
|
|
523
|
+
body: JSON.stringify(opts ?? {}),
|
|
524
|
+
});
|
|
525
|
+
return readRbeResponse<RbeStatus>(res, 'POST /rbe');
|
|
526
|
+
},
|
|
527
|
+
|
|
528
|
+
async getRbe(): Promise<RbeStatus> {
|
|
529
|
+
const res = await nodeProxyTransport.fetch(`${apiUrl}/rbe`, {
|
|
530
|
+
method: 'GET',
|
|
531
|
+
headers: {
|
|
532
|
+
Authorization: `Bearer ${token}`,
|
|
533
|
+
},
|
|
534
|
+
});
|
|
535
|
+
return readRbeResponse<RbeStatus>(res, 'GET /rbe');
|
|
536
|
+
},
|
|
537
|
+
|
|
538
|
+
async stopRbe(): Promise<RbeStatus> {
|
|
539
|
+
const res = await nodeProxyTransport.fetch(`${apiUrl}/rbe`, {
|
|
540
|
+
method: 'DELETE',
|
|
541
|
+
headers: {
|
|
542
|
+
Authorization: `Bearer ${token}`,
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
return readRbeResponse<RbeStatus>(res, 'DELETE /rbe');
|
|
546
|
+
},
|
|
547
|
+
|
|
548
|
+
async startRbeTunnel(opts?: RbeTunnelOptions): Promise<Tunnel> {
|
|
549
|
+
return startTcpTunnel(
|
|
550
|
+
deriveRbeTunnelUrl(apiUrl),
|
|
551
|
+
token,
|
|
552
|
+
opts?.host ?? '127.0.0.1',
|
|
553
|
+
opts?.port ?? DEFAULT_RBE_TUNNEL_PORT,
|
|
554
|
+
{
|
|
555
|
+
mode: 'multiplexed',
|
|
556
|
+
logLevel: opts?.logLevel ?? params.logLevel ?? 'info',
|
|
557
|
+
},
|
|
558
|
+
);
|
|
559
|
+
},
|
|
398
560
|
};
|
|
399
561
|
}
|
|
400
562
|
}
|
package/src/tunnel.ts
CHANGED
|
@@ -899,8 +899,19 @@ async function startMultiplexedTcpTunnel(
|
|
|
899
899
|
let ws: WebSocket | undefined;
|
|
900
900
|
let pingInterval: NodeJS.Timeout | undefined;
|
|
901
901
|
let intentionalDisconnect = false;
|
|
902
|
+
// True once the initial connect has succeeded. Initial-connect failures
|
|
903
|
+
// reject the outer promise (no reconnect); only post-establishment drops
|
|
904
|
+
// reconnect, keeping the local listener open so long builds survive a blip.
|
|
905
|
+
let established = false;
|
|
902
906
|
let connectionState: TunnelConnectionState = 'connecting';
|
|
903
907
|
|
|
908
|
+
const maxReconnectAttempts = options?.maxReconnectAttempts ?? 6;
|
|
909
|
+
const reconnectDelay = options?.reconnectDelay ?? 1000;
|
|
910
|
+
const maxReconnectDelay = options?.maxReconnectDelay ?? 30000;
|
|
911
|
+
let reconnectAttempts = 0;
|
|
912
|
+
let reconnectTimeout: NodeJS.Timeout | undefined;
|
|
913
|
+
let lastError: string | undefined;
|
|
914
|
+
|
|
904
915
|
const stateChangeCallbacks: Set<TunnelConnectionStateCallback> = new Set();
|
|
905
916
|
|
|
906
917
|
const updateConnectionState = (newState: TunnelConnectionState): void => {
|
|
@@ -943,6 +954,10 @@ async function startMultiplexedTcpTunnel(
|
|
|
943
954
|
|
|
944
955
|
const close = () => {
|
|
945
956
|
intentionalDisconnect = true;
|
|
957
|
+
if (reconnectTimeout) {
|
|
958
|
+
clearTimeout(reconnectTimeout);
|
|
959
|
+
reconnectTimeout = undefined;
|
|
960
|
+
}
|
|
946
961
|
cleanupWebSocket();
|
|
947
962
|
closeAllConnections();
|
|
948
963
|
updateConnectionState('disconnected');
|
|
@@ -951,6 +966,37 @@ async function startMultiplexedTcpTunnel(
|
|
|
951
966
|
}
|
|
952
967
|
};
|
|
953
968
|
|
|
969
|
+
// Single reconnect scheduler. Only ws.on('close') drives it (after the
|
|
970
|
+
// tunnel is established), so there is no double-scheduling race; the
|
|
971
|
+
// `reconnectTimeout` guard is belt-and-suspenders. Mirrors the singleton
|
|
972
|
+
// tunnel's backoff, but tears down per-connection streams on each drop
|
|
973
|
+
// since a multiplexed connId cannot be resumed across a WS reconnect.
|
|
974
|
+
const scheduleReconnect = (): void => {
|
|
975
|
+
if (intentionalDisconnect || reconnectTimeout) {
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
if (isNonRetryableError(lastError ?? '')) {
|
|
979
|
+
logger.error(`Closing multiplexed tunnel (non-retryable error): ${lastError}`);
|
|
980
|
+
close();
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
if (reconnectAttempts >= maxReconnectAttempts) {
|
|
984
|
+
logger.error(`Max reconnection attempts (${maxReconnectAttempts}) reached. Closing tunnel.`);
|
|
985
|
+
close();
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
const currentDelay = Math.min(reconnectDelay * Math.pow(2, reconnectAttempts), maxReconnectDelay);
|
|
989
|
+
reconnectAttempts++;
|
|
990
|
+
updateConnectionState('reconnecting');
|
|
991
|
+
logger.debug(`Scheduling reconnection attempt ${reconnectAttempts} in ${currentDelay}ms...`);
|
|
992
|
+
reconnectTimeout = setTimeout(() => {
|
|
993
|
+
reconnectTimeout = undefined;
|
|
994
|
+
// On failure the new ws's own 'close' handler schedules the next
|
|
995
|
+
// attempt, so here we only swallow the rejection.
|
|
996
|
+
setupWebSocket().catch(() => {});
|
|
997
|
+
}, currentDelay);
|
|
998
|
+
};
|
|
999
|
+
|
|
954
1000
|
// Send close signal for a connection (header-only packet)
|
|
955
1001
|
const sendCloseSignal = (connId: number) => {
|
|
956
1002
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
@@ -992,6 +1038,7 @@ async function startMultiplexedTcpTunnel(
|
|
|
992
1038
|
|
|
993
1039
|
ws.on('error', (err: any) => {
|
|
994
1040
|
const errMessage = err.message || String(err);
|
|
1041
|
+
lastError = errMessage;
|
|
995
1042
|
logger.error('WebSocket error:', errMessage, err.code || '');
|
|
996
1043
|
rejectWs(err);
|
|
997
1044
|
});
|
|
@@ -1001,17 +1048,28 @@ async function startMultiplexedTcpTunnel(
|
|
|
1001
1048
|
clearInterval(pingInterval);
|
|
1002
1049
|
pingInterval = undefined;
|
|
1003
1050
|
}
|
|
1004
|
-
|
|
1005
|
-
updateConnectionState('disconnected');
|
|
1006
1051
|
logger.debug(`WebSocket disconnected (code=${code}, reason=${reason.toString()})`);
|
|
1007
1052
|
|
|
1008
|
-
//
|
|
1053
|
+
// Per-connection streams cannot survive a WS drop (their remote peers
|
|
1054
|
+
// are gone), so tear them down; bazel's own gRPC retries re-establish
|
|
1055
|
+
// channels once the WS is back.
|
|
1009
1056
|
closeAllConnections();
|
|
1057
|
+
|
|
1058
|
+
// Before establishment, the listening handler's catch rejects + closes
|
|
1059
|
+
// (no reconnect). After establishment, a non-intentional drop reconnects
|
|
1060
|
+
// (keeping the listener open) instead of killing the tunnel.
|
|
1061
|
+
if (!established || intentionalDisconnect) {
|
|
1062
|
+
updateConnectionState('disconnected');
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
scheduleReconnect();
|
|
1010
1066
|
});
|
|
1011
1067
|
|
|
1012
1068
|
ws.on('open', () => {
|
|
1013
1069
|
const socket = ws as WebSocket;
|
|
1014
1070
|
logger.debug('WebSocket connected');
|
|
1071
|
+
reconnectAttempts = 0;
|
|
1072
|
+
lastError = undefined;
|
|
1015
1073
|
updateConnectionState('connected');
|
|
1016
1074
|
|
|
1017
1075
|
pingInterval = setInterval(() => {
|
|
@@ -1090,6 +1148,7 @@ async function startMultiplexedTcpTunnel(
|
|
|
1090
1148
|
// Start WebSocket connection and wait for it to be ready
|
|
1091
1149
|
try {
|
|
1092
1150
|
await setupWebSocket();
|
|
1151
|
+
established = true;
|
|
1093
1152
|
logger.debug('WebSocket ready, tunnel fully initialized');
|
|
1094
1153
|
resolve({
|
|
1095
1154
|
address,
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.31.0'; // x-release-please-version
|
package/tunnel.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel.d.mts","sourceRoot":"","sources":["src/tunnel.ts"],"names":[],"mappings":"AAKA;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEpE;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,YAAY,GAAG,WAAW,GAAG,cAAc,GAAG,cAAc,CAAC;AAEjG;;GAEG;AACH,MAAM,MAAM,6BAA6B,GAAG,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,CAAC;AAEnF;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG,aAAa,CAAC;AAErD,MAAM,WAAW,MAAM;IACrB,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB;;OAEG;IACH,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;IAChD;;;OAGG;IACH,uBAAuB,EAAE,CAAC,QAAQ,EAAE,6BAA6B,KAAK,MAAM,IAAI,CAAC;CAClF;AAED,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;IAChD,uBAAuB,EAAE,CAAC,QAAQ,EAAE,6BAA6B,KAAK,MAAM,IAAI,CAAC;CAClF;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,MAAM,CAAC,CAQjB;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,aAAa,CAAC,CA+XxB;
|
|
1
|
+
{"version":3,"file":"tunnel.d.mts","sourceRoot":"","sources":["src/tunnel.ts"],"names":[],"mappings":"AAKA;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEpE;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,YAAY,GAAG,WAAW,GAAG,cAAc,GAAG,cAAc,CAAC;AAEjG;;GAEG;AACH,MAAM,MAAM,6BAA6B,GAAG,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,CAAC;AAEnF;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG,aAAa,CAAC;AAErD,MAAM,WAAW,MAAM;IACrB,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB;;OAEG;IACH,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;IAChD;;;OAGG;IACH,uBAAuB,EAAE,CAAC,QAAQ,EAAE,6BAA6B,KAAK,MAAM,IAAI,CAAC;CAClF;AAED,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;IAChD,uBAAuB,EAAE,CAAC,QAAQ,EAAE,6BAA6B,KAAK,MAAM,IAAI,CAAC;CAClF;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,MAAM,CAAC,CAQjB;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,aAAa,CAAC,CA+XxB;AAoqBD,eAAO,MAAM,mBAAmB,GAAI,YAAY,MAAM,KAAG,OAOxD,CAAC;AAEF,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAIrF;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAK7D"}
|
package/tunnel.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["src/tunnel.ts"],"names":[],"mappings":"AAKA;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEpE;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,YAAY,GAAG,WAAW,GAAG,cAAc,GAAG,cAAc,CAAC;AAEjG;;GAEG;AACH,MAAM,MAAM,6BAA6B,GAAG,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,CAAC;AAEnF;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG,aAAa,CAAC;AAErD,MAAM,WAAW,MAAM;IACrB,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB;;OAEG;IACH,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;IAChD;;;OAGG;IACH,uBAAuB,EAAE,CAAC,QAAQ,EAAE,6BAA6B,KAAK,MAAM,IAAI,CAAC;CAClF;AAED,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;IAChD,uBAAuB,EAAE,CAAC,QAAQ,EAAE,6BAA6B,KAAK,MAAM,IAAI,CAAC;CAClF;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,MAAM,CAAC,CAQjB;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,aAAa,CAAC,CA+XxB;
|
|
1
|
+
{"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["src/tunnel.ts"],"names":[],"mappings":"AAKA;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEpE;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,YAAY,GAAG,WAAW,GAAG,cAAc,GAAG,cAAc,CAAC;AAEjG;;GAEG;AACH,MAAM,MAAM,6BAA6B,GAAG,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,CAAC;AAEnF;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG,aAAa,CAAC;AAErD,MAAM,WAAW,MAAM;IACrB,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB;;OAEG;IACH,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;IAChD;;;OAGG;IACH,uBAAuB,EAAE,CAAC,QAAQ,EAAE,6BAA6B,KAAK,MAAM,IAAI,CAAC;CAClF;AAED,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;IAChD,uBAAuB,EAAE,CAAC,QAAQ,EAAE,6BAA6B,KAAK,MAAM,IAAI,CAAC;CAClF;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,MAAM,CAAC,CAQjB;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,aAAa,CAAC,CA+XxB;AAoqBD,eAAO,MAAM,mBAAmB,GAAI,YAAY,MAAM,KAAG,OAOxD,CAAC;AAEF,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAIrF;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAK7D"}
|
package/tunnel.js
CHANGED
|
@@ -690,7 +690,17 @@ async function startMultiplexedTcpTunnel(remoteURL, token, hostname, port, optio
|
|
|
690
690
|
let ws;
|
|
691
691
|
let pingInterval;
|
|
692
692
|
let intentionalDisconnect = false;
|
|
693
|
+
// True once the initial connect has succeeded. Initial-connect failures
|
|
694
|
+
// reject the outer promise (no reconnect); only post-establishment drops
|
|
695
|
+
// reconnect, keeping the local listener open so long builds survive a blip.
|
|
696
|
+
let established = false;
|
|
693
697
|
let connectionState = 'connecting';
|
|
698
|
+
const maxReconnectAttempts = options?.maxReconnectAttempts ?? 6;
|
|
699
|
+
const reconnectDelay = options?.reconnectDelay ?? 1000;
|
|
700
|
+
const maxReconnectDelay = options?.maxReconnectDelay ?? 30000;
|
|
701
|
+
let reconnectAttempts = 0;
|
|
702
|
+
let reconnectTimeout;
|
|
703
|
+
let lastError;
|
|
694
704
|
const stateChangeCallbacks = new Set();
|
|
695
705
|
const updateConnectionState = (newState) => {
|
|
696
706
|
if (connectionState !== newState) {
|
|
@@ -730,6 +740,10 @@ async function startMultiplexedTcpTunnel(remoteURL, token, hostname, port, optio
|
|
|
730
740
|
};
|
|
731
741
|
const close = () => {
|
|
732
742
|
intentionalDisconnect = true;
|
|
743
|
+
if (reconnectTimeout) {
|
|
744
|
+
clearTimeout(reconnectTimeout);
|
|
745
|
+
reconnectTimeout = undefined;
|
|
746
|
+
}
|
|
733
747
|
cleanupWebSocket();
|
|
734
748
|
closeAllConnections();
|
|
735
749
|
updateConnectionState('disconnected');
|
|
@@ -737,6 +751,36 @@ async function startMultiplexedTcpTunnel(remoteURL, token, hostname, port, optio
|
|
|
737
751
|
server.close();
|
|
738
752
|
}
|
|
739
753
|
};
|
|
754
|
+
// Single reconnect scheduler. Only ws.on('close') drives it (after the
|
|
755
|
+
// tunnel is established), so there is no double-scheduling race; the
|
|
756
|
+
// `reconnectTimeout` guard is belt-and-suspenders. Mirrors the singleton
|
|
757
|
+
// tunnel's backoff, but tears down per-connection streams on each drop
|
|
758
|
+
// since a multiplexed connId cannot be resumed across a WS reconnect.
|
|
759
|
+
const scheduleReconnect = () => {
|
|
760
|
+
if (intentionalDisconnect || reconnectTimeout) {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if ((0, exports.isNonRetryableError)(lastError ?? '')) {
|
|
764
|
+
logger.error(`Closing multiplexed tunnel (non-retryable error): ${lastError}`);
|
|
765
|
+
close();
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (reconnectAttempts >= maxReconnectAttempts) {
|
|
769
|
+
logger.error(`Max reconnection attempts (${maxReconnectAttempts}) reached. Closing tunnel.`);
|
|
770
|
+
close();
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
const currentDelay = Math.min(reconnectDelay * Math.pow(2, reconnectAttempts), maxReconnectDelay);
|
|
774
|
+
reconnectAttempts++;
|
|
775
|
+
updateConnectionState('reconnecting');
|
|
776
|
+
logger.debug(`Scheduling reconnection attempt ${reconnectAttempts} in ${currentDelay}ms...`);
|
|
777
|
+
reconnectTimeout = setTimeout(() => {
|
|
778
|
+
reconnectTimeout = undefined;
|
|
779
|
+
// On failure the new ws's own 'close' handler schedules the next
|
|
780
|
+
// attempt, so here we only swallow the rejection.
|
|
781
|
+
setupWebSocket().catch(() => { });
|
|
782
|
+
}, currentDelay);
|
|
783
|
+
};
|
|
740
784
|
// Send close signal for a connection (header-only packet)
|
|
741
785
|
const sendCloseSignal = (connId) => {
|
|
742
786
|
if (ws && ws.readyState === ws_1.WebSocket.OPEN) {
|
|
@@ -772,6 +816,7 @@ async function startMultiplexedTcpTunnel(remoteURL, token, hostname, port, optio
|
|
|
772
816
|
});
|
|
773
817
|
ws.on('error', (err) => {
|
|
774
818
|
const errMessage = err.message || String(err);
|
|
819
|
+
lastError = errMessage;
|
|
775
820
|
logger.error('WebSocket error:', errMessage, err.code || '');
|
|
776
821
|
rejectWs(err);
|
|
777
822
|
});
|
|
@@ -780,14 +825,25 @@ async function startMultiplexedTcpTunnel(remoteURL, token, hostname, port, optio
|
|
|
780
825
|
clearInterval(pingInterval);
|
|
781
826
|
pingInterval = undefined;
|
|
782
827
|
}
|
|
783
|
-
updateConnectionState('disconnected');
|
|
784
828
|
logger.debug(`WebSocket disconnected (code=${code}, reason=${reason.toString()})`);
|
|
785
|
-
//
|
|
829
|
+
// Per-connection streams cannot survive a WS drop (their remote peers
|
|
830
|
+
// are gone), so tear them down; bazel's own gRPC retries re-establish
|
|
831
|
+
// channels once the WS is back.
|
|
786
832
|
closeAllConnections();
|
|
833
|
+
// Before establishment, the listening handler's catch rejects + closes
|
|
834
|
+
// (no reconnect). After establishment, a non-intentional drop reconnects
|
|
835
|
+
// (keeping the listener open) instead of killing the tunnel.
|
|
836
|
+
if (!established || intentionalDisconnect) {
|
|
837
|
+
updateConnectionState('disconnected');
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
scheduleReconnect();
|
|
787
841
|
});
|
|
788
842
|
ws.on('open', () => {
|
|
789
843
|
const socket = ws;
|
|
790
844
|
logger.debug('WebSocket connected');
|
|
845
|
+
reconnectAttempts = 0;
|
|
846
|
+
lastError = undefined;
|
|
791
847
|
updateConnectionState('connected');
|
|
792
848
|
pingInterval = setInterval(() => {
|
|
793
849
|
if (socket.readyState === ws_1.WebSocket.OPEN) {
|
|
@@ -852,6 +908,7 @@ async function startMultiplexedTcpTunnel(remoteURL, token, hostname, port, optio
|
|
|
852
908
|
// Start WebSocket connection and wait for it to be ready
|
|
853
909
|
try {
|
|
854
910
|
await setupWebSocket();
|
|
911
|
+
established = true;
|
|
855
912
|
logger.debug('WebSocket ready, tunnel fully initialized');
|
|
856
913
|
resolve({
|
|
857
914
|
address,
|