@camera.ui/transport 0.0.30 → 0.1.1
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/closeCodes-CltSRjdJ.js +30 -0
- package/dist/core/types.d.ts +1 -0
- package/dist/effects/backgroundProbe.d.ts +1 -0
- package/dist/effects/crossTab.d.ts +6 -0
- package/dist/effects/degradedRecovery.d.ts +1 -0
- package/dist/effects/persistence.d.ts +2 -0
- package/dist/effects/probeLoop.d.ts +1 -0
- package/dist/effects/tokenLifecycle.d.ts +8 -3
- package/dist/effects/workerBridge.d.ts +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +379 -84
- package/dist/signal.d.ts +2 -0
- package/dist/testing/fakeTransport.d.ts +2 -1
- package/dist/testing.js +7 -3
- package/dist/transports/closeCodes.d.ts +2 -0
- package/dist/transports/contract.d.ts +2 -0
- package/dist/transports/http.js +31 -14
- package/dist/transports/nativeHttp.d.ts +3 -1
- package/dist/transports/nativeHttp.js +118 -14
- package/dist/transports/nats.js +185 -86
- package/dist/transports/socketio.d.ts +4 -1
- package/dist/transports/socketio.js +168 -67
- package/dist/transports/ws.d.ts +0 -1
- package/dist/transports/ws.js +39 -13
- package/package.json +14 -13
- package/dist/closeCodes-ClWJVOpq.js +0 -14
package/dist/transports/nats.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { n as refusedByServer, t as classifyClose } from "../closeCodes-CltSRjdJ.js";
|
|
1
2
|
import { n as isEndpointChange, r as isSameTarget, t as TransportEmitter } from "../contract-dt1m8U-o.js";
|
|
2
|
-
import { t as classifyClose } from "../closeCodes-ClWJVOpq.js";
|
|
3
3
|
import { createRPCClient } from "@camera.ui/rpc";
|
|
4
4
|
//#region src/transports/nats.ts
|
|
5
5
|
var NATS_SPEC = {
|
|
@@ -8,6 +8,8 @@ var NATS_SPEC = {
|
|
|
8
8
|
phaseGating: true
|
|
9
9
|
};
|
|
10
10
|
var RESOLVE_RETRY_MIN_MS = 3e3;
|
|
11
|
+
var DIAL_GRACE_MS = 2e3;
|
|
12
|
+
var CLIENT_DEFAULT_PING_INTERVAL_MS = 12e4;
|
|
11
13
|
function newConnId() {
|
|
12
14
|
const c = globalThis.crypto;
|
|
13
15
|
if (c?.randomUUID) return c.randomUUID();
|
|
@@ -35,6 +37,8 @@ function createNatsTransport(options = {}) {
|
|
|
35
37
|
const pingInterval = options.pingInterval ?? 25e3;
|
|
36
38
|
const pingTimeout = options.pingTimeout ?? 2e4;
|
|
37
39
|
const maxPingOut = options.maxPingOut ?? 1;
|
|
40
|
+
const heartbeatInterval = pingInterval || CLIENT_DEFAULT_PING_INTERVAL_MS;
|
|
41
|
+
const heartbeatWindow = heartbeatInterval + (pingTimeout > 0 ? pingTimeout : heartbeatInterval);
|
|
38
42
|
const logger = options.logger;
|
|
39
43
|
const resolveServers = options.resolveServers;
|
|
40
44
|
const resolveRefreshMs = options.resolveRefreshMs ?? 0;
|
|
@@ -48,16 +52,19 @@ function createNatsTransport(options = {}) {
|
|
|
48
52
|
let disposed = false;
|
|
49
53
|
let connId = null;
|
|
50
54
|
let applyEpoch = 0;
|
|
51
|
-
let
|
|
55
|
+
let dialAbort = null;
|
|
56
|
+
let dialAttempt = null;
|
|
57
|
+
let parked = false;
|
|
52
58
|
let poolRefreshAt = 0;
|
|
53
59
|
let poolRefreshing = false;
|
|
60
|
+
let dialAccess;
|
|
61
|
+
let probedUntil = 0;
|
|
54
62
|
function buildServers(target) {
|
|
55
63
|
const url = new URL(target.endpoint.url);
|
|
56
64
|
const wsProtocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
57
65
|
const port = url.port || (url.protocol === "https:" ? "443" : "80");
|
|
58
66
|
const prefix = url.pathname.replace(/\/$/, "");
|
|
59
67
|
const params = new URLSearchParams({ token: target.tokens.access });
|
|
60
|
-
if (target.tokens.proxySession) params.set("session", target.tokens.proxySession);
|
|
61
68
|
if (connId) params.set("connId", connId);
|
|
62
69
|
return [`${wsProtocol}//${url.hostname}:${port}${prefix}${proxyPath}?${params.toString()}`];
|
|
63
70
|
}
|
|
@@ -171,13 +178,14 @@ function createNatsTransport(options = {}) {
|
|
|
171
178
|
})().catch(() => {});
|
|
172
179
|
}
|
|
173
180
|
function handleErrorEvent(event) {
|
|
174
|
-
const
|
|
181
|
+
const payload = event.error ?? event.data;
|
|
182
|
+
const message = stringifyError(payload ?? "server error");
|
|
175
183
|
if (message.toLowerCase().includes("permissions violation")) {
|
|
176
184
|
logger?.warn(`permission violation (connection stays up): ${message}`);
|
|
177
185
|
return;
|
|
178
186
|
}
|
|
179
|
-
const cls = classifyClose(typeof
|
|
180
|
-
...
|
|
187
|
+
const cls = classifyClose(typeof payload === "object" && payload !== null ? {
|
|
188
|
+
...payload,
|
|
181
189
|
message
|
|
182
190
|
} : message);
|
|
183
191
|
if (cls === "auth-expired") {
|
|
@@ -185,102 +193,172 @@ function createNatsTransport(options = {}) {
|
|
|
185
193
|
resign(message);
|
|
186
194
|
return;
|
|
187
195
|
}
|
|
188
|
-
emitter.emit("auth-error", {
|
|
196
|
+
emitter.emit("auth-error", {
|
|
197
|
+
message,
|
|
198
|
+
access: dialAccess
|
|
199
|
+
});
|
|
189
200
|
return;
|
|
190
201
|
}
|
|
191
202
|
if (cls === "forbidden") {
|
|
203
|
+
if (refusedByServer(payload)) {
|
|
204
|
+
park(message);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
192
207
|
logger?.warn(`forbidden by server: ${message}`);
|
|
193
|
-
markDown(
|
|
208
|
+
markDown(forbiddenError(message));
|
|
194
209
|
return;
|
|
195
210
|
}
|
|
196
211
|
markDown(message);
|
|
197
212
|
}
|
|
213
|
+
function park(message) {
|
|
214
|
+
parked = true;
|
|
215
|
+
const dropped = proxy;
|
|
216
|
+
if (dropped) {
|
|
217
|
+
stopStatusMonitor();
|
|
218
|
+
stopRefresh();
|
|
219
|
+
proxy = null;
|
|
220
|
+
try {
|
|
221
|
+
dropped.abortClose();
|
|
222
|
+
} catch {}
|
|
223
|
+
}
|
|
224
|
+
logger?.warn(`forbidden by server: ${message}`);
|
|
225
|
+
markDown(forbiddenError(message));
|
|
226
|
+
if (dropped) notifyClient();
|
|
227
|
+
}
|
|
198
228
|
async function rebuildClient(target, epoch) {
|
|
199
229
|
logger?.debug(`rebuildClient start (epoch=${epoch})`);
|
|
200
|
-
const t0 = Date.now();
|
|
201
230
|
stopStatusMonitor();
|
|
202
231
|
stopRefresh();
|
|
203
|
-
|
|
232
|
+
dialAbort?.abort();
|
|
233
|
+
parked = false;
|
|
204
234
|
if (proxy) {
|
|
205
235
|
try {
|
|
206
236
|
proxy.abortClose();
|
|
207
237
|
} catch {}
|
|
208
238
|
proxy = null;
|
|
239
|
+
markDown("endpoint-changed");
|
|
209
240
|
notifyClient();
|
|
210
241
|
}
|
|
211
|
-
|
|
212
|
-
|
|
242
|
+
const abort = new AbortController();
|
|
243
|
+
dialAbort = abort;
|
|
213
244
|
try {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
245
|
+
for (let retry = 0;; retry++) {
|
|
246
|
+
if (retry > 0) {
|
|
247
|
+
const wait = retryDelay(retry - 1);
|
|
248
|
+
logger?.debug(`rebuildClient retry in ${wait}ms`);
|
|
249
|
+
await sleep(wait, abort.signal);
|
|
250
|
+
if (disposed || epoch !== applyEpoch) return;
|
|
251
|
+
}
|
|
252
|
+
if (await dialOnce(target, epoch, abort.signal)) return;
|
|
253
|
+
}
|
|
254
|
+
} finally {
|
|
255
|
+
if (dialAbort === abort) dialAbort = null;
|
|
221
256
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
reconnect: true,
|
|
232
|
-
maxReconnectAttempts: -1,
|
|
233
|
-
reconnectTimeWait,
|
|
234
|
-
reconnectionDelayMax,
|
|
235
|
-
reconnectionRandomizationFactor,
|
|
236
|
-
timeout,
|
|
237
|
-
connectTimeout,
|
|
238
|
-
pingInterval,
|
|
239
|
-
pingTimeout,
|
|
240
|
-
maxPingOut,
|
|
241
|
-
ignoreAuthErrorAbort: true
|
|
242
|
-
});
|
|
243
|
-
const connectAbort = new AbortController();
|
|
244
|
-
pendingConnectAbort = connectAbort;
|
|
257
|
+
}
|
|
258
|
+
async function dialOnce(target, epoch, signal) {
|
|
259
|
+
const t0 = Date.now();
|
|
260
|
+
let settle;
|
|
261
|
+
const attempt = {
|
|
262
|
+
startedAt: t0,
|
|
263
|
+
settled: new Promise((resolve) => settle = resolve)
|
|
264
|
+
};
|
|
265
|
+
dialAttempt = attempt;
|
|
245
266
|
try {
|
|
246
|
-
|
|
247
|
-
|
|
267
|
+
connId = newConnId();
|
|
268
|
+
let servers;
|
|
248
269
|
try {
|
|
249
|
-
|
|
250
|
-
} catch {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
270
|
+
servers = await serversFor(target);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
if (disposed || epoch !== applyEpoch) return true;
|
|
273
|
+
const msg = stringifyError(err);
|
|
274
|
+
logger?.debug(`rebuildClient resolve FAILED: ${msg}`);
|
|
275
|
+
markDown(msg);
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
if (disposed || epoch !== applyEpoch) return true;
|
|
279
|
+
dialAccess = target.tokens.access;
|
|
280
|
+
const next = createRPCClient({
|
|
281
|
+
servers,
|
|
282
|
+
name: clientName,
|
|
283
|
+
connId,
|
|
284
|
+
auth: {
|
|
285
|
+
user: natsUser,
|
|
286
|
+
password: natsPassword
|
|
287
|
+
},
|
|
288
|
+
reconnect: true,
|
|
289
|
+
maxReconnectAttempts: -1,
|
|
290
|
+
reconnectTimeWait,
|
|
291
|
+
reconnectionDelayMax,
|
|
292
|
+
reconnectionRandomizationFactor,
|
|
293
|
+
timeout,
|
|
294
|
+
connectTimeout,
|
|
295
|
+
pingInterval,
|
|
296
|
+
pingTimeout,
|
|
297
|
+
maxPingOut,
|
|
298
|
+
ignoreAuthErrorAbort: true,
|
|
299
|
+
waitOnFirstConnect: false
|
|
300
|
+
});
|
|
261
301
|
try {
|
|
262
|
-
next.
|
|
263
|
-
} catch {
|
|
264
|
-
|
|
302
|
+
await next.connect({ signal });
|
|
303
|
+
} catch (err) {
|
|
304
|
+
try {
|
|
305
|
+
next.abortClose();
|
|
306
|
+
} catch {}
|
|
307
|
+
if (disposed || epoch !== applyEpoch) return true;
|
|
308
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
309
|
+
logger?.debug(`rebuildClient connect FAILED after ${Date.now() - t0}ms: ${msg}`);
|
|
310
|
+
const cls = classifyClose(err instanceof Error ? err : msg);
|
|
311
|
+
if (cls === "forbidden") {
|
|
312
|
+
park(msg);
|
|
313
|
+
throw err;
|
|
314
|
+
}
|
|
315
|
+
if (cls === "auth-expired" && !resolveServers) {
|
|
316
|
+
parked = true;
|
|
317
|
+
emitter.emit("auth-error", {
|
|
318
|
+
message: msg,
|
|
319
|
+
access: target.tokens.access
|
|
320
|
+
});
|
|
321
|
+
markDown(msg);
|
|
322
|
+
throw err;
|
|
323
|
+
}
|
|
324
|
+
markDown(msg);
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
if (disposed || epoch !== applyEpoch) {
|
|
328
|
+
try {
|
|
329
|
+
next.abortClose();
|
|
330
|
+
} catch {}
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
proxy = next;
|
|
334
|
+
logger?.debug(`rebuildClient connected in ${Date.now() - t0}ms`);
|
|
335
|
+
markUp();
|
|
336
|
+
notifyClient();
|
|
337
|
+
startStatusMonitor(next);
|
|
338
|
+
startRefresh(next);
|
|
339
|
+
return true;
|
|
340
|
+
} finally {
|
|
341
|
+
settle();
|
|
342
|
+
if (dialAttempt === attempt) dialAttempt = null;
|
|
265
343
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
startRefresh(next);
|
|
344
|
+
}
|
|
345
|
+
function retryDelay(retry) {
|
|
346
|
+
const base = reconnectionDelayMax > 0 ? Math.min(reconnectTimeWait * 2 ** retry, reconnectionDelayMax) : reconnectTimeWait;
|
|
347
|
+
const spread = base * reconnectionRandomizationFactor;
|
|
348
|
+
return Math.round(base - spread + Math.random() * 2 * spread);
|
|
272
349
|
}
|
|
273
350
|
async function apply(target) {
|
|
274
351
|
if (disposed) throw new Error("nats-transport disposed");
|
|
275
|
-
if (isSameTarget(currentTarget, target)) return;
|
|
352
|
+
if (isSameTarget(currentTarget, target) && (!target || proxy || dialAbort || parked)) return;
|
|
276
353
|
const epoch = ++applyEpoch;
|
|
277
354
|
const endpointChanged = isEndpointChange(currentTarget, target);
|
|
278
355
|
currentTarget = target;
|
|
279
356
|
if (!target) {
|
|
280
357
|
stopStatusMonitor();
|
|
281
358
|
stopRefresh();
|
|
282
|
-
|
|
283
|
-
|
|
359
|
+
dialAbort?.abort();
|
|
360
|
+
dialAbort = null;
|
|
361
|
+
parked = false;
|
|
284
362
|
if (proxy) {
|
|
285
363
|
try {
|
|
286
364
|
proxy.abortClose();
|
|
@@ -292,20 +370,19 @@ function createNatsTransport(options = {}) {
|
|
|
292
370
|
return;
|
|
293
371
|
}
|
|
294
372
|
if (endpointChanged || !proxy) {
|
|
295
|
-
|
|
296
|
-
await rebuildClient(target, epoch);
|
|
297
|
-
} catch (err) {
|
|
298
|
-
if (epoch === applyEpoch && currentTarget === target) currentTarget = null;
|
|
299
|
-
throw err;
|
|
300
|
-
}
|
|
373
|
+
await rebuildClient(target, epoch);
|
|
301
374
|
return;
|
|
302
375
|
}
|
|
303
376
|
const p = proxy;
|
|
304
377
|
const newServers = await serversFor(target);
|
|
305
|
-
if (
|
|
378
|
+
if (epoch !== applyEpoch) return;
|
|
379
|
+
if (proxy === p) {
|
|
380
|
+
p.setServers(newServers);
|
|
381
|
+
dialAccess = target.tokens.access;
|
|
382
|
+
} else if (parked) await rebuildClient(target, epoch);
|
|
306
383
|
}
|
|
307
384
|
function health() {
|
|
308
|
-
if (status.up && proxy?.isStale) return {
|
|
385
|
+
if (status.up && proxy?.isStale && Date.now() > probedUntil) return {
|
|
309
386
|
up: false,
|
|
310
387
|
lastError: "staleConnection"
|
|
311
388
|
};
|
|
@@ -319,8 +396,8 @@ function createNatsTransport(options = {}) {
|
|
|
319
396
|
applyEpoch++;
|
|
320
397
|
stopStatusMonitor();
|
|
321
398
|
stopRefresh();
|
|
322
|
-
|
|
323
|
-
|
|
399
|
+
dialAbort?.abort();
|
|
400
|
+
dialAbort = null;
|
|
324
401
|
if (proxy) {
|
|
325
402
|
try {
|
|
326
403
|
proxy.abortClose();
|
|
@@ -345,6 +422,7 @@ function createNatsTransport(options = {}) {
|
|
|
345
422
|
const t0 = Date.now();
|
|
346
423
|
try {
|
|
347
424
|
await proxy.flush(timeoutMs);
|
|
425
|
+
probedUntil = Date.now() + heartbeatWindow;
|
|
348
426
|
logger?.debug(`probeAlive OK in ${Date.now() - t0}ms`);
|
|
349
427
|
} catch (err) {
|
|
350
428
|
logger?.debug(`probeAlive FAILED after ${Date.now() - t0}ms: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -365,17 +443,20 @@ function createNatsTransport(options = {}) {
|
|
|
365
443
|
function ensureAlive() {
|
|
366
444
|
if (ensureInflight) return ensureInflight;
|
|
367
445
|
const run = async () => {
|
|
368
|
-
if (disposed || !currentTarget) return health();
|
|
446
|
+
if (disposed || !currentTarget || parked) return health();
|
|
369
447
|
if (!proxy) {
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
await
|
|
373
|
-
|
|
448
|
+
const attempt = dialAttempt;
|
|
449
|
+
if (attempt && Date.now() - attempt.startedAt < DIAL_GRACE_MS) {
|
|
450
|
+
await attempt.settled;
|
|
451
|
+
return health();
|
|
452
|
+
}
|
|
453
|
+
rebuildClient(currentTarget, ++applyEpoch).catch(() => {});
|
|
454
|
+
await dialAttempt?.settled;
|
|
374
455
|
return health();
|
|
375
456
|
}
|
|
376
|
-
if (
|
|
457
|
+
if (status.up) try {
|
|
377
458
|
await probeAlive(3e3);
|
|
378
|
-
return
|
|
459
|
+
return status;
|
|
379
460
|
} catch {}
|
|
380
461
|
try {
|
|
381
462
|
await forceReconnect();
|
|
@@ -400,11 +481,29 @@ function createNatsTransport(options = {}) {
|
|
|
400
481
|
ensureAlive
|
|
401
482
|
};
|
|
402
483
|
}
|
|
484
|
+
function sleep(ms, signal) {
|
|
485
|
+
return new Promise((resolve) => {
|
|
486
|
+
if (signal.aborted) {
|
|
487
|
+
resolve();
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const timer = setTimeout(done, ms);
|
|
491
|
+
signal.addEventListener("abort", done, { once: true });
|
|
492
|
+
function done() {
|
|
493
|
+
clearTimeout(timer);
|
|
494
|
+
signal.removeEventListener("abort", done);
|
|
495
|
+
resolve();
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
function forbiddenError(message) {
|
|
500
|
+
return /^forbidden\b/.test(message) ? message : `forbidden: ${message}`;
|
|
501
|
+
}
|
|
403
502
|
function stringifyError(value) {
|
|
404
503
|
if (value instanceof Error) return value.message;
|
|
405
504
|
if (typeof value === "string") return value;
|
|
406
505
|
try {
|
|
407
|
-
return JSON.stringify(value);
|
|
506
|
+
return JSON.stringify(value) ?? String(value);
|
|
408
507
|
} catch {
|
|
409
508
|
return String(value);
|
|
410
509
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Manager, Socket } from 'socket.io-client';
|
|
2
2
|
import { Logger } from '@camera.ui/logger';
|
|
3
3
|
import { ConnectionTarget, TransportSpec, TransportStatus } from '../core/types.js';
|
|
4
|
-
import { Transport } from './contract.js';
|
|
4
|
+
import { Transport, Unsubscribe } from './contract.js';
|
|
5
5
|
export type { Socket };
|
|
6
6
|
export interface ResolveSocketQueryContext {
|
|
7
7
|
readonly target: ConnectionTarget;
|
|
@@ -20,12 +20,15 @@ export interface SocketioTransportOptions {
|
|
|
20
20
|
readonly reconnectionDelayMax?: number;
|
|
21
21
|
readonly timeout?: number;
|
|
22
22
|
readonly staleAfterMs?: number;
|
|
23
|
+
readonly releaseGraceMs?: number;
|
|
23
24
|
readonly logger?: Logger;
|
|
24
25
|
}
|
|
25
26
|
export interface SocketioTransport extends Transport {
|
|
26
27
|
readonly manager: Manager | null;
|
|
27
28
|
socket(namespace?: string): Socket | null;
|
|
28
29
|
ensureSocket(namespace: string): Socket | null;
|
|
30
|
+
acquire(namespace: string): Unsubscribe;
|
|
31
|
+
onRebuild(handler: () => void): Unsubscribe;
|
|
29
32
|
reviveDeadSockets(): void;
|
|
30
33
|
ensureAlive(): Promise<TransportStatus>;
|
|
31
34
|
}
|