@camera.ui/transport 0.0.29 → 0.1.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/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 +378 -82
- 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.d.ts +1 -0
- package/dist/transports/http.js +70 -3
- 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 +15 -14
- package/dist/closeCodes-ClWJVOpq.js +0 -14
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { 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 {
|
|
3
|
-
import { Manager, io } from "socket.io-client";
|
|
3
|
+
import { Manager } from "socket.io-client";
|
|
4
4
|
//#region src/transports/socketio.ts
|
|
5
5
|
var RESOLVE_RETRY_MIN_MS = 3e3;
|
|
6
|
+
var PROBE_TIMEOUT_MS = 3e3;
|
|
7
|
+
var RELEASE_GRACE_MS = 1e4;
|
|
6
8
|
var SOCKETIO_SPEC = {
|
|
7
9
|
id: "socketio",
|
|
8
10
|
kind: "persistent",
|
|
@@ -22,17 +24,23 @@ function createSocketioTransport(options = {}) {
|
|
|
22
24
|
const logger = options.logger;
|
|
23
25
|
const resolveQuery = options.resolveQuery;
|
|
24
26
|
const resolveRefreshMs = options.resolveRefreshMs ?? 0;
|
|
27
|
+
const releaseGraceMs = options.releaseGraceMs ?? RELEASE_GRACE_MS;
|
|
25
28
|
const emitter = new TransportEmitter();
|
|
26
29
|
const sockets = /* @__PURE__ */ new Map();
|
|
30
|
+
const reopen = /* @__PURE__ */ new Set();
|
|
31
|
+
const rebuildListeners = /* @__PURE__ */ new Set();
|
|
32
|
+
const holds = /* @__PURE__ */ new Map();
|
|
33
|
+
const unheld = /* @__PURE__ */ new Set();
|
|
34
|
+
const releaseTimers = /* @__PURE__ */ new Map();
|
|
27
35
|
let manager = null;
|
|
28
36
|
let currentTarget = null;
|
|
29
|
-
let currentQuery = {};
|
|
30
37
|
let refreshTimer = null;
|
|
31
38
|
let queryRefreshAt = 0;
|
|
32
39
|
let queryRefreshing = false;
|
|
33
40
|
let status = { up: false };
|
|
34
41
|
let disposed = false;
|
|
35
42
|
let applyEpoch = 0;
|
|
43
|
+
let ensureInflight = null;
|
|
36
44
|
const staleAfterMs = options.staleAfterMs ?? 6e4;
|
|
37
45
|
let lastActivityAt = 0;
|
|
38
46
|
function touchActivity() {
|
|
@@ -41,18 +49,16 @@ function createSocketioTransport(options = {}) {
|
|
|
41
49
|
function isStaleNow() {
|
|
42
50
|
return lastActivityAt > 0 && Date.now() - lastActivityAt > staleAfterMs;
|
|
43
51
|
}
|
|
44
|
-
function
|
|
45
|
-
const
|
|
46
|
-
if (
|
|
47
|
-
return
|
|
52
|
+
function isDeadNow() {
|
|
53
|
+
const deadline = (manager?.engine)?._pingTimeoutTime;
|
|
54
|
+
if (typeof deadline === "number") return Date.now() > deadline;
|
|
55
|
+
return isStaleNow();
|
|
48
56
|
}
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
if (target.tokens.proxySession) q.session = target.tokens.proxySession;
|
|
52
|
-
return q;
|
|
57
|
+
function buildAuth(target) {
|
|
58
|
+
return { token: `Bearer ${target.tokens.access}` };
|
|
53
59
|
}
|
|
54
60
|
async function queryFor(target) {
|
|
55
|
-
const base =
|
|
61
|
+
const base = {};
|
|
56
62
|
if (!resolveQuery) return base;
|
|
57
63
|
return resolveQuery({
|
|
58
64
|
target,
|
|
@@ -68,8 +74,7 @@ function createSocketioTransport(options = {}) {
|
|
|
68
74
|
try {
|
|
69
75
|
const query = await queryFor(target);
|
|
70
76
|
if (currentTarget !== target || disposed) return;
|
|
71
|
-
|
|
72
|
-
for (const sock of sockets.values()) sock.io.opts.query = query;
|
|
77
|
+
if (manager) manager.opts.query = query;
|
|
73
78
|
} catch (err) {
|
|
74
79
|
logger?.warn(`query refresh failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
75
80
|
} finally {
|
|
@@ -77,6 +82,14 @@ function createSocketioTransport(options = {}) {
|
|
|
77
82
|
queryRefreshAt = Date.now();
|
|
78
83
|
}
|
|
79
84
|
}
|
|
85
|
+
async function refreshQueryWithin(ms) {
|
|
86
|
+
let timer;
|
|
87
|
+
try {
|
|
88
|
+
await Promise.race([refreshQuery(), new Promise((resolve) => timer = setTimeout(resolve, ms))]);
|
|
89
|
+
} finally {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
80
93
|
function stopRefresh() {
|
|
81
94
|
if (refreshTimer) {
|
|
82
95
|
clearInterval(refreshTimer);
|
|
@@ -103,12 +116,20 @@ function createSocketioTransport(options = {}) {
|
|
|
103
116
|
logger?.debug(`markDown (${reason})`);
|
|
104
117
|
emitter.emit("down", { reason });
|
|
105
118
|
}
|
|
106
|
-
function isAuthError(
|
|
107
|
-
return classifyClose(
|
|
119
|
+
function isAuthError(err) {
|
|
120
|
+
return classifyClose({
|
|
121
|
+
message: err?.message,
|
|
122
|
+
data: err?.data
|
|
123
|
+
}) === "auth-expired";
|
|
124
|
+
}
|
|
125
|
+
function dialedAccess(socket) {
|
|
126
|
+
const token = socket.auth?.token;
|
|
127
|
+
return typeof token === "string" && token.startsWith("Bearer ") ? token.slice(7) : void 0;
|
|
108
128
|
}
|
|
109
129
|
function bindMainSocketEvents(socket) {
|
|
110
130
|
socket.on("connect", () => markUp());
|
|
111
131
|
socket.on("disconnect", (reason) => {
|
|
132
|
+
if (sockets.get(mainNs) !== socket) return;
|
|
112
133
|
markDown(reason);
|
|
113
134
|
if (resolveQuery) refreshQuery();
|
|
114
135
|
});
|
|
@@ -116,8 +137,11 @@ function createSocketioTransport(options = {}) {
|
|
|
116
137
|
const msg = err?.message ?? "connect_error";
|
|
117
138
|
logger?.debug(`connect_error (main): ${msg}`);
|
|
118
139
|
if (resolveQuery) refreshQuery();
|
|
119
|
-
if (isAuthError(
|
|
120
|
-
emitter.emit("auth-error", {
|
|
140
|
+
if (isAuthError(err)) {
|
|
141
|
+
emitter.emit("auth-error", {
|
|
142
|
+
message: msg,
|
|
143
|
+
access: dialedAccess(socket)
|
|
144
|
+
});
|
|
121
145
|
return;
|
|
122
146
|
}
|
|
123
147
|
markDown(msg);
|
|
@@ -125,8 +149,10 @@ function createSocketioTransport(options = {}) {
|
|
|
125
149
|
}
|
|
126
150
|
function bindSecondarySocketEvents(socket) {
|
|
127
151
|
socket.on("connect_error", (err) => {
|
|
128
|
-
|
|
129
|
-
|
|
152
|
+
if (isAuthError(err)) emitter.emit("auth-error", {
|
|
153
|
+
message: err?.message ?? "connect_error",
|
|
154
|
+
access: dialedAccess(socket)
|
|
155
|
+
});
|
|
130
156
|
});
|
|
131
157
|
}
|
|
132
158
|
function socketOrigin(target) {
|
|
@@ -135,24 +161,17 @@ function createSocketioTransport(options = {}) {
|
|
|
135
161
|
function socketPath(target) {
|
|
136
162
|
return `${new URL(target.endpoint.url).pathname.replace(/\/$/, "")}${path}`;
|
|
137
163
|
}
|
|
138
|
-
function openSocket(namespace, target) {
|
|
139
|
-
const
|
|
140
|
-
const sock =
|
|
141
|
-
|
|
142
|
-
auth: buildAuth(target),
|
|
143
|
-
query: currentQuery,
|
|
144
|
-
reconnection,
|
|
145
|
-
reconnectionDelay,
|
|
146
|
-
reconnectionDelayMax,
|
|
147
|
-
timeout,
|
|
148
|
-
rejectUnauthorized: false,
|
|
149
|
-
transports: ["websocket"]
|
|
150
|
-
});
|
|
164
|
+
function openSocket(namespace, mgr, target) {
|
|
165
|
+
const auth = buildAuth(target);
|
|
166
|
+
const sock = mgr.socket(namespace, { auth });
|
|
167
|
+
sock.auth = auth;
|
|
151
168
|
sockets.set(namespace, sock);
|
|
169
|
+
if (namespace === mainNs) bindMainSocketEvents(sock);
|
|
170
|
+
else bindSecondarySocketEvents(sock);
|
|
171
|
+
sock.connect();
|
|
152
172
|
return sock;
|
|
153
173
|
}
|
|
154
|
-
async function rebuildManager(target) {
|
|
155
|
-
stopRefresh();
|
|
174
|
+
async function rebuildManager(target, epoch) {
|
|
156
175
|
let query;
|
|
157
176
|
try {
|
|
158
177
|
query = await queryFor(target);
|
|
@@ -160,11 +179,10 @@ function createSocketioTransport(options = {}) {
|
|
|
160
179
|
markDown(err instanceof Error ? err.message : String(err));
|
|
161
180
|
throw err;
|
|
162
181
|
}
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
currentQuery = query;
|
|
166
|
-
manager = new Manager(socketOrigin(target), {
|
|
182
|
+
if (epoch !== applyEpoch || disposed) return;
|
|
183
|
+
const mgr = new Manager(socketOrigin(target), {
|
|
167
184
|
path: socketPath(target),
|
|
185
|
+
query,
|
|
168
186
|
autoConnect: false,
|
|
169
187
|
reconnection,
|
|
170
188
|
reconnectionDelay,
|
|
@@ -173,24 +191,31 @@ function createSocketioTransport(options = {}) {
|
|
|
173
191
|
rejectUnauthorized: false,
|
|
174
192
|
transports: ["websocket"]
|
|
175
193
|
});
|
|
176
|
-
|
|
177
|
-
bindMainSocketEvents(main);
|
|
178
|
-
startRefresh();
|
|
194
|
+
manager = mgr;
|
|
179
195
|
lastActivityAt = Date.now();
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
196
|
+
mgr.on("open", touchActivity);
|
|
197
|
+
mgr.on("ping", touchActivity);
|
|
198
|
+
mgr.on("packet", touchActivity);
|
|
199
|
+
openSocket(mainNs, mgr, target);
|
|
200
|
+
for (const ns of reopen) if (!sockets.has(ns)) openSocket(ns, mgr, target);
|
|
201
|
+
reopen.clear();
|
|
202
|
+
startRefresh();
|
|
203
|
+
for (const handler of [...rebuildListeners]) try {
|
|
204
|
+
handler();
|
|
205
|
+
} catch (err) {
|
|
206
|
+
logger?.warn(`rebuild listener failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
207
|
+
}
|
|
183
208
|
}
|
|
184
209
|
function closeAllSockets() {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
sock.disconnect();
|
|
188
|
-
}
|
|
210
|
+
const closing = [...sockets.values()];
|
|
211
|
+
const mgr = manager;
|
|
189
212
|
sockets.clear();
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
213
|
+
manager = null;
|
|
214
|
+
for (const sock of closing) {
|
|
215
|
+
sock.disconnect();
|
|
216
|
+
sock.removeAllListeners();
|
|
193
217
|
}
|
|
218
|
+
mgr?._close();
|
|
194
219
|
}
|
|
195
220
|
function rebindAuth(target) {
|
|
196
221
|
const auth = buildAuth(target);
|
|
@@ -205,7 +230,9 @@ function createSocketioTransport(options = {}) {
|
|
|
205
230
|
const endpointChanged = isEndpointChange(currentTarget, target);
|
|
206
231
|
currentTarget = target;
|
|
207
232
|
if (!target) {
|
|
233
|
+
applyEpoch++;
|
|
208
234
|
closeAllSockets();
|
|
235
|
+
reopen.clear();
|
|
209
236
|
stopRefresh();
|
|
210
237
|
lastActivityAt = 0;
|
|
211
238
|
markDown("detached");
|
|
@@ -213,8 +240,11 @@ function createSocketioTransport(options = {}) {
|
|
|
213
240
|
}
|
|
214
241
|
if (endpointChanged || !manager) {
|
|
215
242
|
const epoch = ++applyEpoch;
|
|
243
|
+
for (const ns of sockets.keys()) if (ns !== mainNs) reopen.add(ns);
|
|
244
|
+
closeAllSockets();
|
|
245
|
+
stopRefresh();
|
|
216
246
|
try {
|
|
217
|
-
await rebuildManager(target);
|
|
247
|
+
await rebuildManager(target, epoch);
|
|
218
248
|
} catch (err) {
|
|
219
249
|
if (epoch === applyEpoch && currentTarget === target) currentTarget = null;
|
|
220
250
|
throw err;
|
|
@@ -224,7 +254,7 @@ function createSocketioTransport(options = {}) {
|
|
|
224
254
|
rebindAuth(target);
|
|
225
255
|
}
|
|
226
256
|
function health() {
|
|
227
|
-
if (status.up &&
|
|
257
|
+
if (status.up && isDeadNow()) return {
|
|
228
258
|
up: false,
|
|
229
259
|
lastError: "staleConnection"
|
|
230
260
|
};
|
|
@@ -235,7 +265,14 @@ function createSocketioTransport(options = {}) {
|
|
|
235
265
|
}
|
|
236
266
|
async function dispose() {
|
|
237
267
|
disposed = true;
|
|
268
|
+
applyEpoch++;
|
|
238
269
|
closeAllSockets();
|
|
270
|
+
reopen.clear();
|
|
271
|
+
rebuildListeners.clear();
|
|
272
|
+
for (const timer of releaseTimers.values()) clearTimeout(timer);
|
|
273
|
+
releaseTimers.clear();
|
|
274
|
+
holds.clear();
|
|
275
|
+
unheld.clear();
|
|
239
276
|
stopRefresh();
|
|
240
277
|
currentTarget = null;
|
|
241
278
|
status = { up: false };
|
|
@@ -245,20 +282,54 @@ function createSocketioTransport(options = {}) {
|
|
|
245
282
|
return sockets.get(namespace) ?? null;
|
|
246
283
|
}
|
|
247
284
|
function ensureSocket(namespace) {
|
|
248
|
-
if (!currentTarget
|
|
249
|
-
|
|
250
|
-
if (!
|
|
251
|
-
|
|
252
|
-
|
|
285
|
+
if (!currentTarget) return null;
|
|
286
|
+
if (!holds.has(namespace)) unheld.add(namespace);
|
|
287
|
+
if (!manager) {
|
|
288
|
+
reopen.add(namespace);
|
|
289
|
+
return null;
|
|
253
290
|
}
|
|
254
|
-
return
|
|
291
|
+
return sockets.get(namespace) ?? openSocket(namespace, manager, currentTarget);
|
|
292
|
+
}
|
|
293
|
+
function acquire(namespace) {
|
|
294
|
+
holds.set(namespace, (holds.get(namespace) ?? 0) + 1);
|
|
295
|
+
clearTimeout(releaseTimers.get(namespace));
|
|
296
|
+
releaseTimers.delete(namespace);
|
|
297
|
+
let held = true;
|
|
298
|
+
return () => {
|
|
299
|
+
if (!held || disposed) return;
|
|
300
|
+
held = false;
|
|
301
|
+
const left = (holds.get(namespace) ?? 1) - 1;
|
|
302
|
+
if (left > 0) {
|
|
303
|
+
holds.set(namespace, left);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
holds.delete(namespace);
|
|
307
|
+
if (namespace === mainNs) return;
|
|
308
|
+
releaseTimers.set(namespace, setTimeout(() => closeReleased(namespace), releaseGraceMs));
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function closeReleased(namespace) {
|
|
312
|
+
releaseTimers.delete(namespace);
|
|
313
|
+
if (holds.has(namespace) || unheld.has(namespace)) return;
|
|
314
|
+
reopen.delete(namespace);
|
|
315
|
+
const sock = sockets.get(namespace);
|
|
316
|
+
if (!sock) return;
|
|
317
|
+
sockets.delete(namespace);
|
|
318
|
+
sock.disconnect();
|
|
319
|
+
sock.removeAllListeners();
|
|
320
|
+
}
|
|
321
|
+
function onRebuild(handler) {
|
|
322
|
+
rebuildListeners.add(handler);
|
|
323
|
+
return () => {
|
|
324
|
+
rebuildListeners.delete(handler);
|
|
325
|
+
};
|
|
255
326
|
}
|
|
256
327
|
function reviveDeadSockets() {
|
|
257
328
|
if (!currentTarget) return;
|
|
258
|
-
if (status.up &&
|
|
329
|
+
if (status.up && isDeadNow()) {
|
|
259
330
|
logger?.debug("reviveDeadSockets: engine stale — forcing reconnect");
|
|
260
331
|
lastActivityAt = Date.now();
|
|
261
|
-
|
|
332
|
+
manager?.engine?.close();
|
|
262
333
|
return;
|
|
263
334
|
}
|
|
264
335
|
const auth = buildAuth(currentTarget);
|
|
@@ -270,11 +341,39 @@ function createSocketioTransport(options = {}) {
|
|
|
270
341
|
}
|
|
271
342
|
logger?.debug(`reviveDeadSockets: ${revived}/${sockets.size} reconnecting`);
|
|
272
343
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
344
|
+
function reconnectInactive(target) {
|
|
345
|
+
const auth = buildAuth(target);
|
|
346
|
+
for (const sock of sockets.values()) {
|
|
347
|
+
if (sock.active) continue;
|
|
348
|
+
sock.auth = auth;
|
|
349
|
+
sock.connect();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function ensureAlive() {
|
|
353
|
+
if (ensureInflight) return ensureInflight;
|
|
354
|
+
const run = async () => {
|
|
355
|
+
if (disposed || !currentTarget) return health();
|
|
356
|
+
const main = sockets.get(mainNs);
|
|
357
|
+
if (main?.connected && !isDeadNow()) {
|
|
358
|
+
if (resolveQuery) refreshQuery();
|
|
359
|
+
const engine = main.io.engine;
|
|
360
|
+
const answered = await new Promise((resolve) => {
|
|
361
|
+
main.timeout(PROBE_TIMEOUT_MS).emit("get-status", null, (err) => resolve(!err));
|
|
362
|
+
});
|
|
363
|
+
if (!answered && main.connected && main.io.engine === engine) {
|
|
364
|
+
logger?.debug("ensureAlive: probe unanswered, closing engine");
|
|
365
|
+
engine.close();
|
|
366
|
+
} else if (answered && currentTarget) reconnectInactive(currentTarget);
|
|
367
|
+
return health();
|
|
368
|
+
}
|
|
369
|
+
if (resolveQuery) await refreshQueryWithin(PROBE_TIMEOUT_MS);
|
|
370
|
+
reviveDeadSockets();
|
|
371
|
+
return health();
|
|
372
|
+
};
|
|
373
|
+
ensureInflight = run().finally(() => {
|
|
374
|
+
ensureInflight = null;
|
|
375
|
+
});
|
|
376
|
+
return ensureInflight;
|
|
278
377
|
}
|
|
279
378
|
return {
|
|
280
379
|
get spec() {
|
|
@@ -289,6 +388,8 @@ function createSocketioTransport(options = {}) {
|
|
|
289
388
|
dispose,
|
|
290
389
|
socket,
|
|
291
390
|
ensureSocket,
|
|
391
|
+
acquire,
|
|
392
|
+
onRebuild,
|
|
292
393
|
reviveDeadSockets,
|
|
293
394
|
ensureAlive
|
|
294
395
|
};
|
package/dist/transports/ws.d.ts
CHANGED
|
@@ -33,7 +33,6 @@ export interface WsTransportOptions {
|
|
|
33
33
|
readonly spec?: Partial<TransportSpec>;
|
|
34
34
|
readonly webSocketCtor?: typeof WebSocket;
|
|
35
35
|
readonly tokenParam?: string;
|
|
36
|
-
readonly sessionParam?: string;
|
|
37
36
|
readonly logger?: Logger;
|
|
38
37
|
readonly resolveUrl?: WsUrlResolver;
|
|
39
38
|
}
|
package/dist/transports/ws.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { 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
|
//#region src/transports/ws.ts
|
|
4
4
|
var WS_SPEC = {
|
|
5
5
|
id: "ws",
|
|
@@ -10,13 +10,13 @@ var WS_CLOSE_TARGET_CHANGED = 4e3;
|
|
|
10
10
|
var WS_CLOSE_DETACHED = 4001;
|
|
11
11
|
var WS_CLOSE_DISPOSED = 4002;
|
|
12
12
|
var WS_CLOSE_RESOLVE_FAILED = 4003;
|
|
13
|
+
var RESOLVE_TIMEOUT_MS = 1e4;
|
|
13
14
|
function createWsTransport(options = {}) {
|
|
14
15
|
const spec = {
|
|
15
16
|
...WS_SPEC,
|
|
16
17
|
...options.spec
|
|
17
18
|
};
|
|
18
19
|
const tokenParam = options.tokenParam ?? "token";
|
|
19
|
-
const sessionParam = options.sessionParam ?? "session";
|
|
20
20
|
const WsCtor = options.webSocketCtor ?? (typeof WebSocket !== "undefined" ? WebSocket : void 0);
|
|
21
21
|
const resolveUrl = options.resolveUrl;
|
|
22
22
|
const logger = options.logger;
|
|
@@ -38,7 +38,6 @@ function createWsTransport(options = {}) {
|
|
|
38
38
|
const params = new URLSearchParams();
|
|
39
39
|
for (const [k, v] of Object.entries(handleSpec.query ?? {})) if (v !== void 0 && v !== null && v !== "") params.set(k, v);
|
|
40
40
|
params.set(tokenParam, target.tokens.access);
|
|
41
|
-
if (target.tokens.proxySession) params.set(sessionParam, target.tokens.proxySession);
|
|
42
41
|
return `${wsProtocol}//${base.hostname}:${port}${prefix}${handleSpec.path}?${params.toString()}`;
|
|
43
42
|
}
|
|
44
43
|
function isAuthCloseEvent(event) {
|
|
@@ -67,7 +66,10 @@ function createWsTransport(options = {}) {
|
|
|
67
66
|
closeDelivered.add(ws);
|
|
68
67
|
if (!isCurrent) return;
|
|
69
68
|
logger?.debug(`close ${handle.spec.path} code=${event.code} clean=${event.wasClean}${event.reason ? ` reason=${event.reason}` : ""}`);
|
|
70
|
-
if (isAuthCloseEvent(event)) emitter.emit("auth-error", {
|
|
69
|
+
if (isAuthCloseEvent(event)) emitter.emit("auth-error", {
|
|
70
|
+
message: event.reason || `ws close code ${event.code}`,
|
|
71
|
+
access: handle.access
|
|
72
|
+
});
|
|
71
73
|
const info = {
|
|
72
74
|
code: event.code,
|
|
73
75
|
reason: event.reason,
|
|
@@ -86,6 +88,7 @@ function createWsTransport(options = {}) {
|
|
|
86
88
|
}
|
|
87
89
|
function closeWs(handle, code, reason) {
|
|
88
90
|
handle.openSeq++;
|
|
91
|
+
clearResolveTimer(handle);
|
|
89
92
|
const ws = handle.ws;
|
|
90
93
|
if (!ws) return;
|
|
91
94
|
const Ctor = ensureCtor();
|
|
@@ -108,11 +111,12 @@ function createWsTransport(options = {}) {
|
|
|
108
111
|
};
|
|
109
112
|
for (const fn of [...handle.listeners.close]) fn(info);
|
|
110
113
|
}
|
|
111
|
-
function connectWs(handle, url) {
|
|
114
|
+
function connectWs(handle, url, access) {
|
|
112
115
|
const ws = new (ensureCtor())(url, handle.spec.protocols);
|
|
113
116
|
if (handle.spec.binaryType) ws.binaryType = handle.spec.binaryType;
|
|
114
117
|
handle.ws = ws;
|
|
115
118
|
handle.url = url;
|
|
119
|
+
handle.access = access;
|
|
116
120
|
bindWs(handle, ws);
|
|
117
121
|
}
|
|
118
122
|
function openWs(handle) {
|
|
@@ -121,29 +125,51 @@ function createWsTransport(options = {}) {
|
|
|
121
125
|
const target = currentTarget;
|
|
122
126
|
const defaultUrl = buildUrl(target, handle.spec);
|
|
123
127
|
if (!resolveUrl) {
|
|
124
|
-
connectWs(handle, defaultUrl);
|
|
128
|
+
connectWs(handle, defaultUrl, target.tokens.access);
|
|
125
129
|
return;
|
|
126
130
|
}
|
|
127
131
|
const seq = ++handle.openSeq;
|
|
132
|
+
clearResolveTimer(handle);
|
|
133
|
+
handle.resolveTimer = setTimeout(() => {
|
|
134
|
+
handle.resolveTimer = void 0;
|
|
135
|
+
if (handle.disposed || seq !== handle.openSeq) return;
|
|
136
|
+
handle.openSeq++;
|
|
137
|
+
logger?.warn(`resolveUrl timed out for ${handle.spec.path} after ${RESOLVE_TIMEOUT_MS}ms`);
|
|
138
|
+
failResolve(handle, "resolve-timeout");
|
|
139
|
+
}, RESOLVE_TIMEOUT_MS);
|
|
128
140
|
resolveUrl({
|
|
129
141
|
target,
|
|
130
142
|
spec: handle.spec,
|
|
131
143
|
defaultUrl
|
|
132
144
|
}).then((url) => {
|
|
133
|
-
if (handle.disposed || seq !== handle.openSeq
|
|
145
|
+
if (handle.disposed || seq !== handle.openSeq) return;
|
|
146
|
+
clearResolveTimer(handle);
|
|
147
|
+
if (currentTarget !== target) {
|
|
148
|
+
openWs(handle);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
134
151
|
connectWs(handle, url);
|
|
135
152
|
}, (err) => {
|
|
136
153
|
if (handle.disposed || seq !== handle.openSeq) return;
|
|
154
|
+
clearResolveTimer(handle);
|
|
137
155
|
const reason = err instanceof Error ? err.message : String(err);
|
|
138
156
|
logger?.warn(`resolveUrl failed for ${handle.spec.path}: ${reason}`);
|
|
139
|
-
|
|
140
|
-
code: WS_CLOSE_RESOLVE_FAILED,
|
|
141
|
-
reason: "resolve-failed",
|
|
142
|
-
wasClean: false
|
|
143
|
-
};
|
|
144
|
-
for (const fn of [...handle.listeners.close]) fn(info);
|
|
157
|
+
failResolve(handle, "resolve-failed");
|
|
145
158
|
});
|
|
146
159
|
}
|
|
160
|
+
function failResolve(handle, reason) {
|
|
161
|
+
const info = {
|
|
162
|
+
code: WS_CLOSE_RESOLVE_FAILED,
|
|
163
|
+
reason,
|
|
164
|
+
wasClean: false
|
|
165
|
+
};
|
|
166
|
+
for (const fn of [...handle.listeners.close]) fn(info);
|
|
167
|
+
}
|
|
168
|
+
function clearResolveTimer(handle) {
|
|
169
|
+
if (handle.resolveTimer === void 0) return;
|
|
170
|
+
clearTimeout(handle.resolveTimer);
|
|
171
|
+
handle.resolveTimer = void 0;
|
|
172
|
+
}
|
|
147
173
|
function recycleAll(code, reason) {
|
|
148
174
|
const snapshot = [...handles];
|
|
149
175
|
logger?.debug(`recycleAll (${reason}) handles=${snapshot.length}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camera.ui/transport",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "camera.ui transport layer — framework-agnostic connection kernel, reducer state, pluggable transports (HTTP/WS/Socket.IO/NATS), lifecycle effects and worker bridge",
|
|
5
5
|
"author": "seydx (https://github.com/cameraui/clients)",
|
|
6
6
|
"type": "module",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"import": "./dist/worker.js"
|
|
40
40
|
},
|
|
41
41
|
"./testing": {
|
|
42
|
-
"types": "./dist/testing.d.ts",
|
|
42
|
+
"types": "./dist/testing/index.d.ts",
|
|
43
43
|
"import": "./dist/testing.js"
|
|
44
44
|
}
|
|
45
45
|
},
|
|
@@ -52,30 +52,31 @@
|
|
|
52
52
|
"test": "vitest run",
|
|
53
53
|
"test:watch": "vitest",
|
|
54
54
|
"type-check": "tsc --noEmit",
|
|
55
|
-
"update": "updates --update ./"
|
|
55
|
+
"update": "updates -M npm,pypi,go,cargo,docker,make --update ./"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@camera.ui/logger": ">=0.0.3",
|
|
59
|
-
"@camera.ui/rpc": ">=1.
|
|
59
|
+
"@camera.ui/rpc": ">=1.1.1",
|
|
60
60
|
"axios": ">=1.20.0",
|
|
61
|
-
"socket.io-client": ">=4.8.
|
|
61
|
+
"socket.io-client": ">=4.8.4"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@eslint/js": "9.39.4",
|
|
65
65
|
"@stylistic/eslint-plugin": "^5.10.0",
|
|
66
|
-
"@typescript-eslint/parser": "^8.
|
|
67
|
-
"@vue/language-core": "^3.3.
|
|
66
|
+
"@typescript-eslint/parser": "^8.72.0",
|
|
67
|
+
"@vue/language-core": "^3.3.13",
|
|
68
68
|
"eslint": "9.39.2",
|
|
69
|
-
"globals": "^17.
|
|
69
|
+
"globals": "^17.13.0",
|
|
70
70
|
"jiti": "^2.7.0",
|
|
71
|
-
"prettier": "^3.9.
|
|
71
|
+
"prettier": "^3.9.10",
|
|
72
72
|
"rimraf": "^6.1.3",
|
|
73
|
+
"socket.io": "^4.8.4",
|
|
73
74
|
"typescript": "5.9.3",
|
|
74
|
-
"typescript-eslint": "^8.
|
|
75
|
-
"unplugin-dts": "^1.1.
|
|
76
|
-
"updates": "^
|
|
77
|
-
"vite": "^8.
|
|
78
|
-
"vitest": "^
|
|
75
|
+
"typescript-eslint": "^8.72.0",
|
|
76
|
+
"unplugin-dts": "^1.1.2",
|
|
77
|
+
"updates": "^19.2.1",
|
|
78
|
+
"vite": "^8.3.4",
|
|
79
|
+
"vitest": "^5.0.5"
|
|
79
80
|
},
|
|
80
81
|
"overrides": {
|
|
81
82
|
"@vue/language-core": "$@vue/language-core"
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
//#region src/transports/closeCodes.ts
|
|
2
|
-
function classifyClose(input) {
|
|
3
|
-
const obj = typeof input === "string" ? void 0 : input;
|
|
4
|
-
const code = obj?.wsCloseCode ?? obj?.code;
|
|
5
|
-
if (code === 4401 || code === 1008) return "auth-expired";
|
|
6
|
-
if (code === 4403 || code === 4400) return "forbidden";
|
|
7
|
-
const text = (typeof input === "string" ? input : obj?.reason ?? obj?.message ?? "").toLowerCase();
|
|
8
|
-
if (!text) return "other";
|
|
9
|
-
if (text.includes("forbidden") || text.includes("403")) return "forbidden";
|
|
10
|
-
if (text.includes("unauthorized") || text.includes("401") || text.includes("auth")) return "auth-expired";
|
|
11
|
-
return "other";
|
|
12
|
-
}
|
|
13
|
-
//#endregion
|
|
14
|
-
export { classifyClose as t };
|