@openclaw/gateway-client 0.0.0 → 2026.7.2-beta.4
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 +7 -0
- package/LICENSE +21 -0
- package/README.md +133 -2
- package/dist/browser-device-auth-Bp297Rtf.d.mts +130 -0
- package/dist/browser.d.mts +20 -0
- package/dist/browser.mjs +8 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +1781 -0
- package/dist/protocol-client-Cj1WAoMt.d.mts +180 -0
- package/dist/readiness-CPiKyi1s.d.mts +201 -0
- package/dist/readiness-CrWxd4TI.mjs +105 -0
- package/dist/readiness.d.mts +2 -0
- package/dist/readiness.mjs +3 -0
- package/dist/reconnect-policy-CkzLe1_K.mjs +850 -0
- package/dist/timeouts.d.mts +39 -0
- package/dist/timeouts.mjs +77 -0
- package/npm-shrinkwrap.json +50 -0
- package/package.json +69 -6
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
import { ConnectErrorDetailCodes, readConnectErrorDetailCode, readConnectErrorRecoveryAdvice, readPairingConnectErrorDetails } from "@openclaw/gateway-protocol/connect-error-details";
|
|
2
|
+
import { isGatewayEventFrame, isGatewayResponseFrame } from "@openclaw/gateway-protocol/frame-guards";
|
|
3
|
+
//#region packages/gateway-client/src/device-auth.ts
|
|
4
|
+
function normalizeDeviceMetadataForAuth(value) {
|
|
5
|
+
if (typeof value !== "string") return "";
|
|
6
|
+
const trimmed = value.trim();
|
|
7
|
+
if (!trimmed) return "";
|
|
8
|
+
return trimmed.replace(/[A-Z]/g, (char) => String.fromCharCode(char.charCodeAt(0) + 32));
|
|
9
|
+
}
|
|
10
|
+
function buildDeviceAuthPayload(params) {
|
|
11
|
+
const scopes = params.scopes.join(",");
|
|
12
|
+
const token = params.token ?? "";
|
|
13
|
+
return [
|
|
14
|
+
"v2",
|
|
15
|
+
params.deviceId,
|
|
16
|
+
params.clientId,
|
|
17
|
+
params.clientMode,
|
|
18
|
+
params.role,
|
|
19
|
+
scopes,
|
|
20
|
+
String(params.signedAtMs),
|
|
21
|
+
token,
|
|
22
|
+
params.nonce
|
|
23
|
+
].join("|");
|
|
24
|
+
}
|
|
25
|
+
function buildDeviceAuthPayloadV3(params) {
|
|
26
|
+
const scopes = params.scopes.join(",");
|
|
27
|
+
const token = params.token ?? "";
|
|
28
|
+
const platform = normalizeDeviceMetadataForAuth(params.platform);
|
|
29
|
+
const deviceFamily = normalizeDeviceMetadataForAuth(params.deviceFamily);
|
|
30
|
+
return [
|
|
31
|
+
"v3",
|
|
32
|
+
params.deviceId,
|
|
33
|
+
params.clientId,
|
|
34
|
+
params.clientMode,
|
|
35
|
+
params.role,
|
|
36
|
+
scopes,
|
|
37
|
+
String(params.signedAtMs),
|
|
38
|
+
token,
|
|
39
|
+
params.nonce,
|
|
40
|
+
platform,
|
|
41
|
+
deviceFamily
|
|
42
|
+
].join("|");
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region packages/gateway-client/src/connect-auth.ts
|
|
46
|
+
function normalized(value) {
|
|
47
|
+
return typeof value === "string" ? value.trim() || void 0 : void 0;
|
|
48
|
+
}
|
|
49
|
+
function selectGatewayConnectAuth(params) {
|
|
50
|
+
const authToken = normalized(params.token);
|
|
51
|
+
const bootstrapToken = normalized(params.bootstrapToken);
|
|
52
|
+
const explicitDeviceToken = normalized(params.deviceToken);
|
|
53
|
+
const authPassword = normalized(params.password);
|
|
54
|
+
const storedToken = normalized(params.storedToken);
|
|
55
|
+
const stored = {
|
|
56
|
+
storedToken,
|
|
57
|
+
storedScopes: params.storedScopes
|
|
58
|
+
};
|
|
59
|
+
if (params.preferBootstrapToken && bootstrapToken) return {
|
|
60
|
+
authBootstrapToken: bootstrapToken,
|
|
61
|
+
authPassword,
|
|
62
|
+
...stored
|
|
63
|
+
};
|
|
64
|
+
const useRetryToken = params.pendingDeviceTokenRetry === true && !explicitDeviceToken && Boolean(authToken && storedToken && params.trustedDeviceTokenRetry);
|
|
65
|
+
const resolvedDeviceToken = explicitDeviceToken ?? (useRetryToken || !(authToken || authPassword) && (!bootstrapToken || storedToken) ? storedToken : void 0);
|
|
66
|
+
const usingStoredDeviceToken = Boolean(resolvedDeviceToken && !explicitDeviceToken && storedToken) && resolvedDeviceToken === storedToken;
|
|
67
|
+
const selectedToken = authToken ?? resolvedDeviceToken;
|
|
68
|
+
const authBootstrapToken = !authToken && !resolvedDeviceToken && !authPassword ? bootstrapToken : void 0;
|
|
69
|
+
return {
|
|
70
|
+
authToken: selectedToken,
|
|
71
|
+
authBootstrapToken,
|
|
72
|
+
authDeviceToken: useRetryToken ? storedToken : void 0,
|
|
73
|
+
authPassword,
|
|
74
|
+
authApprovalRuntimeToken: normalized(params.approvalRuntimeToken),
|
|
75
|
+
authAgentRuntimeIdentityToken: normalized(params.agentRuntimeIdentityToken),
|
|
76
|
+
signatureToken: selectedToken ?? authBootstrapToken,
|
|
77
|
+
resolvedDeviceToken,
|
|
78
|
+
usingStoredDeviceToken,
|
|
79
|
+
...stored
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function buildGatewayConnectAuth(selected) {
|
|
83
|
+
const auth = {
|
|
84
|
+
token: selected.authToken,
|
|
85
|
+
bootstrapToken: selected.authBootstrapToken,
|
|
86
|
+
deviceToken: selected.authDeviceToken ?? selected.resolvedDeviceToken,
|
|
87
|
+
password: selected.authPassword,
|
|
88
|
+
approvalRuntimeToken: selected.authApprovalRuntimeToken,
|
|
89
|
+
agentRuntimeIdentityToken: selected.authAgentRuntimeIdentityToken
|
|
90
|
+
};
|
|
91
|
+
return Object.values(auth).some(Boolean) ? auth : void 0;
|
|
92
|
+
}
|
|
93
|
+
function resolveGatewayConnectScopes(params) {
|
|
94
|
+
return params.requestedScopes ?? (params.usingStoredDeviceToken && params.storedScopes?.length ? params.storedScopes : [...params.defaultScopes]);
|
|
95
|
+
}
|
|
96
|
+
function shouldRetryGatewayWithDeviceToken(params) {
|
|
97
|
+
if (params.retryBudgetUsed || params.currentDeviceToken || !params.explicitToken || !params.storedToken || !params.trustedEndpoint) return false;
|
|
98
|
+
const advice = readConnectErrorRecoveryAdvice(params.errorDetails);
|
|
99
|
+
return params.canRetryWithDeviceTokenHint === true || advice.canRetryWithDeviceToken === true || advice.recommendedNextStep === "retry_with_device_token" || readConnectErrorDetailCode(params.errorDetails) === ConnectErrorDetailCodes.AUTH_TOKEN_MISMATCH;
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region packages/gateway-client/src/browser-device-auth.ts
|
|
103
|
+
/** Browser-safe device pairing and issued-token lifecycle shared by first-party UI clients. */
|
|
104
|
+
var GatewayBrowserDeviceAuthLifecycle = class {
|
|
105
|
+
constructor(deps) {
|
|
106
|
+
this.deps = deps;
|
|
107
|
+
}
|
|
108
|
+
async buildPlan(params) {
|
|
109
|
+
const identity = await this.deps.loadIdentity();
|
|
110
|
+
const stored = identity ? await this.deps.tokenStore.load({
|
|
111
|
+
clientId: params.client.id,
|
|
112
|
+
deviceId: identity.deviceId,
|
|
113
|
+
role: params.role
|
|
114
|
+
}) : null;
|
|
115
|
+
const storedValue = stored?.token;
|
|
116
|
+
const selectedAuth = selectGatewayConnectAuth({
|
|
117
|
+
token: params.token,
|
|
118
|
+
bootstrapToken: params.bootstrapToken,
|
|
119
|
+
password: params.password,
|
|
120
|
+
storedToken: storedValue,
|
|
121
|
+
storedScopes: stored?.scopes,
|
|
122
|
+
pendingDeviceTokenRetry: params.pendingDeviceTokenRetry,
|
|
123
|
+
trustedDeviceTokenRetry: params.trustedDeviceTokenRetry,
|
|
124
|
+
preferBootstrapToken: params.preferBootstrapToken
|
|
125
|
+
});
|
|
126
|
+
const { usingStoredDeviceToken } = selectedAuth;
|
|
127
|
+
const scopes = resolveGatewayConnectScopes({
|
|
128
|
+
requestedScopes: selectedAuth.authBootstrapToken ? params.bootstrapScopes ? [...params.bootstrapScopes] : void 0 : void 0,
|
|
129
|
+
usingStoredDeviceToken,
|
|
130
|
+
storedScopes: selectedAuth.storedScopes,
|
|
131
|
+
defaultScopes: params.defaultScopes
|
|
132
|
+
});
|
|
133
|
+
if (!identity) return {
|
|
134
|
+
clientId: params.client.id,
|
|
135
|
+
role: params.role,
|
|
136
|
+
identity,
|
|
137
|
+
selectedAuth,
|
|
138
|
+
scopes,
|
|
139
|
+
auth: buildGatewayConnectAuth(selectedAuth)
|
|
140
|
+
};
|
|
141
|
+
const signedAtMs = this.deps.nowMs?.() ?? Date.now();
|
|
142
|
+
const nonce = params.nonce ?? "";
|
|
143
|
+
const { authBootstrapToken: primary, signatureToken: signed } = selectedAuth;
|
|
144
|
+
let token = null;
|
|
145
|
+
if (primary) token = primary;
|
|
146
|
+
else if (signed) token = signed;
|
|
147
|
+
const payload = buildDeviceAuthPayloadV3({
|
|
148
|
+
deviceId: identity.deviceId,
|
|
149
|
+
clientId: params.client.id,
|
|
150
|
+
clientMode: params.client.mode,
|
|
151
|
+
role: params.role,
|
|
152
|
+
scopes,
|
|
153
|
+
signedAtMs,
|
|
154
|
+
token,
|
|
155
|
+
nonce,
|
|
156
|
+
platform: params.client.platform,
|
|
157
|
+
deviceFamily: params.client.deviceFamily
|
|
158
|
+
});
|
|
159
|
+
return {
|
|
160
|
+
clientId: params.client.id,
|
|
161
|
+
role: params.role,
|
|
162
|
+
identity,
|
|
163
|
+
selectedAuth,
|
|
164
|
+
scopes,
|
|
165
|
+
auth: buildGatewayConnectAuth(selectedAuth),
|
|
166
|
+
device: {
|
|
167
|
+
id: identity.deviceId,
|
|
168
|
+
publicKey: identity.publicKey,
|
|
169
|
+
signature: await identity.sign(payload),
|
|
170
|
+
signedAt: signedAtMs,
|
|
171
|
+
nonce
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
async acceptHello(hello, plan) {
|
|
176
|
+
const token = hello.auth?.deviceToken?.trim();
|
|
177
|
+
if (!token || !plan.identity) return;
|
|
178
|
+
await this.deps.tokenStore.store({
|
|
179
|
+
clientId: plan.clientId,
|
|
180
|
+
deviceId: plan.identity.deviceId,
|
|
181
|
+
role: hello.auth?.role ?? plan.role,
|
|
182
|
+
token,
|
|
183
|
+
scopes: hello.auth?.scopes ?? []
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
async clearStoredToken(plan) {
|
|
187
|
+
if (!plan.identity) return;
|
|
188
|
+
await this.deps.tokenStore.clear({
|
|
189
|
+
clientId: plan.clientId,
|
|
190
|
+
deviceId: plan.identity.deviceId,
|
|
191
|
+
role: plan.role
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region packages/retry/src/index.ts
|
|
197
|
+
const MAX_TIMER_TIMEOUT_MS = 2147e6;
|
|
198
|
+
function computeBackoff(policy, attempt) {
|
|
199
|
+
const base = Math.min(policy.maxMs, policy.initialMs * policy.factor ** Math.max(attempt - 1, 0));
|
|
200
|
+
const jitter = base * policy.jitter * Math.random();
|
|
201
|
+
return Math.min(policy.maxMs, Math.round(base + jitter));
|
|
202
|
+
}
|
|
203
|
+
async function sleepWithAbort(ms, abortSignal, options = {}) {
|
|
204
|
+
if (!Number.isFinite(ms) || ms <= 0) return;
|
|
205
|
+
const delayMs = Math.min(Math.max(Math.floor(ms), 1), MAX_TIMER_TIMEOUT_MS);
|
|
206
|
+
await new Promise((resolve, reject) => {
|
|
207
|
+
let settled = false;
|
|
208
|
+
let timer = null;
|
|
209
|
+
const cleanup = () => abortSignal?.removeEventListener("abort", onAbort);
|
|
210
|
+
const onAbort = () => {
|
|
211
|
+
if (settled) return;
|
|
212
|
+
settled = true;
|
|
213
|
+
if (timer) clearTimeout(timer);
|
|
214
|
+
timer = null;
|
|
215
|
+
cleanup();
|
|
216
|
+
reject(new Error("aborted", { cause: abortSignal?.reason ?? /* @__PURE__ */ new Error("aborted") }));
|
|
217
|
+
};
|
|
218
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
219
|
+
if (abortSignal?.aborted) {
|
|
220
|
+
onAbort();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
timer = setTimeout(() => {
|
|
224
|
+
settled = true;
|
|
225
|
+
cleanup();
|
|
226
|
+
timer = null;
|
|
227
|
+
resolve();
|
|
228
|
+
}, delayMs);
|
|
229
|
+
if (options.ref === false) timer.unref?.();
|
|
230
|
+
if (abortSignal?.aborted) onAbort();
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
var RetrySupervisor = class {
|
|
234
|
+
constructor(policy, maxAttempts = Number.POSITIVE_INFINITY) {
|
|
235
|
+
this.policy = policy;
|
|
236
|
+
this.maxAttempts = maxAttempts;
|
|
237
|
+
this.attempts = 0;
|
|
238
|
+
this.initialMs = policy.initialMs;
|
|
239
|
+
}
|
|
240
|
+
reset(initialMs = this.policy.initialMs) {
|
|
241
|
+
this.cancel();
|
|
242
|
+
this.attempts = 0;
|
|
243
|
+
this.initialMs = initialMs;
|
|
244
|
+
this.nextDelayOverrideMs = void 0;
|
|
245
|
+
}
|
|
246
|
+
cancel(reason = /* @__PURE__ */ new Error("retry cancelled")) {
|
|
247
|
+
this.pendingAbort?.abort(reason);
|
|
248
|
+
this.pendingAbort = void 0;
|
|
249
|
+
}
|
|
250
|
+
next(abortSignal) {
|
|
251
|
+
const override = this.nextDelayOverrideMs;
|
|
252
|
+
this.nextDelayOverrideMs = void 0;
|
|
253
|
+
if (override === void 0 && ++this.attempts > Math.ceil(this.maxAttempts)) return;
|
|
254
|
+
const attempt = Math.max(this.attempts, 1);
|
|
255
|
+
const delayMs = override ?? computeBackoff({
|
|
256
|
+
...this.policy,
|
|
257
|
+
initialMs: this.initialMs
|
|
258
|
+
}, attempt);
|
|
259
|
+
this.cancel();
|
|
260
|
+
const pendingAbort = new AbortController();
|
|
261
|
+
this.pendingAbort = pendingAbort;
|
|
262
|
+
return {
|
|
263
|
+
attempt,
|
|
264
|
+
delayMs,
|
|
265
|
+
signal: abortSignal ? AbortSignal.any([pendingAbort.signal, abortSignal]) : pendingAbort.signal
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
270
|
+
attempts: 3,
|
|
271
|
+
minDelayMs: 300,
|
|
272
|
+
maxDelayMs: 3e4,
|
|
273
|
+
jitter: 0
|
|
274
|
+
};
|
|
275
|
+
const defaultSleep = (ms) => new Promise((resolve) => {
|
|
276
|
+
setTimeout(resolve, ms);
|
|
277
|
+
});
|
|
278
|
+
function asFiniteNumber(value) {
|
|
279
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
280
|
+
}
|
|
281
|
+
function clampNumber(value, fallback, min, max) {
|
|
282
|
+
const next = asFiniteNumber(value);
|
|
283
|
+
if (next === void 0) return fallback;
|
|
284
|
+
return Math.min(Math.max(next, min ?? Number.NEGATIVE_INFINITY), max ?? Number.POSITIVE_INFINITY);
|
|
285
|
+
}
|
|
286
|
+
function resolveAttemptCount(value, fallback) {
|
|
287
|
+
return Math.max(1, Math.round(asFiniteNumber(value) ?? fallback));
|
|
288
|
+
}
|
|
289
|
+
function resolveRetryDelayMs(value) {
|
|
290
|
+
const finite = value === Number.POSITIVE_INFINITY ? MAX_TIMER_TIMEOUT_MS : asFiniteNumber(value) ?? 0;
|
|
291
|
+
return Math.min(Math.max(Math.round(finite), 0), MAX_TIMER_TIMEOUT_MS);
|
|
292
|
+
}
|
|
293
|
+
function resolveJitterConfig(value, fallback) {
|
|
294
|
+
if (value === "full") return "full";
|
|
295
|
+
const fraction = asFiniteNumber(value);
|
|
296
|
+
return fraction === void 0 ? fallback : Math.min(Math.max(fraction, 0), 1);
|
|
297
|
+
}
|
|
298
|
+
function resolveRetryConfig(defaults = DEFAULT_RETRY_CONFIG, overrides) {
|
|
299
|
+
const attempts = resolveAttemptCount(overrides?.attempts, defaults.attempts);
|
|
300
|
+
const minDelayMs = resolveRetryDelayMs(clampNumber(overrides?.minDelayMs, defaults.minDelayMs, 0));
|
|
301
|
+
return {
|
|
302
|
+
attempts,
|
|
303
|
+
minDelayMs,
|
|
304
|
+
maxDelayMs: Math.max(minDelayMs, resolveRetryDelayMs(clampNumber(overrides?.maxDelayMs, defaults.maxDelayMs, 0))),
|
|
305
|
+
jitter: resolveJitterConfig(overrides?.jitter, defaults.jitter)
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function applyJitter(delayMs, jitter, mode, random) {
|
|
309
|
+
if (jitter === "full") {
|
|
310
|
+
if (mode === "symmetric") return Math.max(0, Math.round(delayMs * (.5 + random() * .5)));
|
|
311
|
+
return Math.max(0, Math.ceil(delayMs * (1 + random())));
|
|
312
|
+
}
|
|
313
|
+
if (jitter <= 0) return mode === "positive" ? Math.ceil(delayMs) : delayMs;
|
|
314
|
+
const fraction = random();
|
|
315
|
+
const raw = delayMs * (1 + (mode === "positive" ? fraction * jitter : (fraction * 2 - 1) * jitter));
|
|
316
|
+
return Math.max(0, mode === "positive" ? Math.ceil(raw) : Math.round(raw));
|
|
317
|
+
}
|
|
318
|
+
function toRetryError(value, fallbackMessage = "Non-Error thrown") {
|
|
319
|
+
if (value instanceof Error) return value;
|
|
320
|
+
if (typeof value === "string") return new Error(value);
|
|
321
|
+
const error = new Error(fallbackMessage, { cause: value });
|
|
322
|
+
if (typeof value === "object" && value !== null || typeof value === "function") Object.assign(error, value);
|
|
323
|
+
return error;
|
|
324
|
+
}
|
|
325
|
+
function createRetryRunner(runtime = {}) {
|
|
326
|
+
const runtimeSleep = runtime.sleep ?? defaultSleep;
|
|
327
|
+
const runtimeRandom = runtime.random ?? Math.random;
|
|
328
|
+
const createFailure = runtime.createFailure ?? ((errors) => toRetryError(errors.at(-1) ?? /* @__PURE__ */ new Error("Retry failed")));
|
|
329
|
+
return async function retryAsync(fn, attemptsOrOptions = 3, initialDelayMs = 300) {
|
|
330
|
+
const attemptErrors = [];
|
|
331
|
+
if (typeof attemptsOrOptions === "number") {
|
|
332
|
+
const attempts = resolveAttemptCount(attemptsOrOptions, DEFAULT_RETRY_CONFIG.attempts);
|
|
333
|
+
for (let index = 0; index < attempts; index += 1) try {
|
|
334
|
+
return await fn();
|
|
335
|
+
} catch (err) {
|
|
336
|
+
attemptErrors.push(err);
|
|
337
|
+
if (index === attempts - 1) break;
|
|
338
|
+
await runtimeSleep(resolveRetryDelayMs(initialDelayMs * 2 ** index));
|
|
339
|
+
}
|
|
340
|
+
throw createFailure(attemptErrors);
|
|
341
|
+
}
|
|
342
|
+
const options = attemptsOrOptions;
|
|
343
|
+
const resolved = resolveRetryConfig(DEFAULT_RETRY_CONFIG, options);
|
|
344
|
+
const maxAttempts = resolved.attempts;
|
|
345
|
+
const minDelayMs = resolved.minDelayMs;
|
|
346
|
+
const maxDelayMs = resolved.maxDelayMs > 0 ? resolved.maxDelayMs : Number.POSITIVE_INFINITY;
|
|
347
|
+
const retryAfterMaxDelayMs = options.retryAfterMaxDelayMs === void 0 ? maxDelayMs : Math.max(minDelayMs, resolveRetryDelayMs(clampNumber(options.retryAfterMaxDelayMs, maxDelayMs, 0)));
|
|
348
|
+
const random = options.random ?? runtimeRandom;
|
|
349
|
+
const sleep = options.sleep ?? runtimeSleep;
|
|
350
|
+
const shouldRetry = options.shouldRetry ?? (() => true);
|
|
351
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) try {
|
|
352
|
+
return await fn();
|
|
353
|
+
} catch (err) {
|
|
354
|
+
attemptErrors.push(err);
|
|
355
|
+
if (attempt >= maxAttempts || !shouldRetry(err, attempt)) break;
|
|
356
|
+
const context = {
|
|
357
|
+
attempt,
|
|
358
|
+
maxAttempts,
|
|
359
|
+
err,
|
|
360
|
+
label: options.label
|
|
361
|
+
};
|
|
362
|
+
const retryAfterMs = options.retryAfterMs?.(err);
|
|
363
|
+
const hasRetryAfter = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs);
|
|
364
|
+
const configuredDelay = typeof options.delayMs === "function" ? options.delayMs(context) : options.delayMs;
|
|
365
|
+
const resolvedConfiguredDelay = configuredDelay === void 0 ? void 0 : resolveRetryDelayMs(configuredDelay);
|
|
366
|
+
const baseDelay = hasRetryAfter ? Math.max(retryAfterMs, minDelayMs) : resolvedConfiguredDelay === void 0 ? minDelayMs * 2 ** (attempt - 1) : Math.max(resolvedConfiguredDelay, minDelayMs);
|
|
367
|
+
const delayCap = hasRetryAfter ? retryAfterMaxDelayMs : maxDelayMs;
|
|
368
|
+
let delay = Math.min(baseDelay, delayCap);
|
|
369
|
+
const canHonorRetryAfter = hasRetryAfter && (retryAfterMs ?? 0) <= delayCap;
|
|
370
|
+
const wantsPositiveDraw = resolved.jitter === "full" ? !hasRetryAfter || canHonorRetryAfter : canHonorRetryAfter;
|
|
371
|
+
delay = applyJitter(delay, resolved.jitter, wantsPositiveDraw ? "positive" : "symmetric", random);
|
|
372
|
+
delay = Math.min(Math.max(delay, minDelayMs), delayCap);
|
|
373
|
+
await options.onRetry?.({
|
|
374
|
+
...context,
|
|
375
|
+
delayMs: delay
|
|
376
|
+
});
|
|
377
|
+
if (delay > 0) await sleep(delay);
|
|
378
|
+
}
|
|
379
|
+
throw createFailure(attemptErrors);
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
createRetryRunner();
|
|
383
|
+
//#endregion
|
|
384
|
+
//#region packages/gateway-client/src/protocol-client.ts
|
|
385
|
+
var GatewayProtocolRequestError = class extends Error {
|
|
386
|
+
constructor(error) {
|
|
387
|
+
super(error.message ?? "request failed");
|
|
388
|
+
this.name = "GatewayProtocolRequestError";
|
|
389
|
+
this.code = error.code ?? "UNAVAILABLE";
|
|
390
|
+
this.gatewayCode = this.code;
|
|
391
|
+
this.details = error.details;
|
|
392
|
+
this.retryable = error.retryable === true;
|
|
393
|
+
this.retryAfterMs = error.retryAfterMs;
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
/**
|
|
397
|
+
* Browser-safe gateway wire client. Environment adapters own transport and auth
|
|
398
|
+
* policy; this class owns the single socket/handshake/reconnect/frame state machine.
|
|
399
|
+
*/
|
|
400
|
+
var GatewayProtocolClient = class {
|
|
401
|
+
constructor(opts) {
|
|
402
|
+
this.opts = opts;
|
|
403
|
+
this.socket = null;
|
|
404
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
405
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
406
|
+
this.stopped = true;
|
|
407
|
+
this.generation = 0;
|
|
408
|
+
this.lastSeq = null;
|
|
409
|
+
this.connectNonce = null;
|
|
410
|
+
this.connectSent = false;
|
|
411
|
+
this.connectRequestSent = false;
|
|
412
|
+
this.handshakeTimer = null;
|
|
413
|
+
this.socketOpened = false;
|
|
414
|
+
this.helloReceived = false;
|
|
415
|
+
this.connectTiming = null;
|
|
416
|
+
this.reconnectSupervisor = new RetrySupervisor({
|
|
417
|
+
initialMs: opts.reconnect.initialMs,
|
|
418
|
+
maxMs: opts.reconnect.maxMs,
|
|
419
|
+
factor: opts.reconnect.multiplier,
|
|
420
|
+
jitter: 0
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
get connected() {
|
|
424
|
+
return this.socket?.isOpen() ?? false;
|
|
425
|
+
}
|
|
426
|
+
get hasPendingRequests() {
|
|
427
|
+
return this.pending.size > 0;
|
|
428
|
+
}
|
|
429
|
+
get connecting() {
|
|
430
|
+
return this.connectSent && !this.helloReceived;
|
|
431
|
+
}
|
|
432
|
+
get hasUnboundedPendingRequests() {
|
|
433
|
+
return [...this.pending.values()].some((pending) => pending.unbounded);
|
|
434
|
+
}
|
|
435
|
+
start() {
|
|
436
|
+
this.stopped = false;
|
|
437
|
+
this.reconnectSupervisor.cancel();
|
|
438
|
+
this.connect();
|
|
439
|
+
}
|
|
440
|
+
stop() {
|
|
441
|
+
this.stopped = true;
|
|
442
|
+
this.clearHandshakeTimer();
|
|
443
|
+
this.reconnectSupervisor.reset();
|
|
444
|
+
const socket = this.socket;
|
|
445
|
+
if (socket && this.opts.notifyStoppedClose) this.stoppedSocket = {
|
|
446
|
+
socket,
|
|
447
|
+
context: this.closeContext()
|
|
448
|
+
};
|
|
449
|
+
this.socket = null;
|
|
450
|
+
this.connectFailure = void 0;
|
|
451
|
+
this.connectTiming = null;
|
|
452
|
+
this.flushRequests(/* @__PURE__ */ new Error("gateway client stopped"));
|
|
453
|
+
socket?.close();
|
|
454
|
+
}
|
|
455
|
+
request(method, params, options) {
|
|
456
|
+
const socket = this.socket;
|
|
457
|
+
if (!socket?.isOpen()) return Promise.reject(/* @__PURE__ */ new Error("gateway not connected"));
|
|
458
|
+
if (typeof method !== "string" || method.length === 0) return Promise.reject(/* @__PURE__ */ new Error("invalid request frame: method must be a non-empty string"));
|
|
459
|
+
const id = this.opts.createRequestId();
|
|
460
|
+
const timeoutMs = options?.timeoutMs === null ? void 0 : options?.timeoutMs ?? this.opts.requestTimeoutMs;
|
|
461
|
+
return new Promise((resolve, reject) => {
|
|
462
|
+
let timeout;
|
|
463
|
+
const pending = {
|
|
464
|
+
resolve: (value) => resolve(value),
|
|
465
|
+
reject,
|
|
466
|
+
expectFinal: options?.expectFinal === true,
|
|
467
|
+
acceptedNotified: false,
|
|
468
|
+
onAccepted: options?.onAccepted,
|
|
469
|
+
unbounded: timeoutMs === void 0,
|
|
470
|
+
method,
|
|
471
|
+
startedAtMs: this.nowMs()
|
|
472
|
+
};
|
|
473
|
+
const onAbort = () => {
|
|
474
|
+
this.pending.delete(id);
|
|
475
|
+
if (timeout) clearTimeout(timeout);
|
|
476
|
+
this.finishRequestTiming(id, pending, false, "CLIENT_ABORTED");
|
|
477
|
+
reject(this.opts.createRequestAbortError?.(method) ?? /* @__PURE__ */ new Error(`gateway request aborted for ${method}`));
|
|
478
|
+
};
|
|
479
|
+
const cleanup = () => {
|
|
480
|
+
if (timeout) clearTimeout(timeout);
|
|
481
|
+
options?.signal?.removeEventListener("abort", onAbort);
|
|
482
|
+
};
|
|
483
|
+
if (options?.signal?.aborted) {
|
|
484
|
+
reject(this.opts.createRequestAbortError?.(method) ?? /* @__PURE__ */ new Error(`gateway request aborted for ${method}`));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
pending.cleanup = cleanup;
|
|
488
|
+
if (timeoutMs !== void 0 && timeoutMs >= 0) {
|
|
489
|
+
timeout = setTimeout(() => {
|
|
490
|
+
this.pending.delete(id);
|
|
491
|
+
options?.signal?.removeEventListener("abort", onAbort);
|
|
492
|
+
this.finishRequestTiming(id, pending, false, "CLIENT_TIMEOUT");
|
|
493
|
+
reject(this.opts.createRequestTimeoutError?.(method, timeoutMs) ?? /* @__PURE__ */ new Error(`gateway request timed out after ${timeoutMs}ms: ${method}`));
|
|
494
|
+
}, timeoutMs);
|
|
495
|
+
timeout.unref?.();
|
|
496
|
+
}
|
|
497
|
+
options?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
498
|
+
this.pending.set(id, pending);
|
|
499
|
+
try {
|
|
500
|
+
socket.send(JSON.stringify({
|
|
501
|
+
type: "req",
|
|
502
|
+
id,
|
|
503
|
+
method,
|
|
504
|
+
params
|
|
505
|
+
}));
|
|
506
|
+
this.invoke("sent", () => options?.onSent?.());
|
|
507
|
+
} catch (error) {
|
|
508
|
+
this.pending.delete(id);
|
|
509
|
+
cleanup();
|
|
510
|
+
this.finishRequestTiming(id, pending, false, "CLIENT_SEND_ERROR");
|
|
511
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
addEventListener(listener) {
|
|
516
|
+
this.listeners.add(listener);
|
|
517
|
+
return () => this.listeners.delete(listener);
|
|
518
|
+
}
|
|
519
|
+
closeSocket(code, reason) {
|
|
520
|
+
this.socket?.close(code, reason);
|
|
521
|
+
}
|
|
522
|
+
resetReconnectBackoff(initialMs) {
|
|
523
|
+
this.reconnectSupervisor.reset(initialMs);
|
|
524
|
+
}
|
|
525
|
+
recordTiming(phase, generation, plan, detail) {
|
|
526
|
+
const now = this.nowMs();
|
|
527
|
+
const state = this.connectTiming;
|
|
528
|
+
if (!state || state.generation !== generation) return;
|
|
529
|
+
state.hasChallenge ||= phase === "challenge";
|
|
530
|
+
state.usedFallback ||= phase === "fallback";
|
|
531
|
+
this.invoke("connect timing", () => this.opts.onTiming?.({
|
|
532
|
+
phase,
|
|
533
|
+
generation,
|
|
534
|
+
durationMs: Math.max(0, now - state.startedAtMs),
|
|
535
|
+
phaseDurationMs: Math.max(0, now - state.lastAtMs),
|
|
536
|
+
hasChallenge: state.hasChallenge,
|
|
537
|
+
usedFallback: state.usedFallback,
|
|
538
|
+
plan,
|
|
539
|
+
detail
|
|
540
|
+
}));
|
|
541
|
+
state.lastAtMs = now;
|
|
542
|
+
if (phase === "hello" || phase === "failed") this.connectTiming = null;
|
|
543
|
+
}
|
|
544
|
+
connect() {
|
|
545
|
+
if (this.stopped) return;
|
|
546
|
+
const generation = this.generation + 1;
|
|
547
|
+
this.connectNonce = null;
|
|
548
|
+
this.connectSent = false;
|
|
549
|
+
this.connectRequestSent = false;
|
|
550
|
+
this.socketOpened = false;
|
|
551
|
+
this.helloReceived = false;
|
|
552
|
+
this.connectFailure = void 0;
|
|
553
|
+
let socket;
|
|
554
|
+
try {
|
|
555
|
+
socket = this.opts.createSocket({
|
|
556
|
+
open: () => this.handleOpen(socket, generation),
|
|
557
|
+
message: (data) => this.handleMessage(socket, generation, data),
|
|
558
|
+
close: (code, reason) => this.handleClose(socket, generation, code, reason),
|
|
559
|
+
error: (error) => this.handleSocketError(socket, generation, error)
|
|
560
|
+
});
|
|
561
|
+
} catch (error) {
|
|
562
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
563
|
+
this.opts.onSocketFactoryError?.(normalized);
|
|
564
|
+
this.opts.onConnectError?.(normalized);
|
|
565
|
+
if (this.opts.rethrowSocketFactoryError?.(normalized)) throw normalized;
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
this.generation = generation;
|
|
569
|
+
this.socket = socket;
|
|
570
|
+
const now = this.nowMs();
|
|
571
|
+
this.connectTiming = {
|
|
572
|
+
generation,
|
|
573
|
+
startedAtMs: now,
|
|
574
|
+
lastAtMs: now,
|
|
575
|
+
hasChallenge: false,
|
|
576
|
+
usedFallback: false
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
handleOpen(socket, generation) {
|
|
580
|
+
if (!this.isActive(socket, generation)) return;
|
|
581
|
+
this.socketOpened = true;
|
|
582
|
+
this.recordTiming("socket-open", generation);
|
|
583
|
+
if (this.connectNonce) {
|
|
584
|
+
this.sendConnect(socket, generation);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
this.armHandshakeTimer(socket, generation);
|
|
588
|
+
}
|
|
589
|
+
armHandshakeTimer(socket, generation) {
|
|
590
|
+
this.clearHandshakeTimer();
|
|
591
|
+
const armedAt = Date.now();
|
|
592
|
+
this.handshakeTimer = setTimeout(() => {
|
|
593
|
+
this.handshakeTimer = null;
|
|
594
|
+
if (!this.isActive(socket, generation) || this.connectSent || !socket.isOpen()) return;
|
|
595
|
+
if (this.opts.handshake.mode === "fallback") {
|
|
596
|
+
this.recordTiming("fallback", generation);
|
|
597
|
+
this.sendConnect(socket, generation);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const elapsedMs = Date.now() - armedAt;
|
|
601
|
+
const error = new Error(this.opts.handshake.timeoutMessage?.(elapsedMs) ?? `gateway connect challenge timeout after ${elapsedMs}ms`);
|
|
602
|
+
this.opts.onConnectError?.(error);
|
|
603
|
+
socket.close(1008, "connect challenge timeout");
|
|
604
|
+
}, this.opts.handshake.timeoutMs);
|
|
605
|
+
this.handshakeTimer.unref?.();
|
|
606
|
+
}
|
|
607
|
+
sendConnect(socket, generation) {
|
|
608
|
+
if (!this.isActive(socket, generation) || !socket.isOpen() || this.connectSent) return;
|
|
609
|
+
this.connectSent = true;
|
|
610
|
+
this.clearHandshakeTimer();
|
|
611
|
+
let planOrPromise;
|
|
612
|
+
try {
|
|
613
|
+
planOrPromise = this.opts.buildConnectPlan({
|
|
614
|
+
nonce: this.connectNonce,
|
|
615
|
+
generation
|
|
616
|
+
});
|
|
617
|
+
} catch (error) {
|
|
618
|
+
this.handleConnectPlanError(socket, generation, error);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (planOrPromise instanceof Promise) {
|
|
622
|
+
planOrPromise.then((plan) => this.sendConnectPlan(socket, generation, plan)).catch((error) => this.handleConnectPlanError(socket, generation, error));
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
this.sendConnectPlan(socket, generation, planOrPromise);
|
|
626
|
+
}
|
|
627
|
+
handleConnectPlanError(socket, generation, error) {
|
|
628
|
+
if (!this.isActive(socket, generation)) return;
|
|
629
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
630
|
+
const outcome = this.opts.onConnectPlanError?.(normalized) ?? {
|
|
631
|
+
closeCode: 1008,
|
|
632
|
+
closeReason: "connect failed"
|
|
633
|
+
};
|
|
634
|
+
this.opts.onConnectError?.(outcome.error ?? normalized);
|
|
635
|
+
if (outcome.stop) this.stopped = true;
|
|
636
|
+
socket.close(outcome.closeCode, outcome.closeReason);
|
|
637
|
+
}
|
|
638
|
+
sendConnectPlan(socket, generation, plan) {
|
|
639
|
+
if (!this.isActive(socket, generation) || !socket.isOpen()) return;
|
|
640
|
+
const context = {
|
|
641
|
+
generation,
|
|
642
|
+
nonce: this.connectNonce,
|
|
643
|
+
plan
|
|
644
|
+
};
|
|
645
|
+
this.recordTiming("connect-plan-ready", generation, plan);
|
|
646
|
+
this.recordTiming("request-sent", generation, plan);
|
|
647
|
+
this.connectRequestSent = true;
|
|
648
|
+
this.request("connect", this.opts.buildConnectParams(plan)).then((hello) => {
|
|
649
|
+
if (!this.isActive(socket, generation)) return;
|
|
650
|
+
this.helloReceived = true;
|
|
651
|
+
this.connectFailure = void 0;
|
|
652
|
+
this.reconnectSupervisor.reset();
|
|
653
|
+
this.recordTiming("hello", generation, plan);
|
|
654
|
+
this.opts.onConnectHello?.(hello, context);
|
|
655
|
+
this.invoke("hello", () => this.opts.onHello?.(hello));
|
|
656
|
+
}).catch((error) => {
|
|
657
|
+
if (!this.isActive(socket, generation)) return;
|
|
658
|
+
const requestError = error instanceof GatewayProtocolRequestError ? error : new GatewayProtocolRequestError({ message: String(error) });
|
|
659
|
+
const outcome = this.opts.onConnectFailure?.(requestError, context) ?? {
|
|
660
|
+
closeCode: 1008,
|
|
661
|
+
closeReason: "connect failed"
|
|
662
|
+
};
|
|
663
|
+
this.connectFailure = {
|
|
664
|
+
error: requestError,
|
|
665
|
+
reconnectDelayMs: outcome.reconnectDelayMs
|
|
666
|
+
};
|
|
667
|
+
if (outcome.stop) this.stopped = true;
|
|
668
|
+
socket.close(outcome.closeCode, outcome.closeReason);
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
handleMessage(socket, generation, raw) {
|
|
672
|
+
if (!this.isActive(socket, generation)) return;
|
|
673
|
+
let parsed;
|
|
674
|
+
try {
|
|
675
|
+
parsed = JSON.parse(raw);
|
|
676
|
+
} catch (error) {
|
|
677
|
+
this.opts.onParseError?.(error);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
if (isGatewayEventFrame(parsed)) {
|
|
681
|
+
this.opts.onActivity?.();
|
|
682
|
+
if (parsed.event === "connect.challenge") {
|
|
683
|
+
const payload = parsed.payload;
|
|
684
|
+
const nonce = typeof payload?.nonce === "string" ? payload.nonce.trim() : "";
|
|
685
|
+
if (!nonce) {
|
|
686
|
+
if (this.opts.handshake.mode === "require-challenge") {
|
|
687
|
+
const error = /* @__PURE__ */ new Error("gateway connect challenge missing nonce");
|
|
688
|
+
this.opts.onConnectError?.(error);
|
|
689
|
+
socket.close(1008, "connect challenge missing nonce");
|
|
690
|
+
}
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
this.connectNonce = nonce;
|
|
694
|
+
this.recordTiming("challenge", generation);
|
|
695
|
+
this.sendConnect(socket, generation);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const seq = typeof parsed.seq === "number" ? parsed.seq : null;
|
|
699
|
+
if (seq !== null) {
|
|
700
|
+
if (this.lastSeq !== null && seq > this.lastSeq + 1) {
|
|
701
|
+
const expected = this.lastSeq + 1;
|
|
702
|
+
this.invoke("gap", () => this.opts.onGap?.({
|
|
703
|
+
expected,
|
|
704
|
+
received: seq
|
|
705
|
+
}));
|
|
706
|
+
}
|
|
707
|
+
this.lastSeq = seq;
|
|
708
|
+
}
|
|
709
|
+
this.invoke("event", () => this.opts.onEvent?.(parsed));
|
|
710
|
+
for (const listener of this.listeners) this.invoke("event listener", () => listener(parsed));
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (!isGatewayResponseFrame(parsed)) return;
|
|
714
|
+
this.opts.onActivity?.();
|
|
715
|
+
this.handleResponse(parsed);
|
|
716
|
+
}
|
|
717
|
+
handleResponse(frame) {
|
|
718
|
+
const pending = this.pending.get(frame.id);
|
|
719
|
+
if (!pending) return;
|
|
720
|
+
const status = frame.payload?.status;
|
|
721
|
+
if (pending.expectFinal && status === "accepted") {
|
|
722
|
+
if (!pending.acceptedNotified) {
|
|
723
|
+
pending.acceptedNotified = true;
|
|
724
|
+
this.invoke("accepted", () => pending.onAccepted?.(frame.payload));
|
|
725
|
+
}
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
this.pending.delete(frame.id);
|
|
729
|
+
pending.cleanup?.();
|
|
730
|
+
if (frame.ok) {
|
|
731
|
+
this.finishRequestTiming(frame.id, pending, true);
|
|
732
|
+
pending.resolve(frame.payload);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
this.finishRequestTiming(frame.id, pending, false, frame.error?.code);
|
|
736
|
+
pending.reject(this.opts.createRequestError?.(frame.error ?? {}) ?? new GatewayProtocolRequestError(frame.error ?? {}));
|
|
737
|
+
}
|
|
738
|
+
handleClose(socket, generation, code, reason) {
|
|
739
|
+
if (this.socket !== socket) {
|
|
740
|
+
if (this.stoppedSocket?.socket === socket) {
|
|
741
|
+
const context = {
|
|
742
|
+
...this.stoppedSocket.context,
|
|
743
|
+
code,
|
|
744
|
+
reason
|
|
745
|
+
};
|
|
746
|
+
this.stoppedSocket = void 0;
|
|
747
|
+
this.invoke("close", () => this.opts.onClose?.(context, {
|
|
748
|
+
retry: false,
|
|
749
|
+
notify: true
|
|
750
|
+
}));
|
|
751
|
+
}
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
this.socket = null;
|
|
755
|
+
this.clearHandshakeTimer();
|
|
756
|
+
const context = {
|
|
757
|
+
...this.closeContext(),
|
|
758
|
+
code,
|
|
759
|
+
reason,
|
|
760
|
+
generation
|
|
761
|
+
};
|
|
762
|
+
this.connectFailure = void 0;
|
|
763
|
+
const decision = this.opts.resolveClose(context);
|
|
764
|
+
this.flushRequests(decision.pendingError ?? context.connectFailure?.error ?? /* @__PURE__ */ new Error(`gateway closed (${code}): ${reason}`));
|
|
765
|
+
this.invoke("close", () => this.opts.onClose?.(context, decision));
|
|
766
|
+
if (decision.retry && !this.stopped) this.scheduleReconnect(decision.reconnectDelayMs ?? context.connectFailure?.reconnectDelayMs);
|
|
767
|
+
}
|
|
768
|
+
handleSocketError(socket, generation, error) {
|
|
769
|
+
if (!this.isActive(socket, generation) || this.connectSent) return;
|
|
770
|
+
this.opts.onConnectError?.(error);
|
|
771
|
+
}
|
|
772
|
+
flushRequests(error) {
|
|
773
|
+
for (const [id, pending] of this.pending) {
|
|
774
|
+
this.finishRequestTiming(id, pending, false, "CLIENT_CLOSED");
|
|
775
|
+
pending.cleanup?.();
|
|
776
|
+
pending.reject(error);
|
|
777
|
+
}
|
|
778
|
+
this.pending.clear();
|
|
779
|
+
}
|
|
780
|
+
finishRequestTiming(id, pending, ok, errorCode) {
|
|
781
|
+
const endedAtMs = this.nowMs();
|
|
782
|
+
this.invoke("request timing", () => this.opts.onRequestTiming?.({
|
|
783
|
+
id,
|
|
784
|
+
method: pending.method,
|
|
785
|
+
ok,
|
|
786
|
+
durationMs: Math.max(0, endedAtMs - pending.startedAtMs),
|
|
787
|
+
startedAtMs: pending.startedAtMs,
|
|
788
|
+
endedAtMs,
|
|
789
|
+
errorCode
|
|
790
|
+
}));
|
|
791
|
+
}
|
|
792
|
+
scheduleReconnect(overrideMs) {
|
|
793
|
+
if (overrideMs !== void 0) this.reconnectSupervisor.nextDelayOverrideMs = overrideMs;
|
|
794
|
+
const retry = this.reconnectSupervisor.next();
|
|
795
|
+
if (!retry) return;
|
|
796
|
+
sleepWithAbort(retry.delayMs, retry.signal).then(() => this.connect(), () => {});
|
|
797
|
+
}
|
|
798
|
+
closeContext() {
|
|
799
|
+
return {
|
|
800
|
+
generation: this.generation,
|
|
801
|
+
socketOpened: this.socketOpened,
|
|
802
|
+
helloReceived: this.helloReceived,
|
|
803
|
+
connectRequestSent: this.connectRequestSent,
|
|
804
|
+
connectFailure: this.connectFailure
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
isActive(socket, generation) {
|
|
808
|
+
return !this.stopped && this.socket === socket && this.generation === generation;
|
|
809
|
+
}
|
|
810
|
+
nowMs() {
|
|
811
|
+
return this.opts.nowMs?.() ?? Date.now();
|
|
812
|
+
}
|
|
813
|
+
clearHandshakeTimer() {
|
|
814
|
+
if (this.handshakeTimer) {
|
|
815
|
+
clearTimeout(this.handshakeTimer);
|
|
816
|
+
this.handshakeTimer = null;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
invoke(label, callback) {
|
|
820
|
+
try {
|
|
821
|
+
callback();
|
|
822
|
+
} catch (error) {
|
|
823
|
+
this.opts.onCallbackError?.(label, error);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
//#endregion
|
|
828
|
+
//#region packages/gateway-client/src/reconnect-policy.ts
|
|
829
|
+
const NON_RECOVERABLE_AUTH_ERRORS = /* @__PURE__ */ new Set([
|
|
830
|
+
ConnectErrorDetailCodes.AUTH_TOKEN_MISSING,
|
|
831
|
+
ConnectErrorDetailCodes.AUTH_BOOTSTRAP_TOKEN_INVALID,
|
|
832
|
+
ConnectErrorDetailCodes.AUTH_PASSWORD_MISSING,
|
|
833
|
+
ConnectErrorDetailCodes.AUTH_PASSWORD_MISMATCH,
|
|
834
|
+
ConnectErrorDetailCodes.AUTH_RATE_LIMITED,
|
|
835
|
+
ConnectErrorDetailCodes.AUTH_DEVICE_TOKEN_MISMATCH,
|
|
836
|
+
ConnectErrorDetailCodes.AUTH_SCOPE_MISMATCH,
|
|
837
|
+
ConnectErrorDetailCodes.PAIRING_REQUIRED,
|
|
838
|
+
ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED,
|
|
839
|
+
ConnectErrorDetailCodes.DEVICE_IDENTITY_REQUIRED
|
|
840
|
+
]);
|
|
841
|
+
function shouldPauseGatewayReconnect(params) {
|
|
842
|
+
const code = readConnectErrorDetailCode(params.details);
|
|
843
|
+
if (!code) return false;
|
|
844
|
+
const pairing = readPairingConnectErrorDetails(params.details);
|
|
845
|
+
if (code === ConnectErrorDetailCodes.PAIRING_REQUIRED && (pairing?.pauseReconnect === false || pairing?.recommendedNextStep === "wait_then_retry")) return false;
|
|
846
|
+
if (code === ConnectErrorDetailCodes.AUTH_TOKEN_MISMATCH) return params.tokenMismatchIsTerminal === true && !params.deviceTokenRetryPending;
|
|
847
|
+
return NON_RECOVERABLE_AUTH_ERRORS.has(code) || params.protocolMismatchIsTerminal === true && code === ConnectErrorDetailCodes.PROTOCOL_MISMATCH || params.clientVersionMismatchIsTerminal === true && code === ConnectErrorDetailCodes.CLIENT_VERSION_MISMATCH;
|
|
848
|
+
}
|
|
849
|
+
//#endregion
|
|
850
|
+
export { buildGatewayConnectAuth as a, shouldRetryGatewayWithDeviceToken as c, normalizeDeviceMetadataForAuth as d, GatewayBrowserDeviceAuthLifecycle as i, buildDeviceAuthPayload as l, GatewayProtocolClient as n, resolveGatewayConnectScopes as o, GatewayProtocolRequestError as r, selectGatewayConnectAuth as s, shouldPauseGatewayReconnect as t, buildDeviceAuthPayloadV3 as u };
|