@camera.ui/transport 0.0.13 → 0.0.16
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/core/types.d.ts +1 -0
- package/dist/effects/crossTab.d.ts +1 -0
- package/dist/effects/persistence.d.ts +1 -0
- package/dist/index.js +47 -9
- package/dist/transports/nats.js +30 -5
- package/dist/transports/ws.js +8 -1
- package/dist/worker.js +8 -1
- package/package.json +4 -4
package/dist/core/types.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ export type ConnectionPhase = {
|
|
|
38
38
|
readonly kind: 'discovering';
|
|
39
39
|
readonly instanceId: string;
|
|
40
40
|
readonly attempt: number;
|
|
41
|
+
readonly transports?: ReadonlyMap<TransportId, TransportStatus>;
|
|
41
42
|
} | {
|
|
42
43
|
readonly kind: 'online';
|
|
43
44
|
readonly instanceId: string;
|
|
@@ -7,6 +7,7 @@ export interface CrossTabOptions {
|
|
|
7
7
|
readonly kernel: Kernel;
|
|
8
8
|
readonly key?: string;
|
|
9
9
|
readonly source?: CrossTabSource;
|
|
10
|
+
readonly absorb?: (target: ConnectionTarget | null) => void;
|
|
10
11
|
readonly onTokensReceived?: (tokens: ConnectionTarget['tokens']) => void;
|
|
11
12
|
readonly onResetReceived?: () => void;
|
|
12
13
|
readonly onError?: (op: 'parse', err: unknown) => void;
|
|
@@ -16,6 +16,7 @@ export interface Persistence {
|
|
|
16
16
|
readonly detach: Detach;
|
|
17
17
|
peek(): ConnectionTarget | null;
|
|
18
18
|
seed(target: ConnectionTarget): Promise<void>;
|
|
19
|
+
absorb(target: ConnectionTarget | null): void;
|
|
19
20
|
}
|
|
20
21
|
export interface PersistenceOptions {
|
|
21
22
|
readonly kernel: Kernel;
|
package/dist/index.js
CHANGED
|
@@ -22,7 +22,8 @@ function reducer(phase, action, ctx) {
|
|
|
22
22
|
if (phase.kind === "reconnecting") return {
|
|
23
23
|
kind: "discovering",
|
|
24
24
|
instanceId: phase.instanceId,
|
|
25
|
-
attempt: 1
|
|
25
|
+
attempt: 1,
|
|
26
|
+
transports: phase.transports
|
|
26
27
|
};
|
|
27
28
|
if (phase.kind === "needs-auth") return {
|
|
28
29
|
kind: "discovering",
|
|
@@ -32,7 +33,8 @@ function reducer(phase, action, ctx) {
|
|
|
32
33
|
if (phase.kind === "online") return {
|
|
33
34
|
kind: "discovering",
|
|
34
35
|
instanceId: phase.instanceId,
|
|
35
|
-
attempt: 1
|
|
36
|
+
attempt: 1,
|
|
37
|
+
transports: phase.transports
|
|
36
38
|
};
|
|
37
39
|
return phase;
|
|
38
40
|
case "PROBE_SUCCEEDED":
|
|
@@ -43,7 +45,7 @@ function reducer(phase, action, ctx) {
|
|
|
43
45
|
endpoint: action.endpoint,
|
|
44
46
|
tokens: action.tokens
|
|
45
47
|
},
|
|
46
|
-
transports: EMPTY_TRANSPORTS
|
|
48
|
+
transports: phase.transports ?? EMPTY_TRANSPORTS
|
|
47
49
|
};
|
|
48
50
|
if (phase.kind === "reconnecting") return {
|
|
49
51
|
kind: "online",
|
|
@@ -89,6 +91,10 @@ function reducer(phase, action, ctx) {
|
|
|
89
91
|
transports: next
|
|
90
92
|
};
|
|
91
93
|
}
|
|
94
|
+
if (phase.kind === "discovering") return {
|
|
95
|
+
...phase,
|
|
96
|
+
transports: setStatus(phase.transports ?? EMPTY_TRANSPORTS, action.id, { up: true })
|
|
97
|
+
};
|
|
92
98
|
return phase;
|
|
93
99
|
case "TRANSPORT_DOWN":
|
|
94
100
|
if (phase.kind === "online" || phase.kind === "reconnecting") return {
|
|
@@ -99,6 +105,14 @@ function reducer(phase, action, ctx) {
|
|
|
99
105
|
downSince: ctx.now()
|
|
100
106
|
})
|
|
101
107
|
};
|
|
108
|
+
if (phase.kind === "discovering") return {
|
|
109
|
+
...phase,
|
|
110
|
+
transports: setStatus(phase.transports ?? EMPTY_TRANSPORTS, action.id, {
|
|
111
|
+
up: false,
|
|
112
|
+
lastError: action.reason,
|
|
113
|
+
downSince: ctx.now()
|
|
114
|
+
})
|
|
115
|
+
};
|
|
102
116
|
return phase;
|
|
103
117
|
case "TRANSPORT_DOWN_CONFIRMED":
|
|
104
118
|
if (phase.kind !== "online") return phase;
|
|
@@ -321,6 +335,7 @@ function attachCrossTab(options) {
|
|
|
321
335
|
const e = event;
|
|
322
336
|
if (e.key !== key) return;
|
|
323
337
|
if (e.newValue === null) {
|
|
338
|
+
options.absorb?.(null);
|
|
324
339
|
const k = options.kernel.phase.kind;
|
|
325
340
|
if (k !== "online" && k !== "reconnecting") return;
|
|
326
341
|
options.kernel.dispatch({ type: "RESET" });
|
|
@@ -336,7 +351,16 @@ function attachCrossTab(options) {
|
|
|
336
351
|
}
|
|
337
352
|
if (!parsed?.tokens?.access) return;
|
|
338
353
|
const k = options.kernel.phase.kind;
|
|
339
|
-
if (k !== "online" && k !== "reconnecting")
|
|
354
|
+
if (k !== "online" && k !== "reconnecting") {
|
|
355
|
+
if (parsed.endpoint?.url) {
|
|
356
|
+
options.absorb?.({
|
|
357
|
+
endpoint: parsed.endpoint,
|
|
358
|
+
tokens: parsed.tokens
|
|
359
|
+
});
|
|
360
|
+
options.onTokensReceived?.(parsed.tokens);
|
|
361
|
+
}
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
340
364
|
options.kernel.dispatch({
|
|
341
365
|
type: "TOKENS_REFRESHED",
|
|
342
366
|
tokens: parsed.tokens
|
|
@@ -450,6 +474,10 @@ function attachPersistence(options) {
|
|
|
450
474
|
seed: async (target) => {
|
|
451
475
|
if (detached) return;
|
|
452
476
|
await persist(target);
|
|
477
|
+
},
|
|
478
|
+
absorb: (target) => {
|
|
479
|
+
if (detached) return;
|
|
480
|
+
cached = target;
|
|
453
481
|
}
|
|
454
482
|
};
|
|
455
483
|
}
|
|
@@ -695,7 +723,7 @@ function attachProbeLoop(options) {
|
|
|
695
723
|
const underlying = err instanceof RaceFirstError ? err.cause : err;
|
|
696
724
|
if (isProbeFailure(underlying) && underlying.kind === "aborted") {
|
|
697
725
|
setTimeout(() => {
|
|
698
|
-
if (!detached) runRound();
|
|
726
|
+
if (!detached && options.kernel.phase.kind === "discovering") runRound();
|
|
699
727
|
}, 200);
|
|
700
728
|
return;
|
|
701
729
|
}
|
|
@@ -762,6 +790,7 @@ function attachReconnectWatchdog(options) {
|
|
|
762
790
|
var DEFAULT_GRACE_MS$1 = 5e3;
|
|
763
791
|
var DEFAULT_MAX_TRANSIENT_RETRIES = 3;
|
|
764
792
|
var DEFAULT_TRANSIENT_RETRY_DELAY_MS = 2e3;
|
|
793
|
+
var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
|
|
765
794
|
function attachTokenLifecycle(options) {
|
|
766
795
|
const graceMs = options.graceMs ?? DEFAULT_GRACE_MS$1;
|
|
767
796
|
const isTransient = options.isTransientError ?? (() => false);
|
|
@@ -786,7 +815,7 @@ function attachTokenLifecycle(options) {
|
|
|
786
815
|
cancelTimer();
|
|
787
816
|
const exp = target.tokens.accessExpiresAt;
|
|
788
817
|
if (!exp) return;
|
|
789
|
-
const delayMs = Math.max(0, exp - now() - graceMs);
|
|
818
|
+
const delayMs = Math.min(Math.max(0, exp - now() - graceMs), MAX_TIMER_DELAY_MS);
|
|
790
819
|
options.onScheduled?.(delayMs, exp);
|
|
791
820
|
timer = setTimer(() => {
|
|
792
821
|
timer = void 0;
|
|
@@ -824,7 +853,13 @@ function attachTokenLifecycle(options) {
|
|
|
824
853
|
skipped: true
|
|
825
854
|
};
|
|
826
855
|
}
|
|
827
|
-
const
|
|
856
|
+
const livePhase = options.kernel.phase;
|
|
857
|
+
const base = (livePhase.kind === "online" ? livePhase.target : livePhase.kind === "reconnecting" ? livePhase.lastTarget : null) ?? target;
|
|
858
|
+
const refreshTarget = fresh ? {
|
|
859
|
+
...base,
|
|
860
|
+
tokens: fresh
|
|
861
|
+
} : base;
|
|
862
|
+
const tokens = await options.refresh(refreshTarget, reason);
|
|
828
863
|
if (detached) return {
|
|
829
864
|
tokens,
|
|
830
865
|
skipped: false
|
|
@@ -840,8 +875,11 @@ function attachTokenLifecycle(options) {
|
|
|
840
875
|
});
|
|
841
876
|
if (detached) return;
|
|
842
877
|
transientRetries = 0;
|
|
843
|
-
if (result.skipped)
|
|
844
|
-
|
|
878
|
+
if (result.skipped) {
|
|
879
|
+
options.onRefreshSkipped?.(reason, result.tokens);
|
|
880
|
+
const p = options.kernel.phase;
|
|
881
|
+
if (p.kind === "online" && timer === void 0) schedule(p.target);
|
|
882
|
+
} else options.onRefreshSuccess?.(reason, result.tokens);
|
|
845
883
|
} catch (err) {
|
|
846
884
|
if (detached) return;
|
|
847
885
|
const transient = isTransient(err);
|
package/dist/transports/nats.js
CHANGED
|
@@ -41,6 +41,8 @@ function createNatsTransport(options = {}) {
|
|
|
41
41
|
let statusAbort = null;
|
|
42
42
|
let disposed = false;
|
|
43
43
|
let connId = null;
|
|
44
|
+
let applyEpoch = 0;
|
|
45
|
+
let pendingConnectAbort = null;
|
|
44
46
|
function buildServers(target) {
|
|
45
47
|
const url = new URL(target.endpoint.url);
|
|
46
48
|
const wsProtocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
@@ -111,8 +113,9 @@ function createNatsTransport(options = {}) {
|
|
|
111
113
|
}
|
|
112
114
|
markDown(message);
|
|
113
115
|
}
|
|
114
|
-
async function rebuildClient(target) {
|
|
116
|
+
async function rebuildClient(target, epoch) {
|
|
115
117
|
stopStatusMonitor();
|
|
118
|
+
pendingConnectAbort?.abort();
|
|
116
119
|
if (proxy) {
|
|
117
120
|
try {
|
|
118
121
|
proxy.abortClose();
|
|
@@ -140,14 +143,25 @@ function createNatsTransport(options = {}) {
|
|
|
140
143
|
maxPingOut,
|
|
141
144
|
ignoreAuthErrorAbort: true
|
|
142
145
|
});
|
|
146
|
+
const connectAbort = new AbortController();
|
|
147
|
+
pendingConnectAbort = connectAbort;
|
|
143
148
|
try {
|
|
144
|
-
await next.connect();
|
|
149
|
+
await next.connect({ signal: connectAbort.signal });
|
|
145
150
|
} catch (err) {
|
|
146
151
|
try {
|
|
147
152
|
next.abortClose();
|
|
148
153
|
} catch {}
|
|
154
|
+
if (disposed || epoch !== applyEpoch) return;
|
|
149
155
|
markDown(err instanceof Error ? err.message : String(err));
|
|
150
156
|
throw err;
|
|
157
|
+
} finally {
|
|
158
|
+
if (pendingConnectAbort === connectAbort) pendingConnectAbort = null;
|
|
159
|
+
}
|
|
160
|
+
if (disposed || epoch !== applyEpoch) {
|
|
161
|
+
try {
|
|
162
|
+
next.abortClose();
|
|
163
|
+
} catch {}
|
|
164
|
+
return;
|
|
151
165
|
}
|
|
152
166
|
proxy = next;
|
|
153
167
|
markUp();
|
|
@@ -157,10 +171,13 @@ function createNatsTransport(options = {}) {
|
|
|
157
171
|
async function apply(target) {
|
|
158
172
|
if (disposed) throw new Error("nats-transport disposed");
|
|
159
173
|
if (isSameTarget(currentTarget, target)) return;
|
|
174
|
+
const epoch = ++applyEpoch;
|
|
160
175
|
const endpointChanged = isEndpointChange(currentTarget, target);
|
|
161
176
|
currentTarget = target;
|
|
162
177
|
if (!target) {
|
|
163
178
|
stopStatusMonitor();
|
|
179
|
+
pendingConnectAbort?.abort();
|
|
180
|
+
pendingConnectAbort = null;
|
|
164
181
|
if (proxy) {
|
|
165
182
|
try {
|
|
166
183
|
proxy.abortClose();
|
|
@@ -172,7 +189,12 @@ function createNatsTransport(options = {}) {
|
|
|
172
189
|
return;
|
|
173
190
|
}
|
|
174
191
|
if (endpointChanged || !proxy) {
|
|
175
|
-
|
|
192
|
+
try {
|
|
193
|
+
await rebuildClient(target, epoch);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
if (epoch === applyEpoch && currentTarget === target) currentTarget = null;
|
|
196
|
+
throw err;
|
|
197
|
+
}
|
|
176
198
|
return;
|
|
177
199
|
}
|
|
178
200
|
const newServers = buildServers(target);
|
|
@@ -190,7 +212,10 @@ function createNatsTransport(options = {}) {
|
|
|
190
212
|
}
|
|
191
213
|
async function dispose() {
|
|
192
214
|
disposed = true;
|
|
215
|
+
applyEpoch++;
|
|
193
216
|
stopStatusMonitor();
|
|
217
|
+
pendingConnectAbort?.abort();
|
|
218
|
+
pendingConnectAbort = null;
|
|
194
219
|
if (proxy) {
|
|
195
220
|
try {
|
|
196
221
|
proxy.abortClose();
|
|
@@ -203,11 +228,11 @@ function createNatsTransport(options = {}) {
|
|
|
203
228
|
emitter.clear();
|
|
204
229
|
}
|
|
205
230
|
function getClient() {
|
|
206
|
-
return proxy;
|
|
231
|
+
return status.up ? proxy : null;
|
|
207
232
|
}
|
|
208
233
|
function subscribeClient(listener) {
|
|
209
234
|
clientListeners.add(listener);
|
|
210
|
-
listener(proxy);
|
|
235
|
+
listener(status.up ? proxy : null);
|
|
211
236
|
return () => clientListeners.delete(listener);
|
|
212
237
|
}
|
|
213
238
|
async function probeAlive(timeoutMs = 5e3) {
|
package/dist/transports/ws.js
CHANGED
|
@@ -18,6 +18,7 @@ function createWsTransport(options = {}) {
|
|
|
18
18
|
const WsCtor = options.webSocketCtor ?? (typeof WebSocket !== "undefined" ? WebSocket : void 0);
|
|
19
19
|
const emitter = new TransportEmitter();
|
|
20
20
|
const handles = /* @__PURE__ */ new Set();
|
|
21
|
+
const closeDelivered = /* @__PURE__ */ new WeakSet();
|
|
21
22
|
let currentTarget = null;
|
|
22
23
|
let status = { up: false };
|
|
23
24
|
let disposed = false;
|
|
@@ -51,10 +52,14 @@ function createWsTransport(options = {}) {
|
|
|
51
52
|
for (const fn of [...handle.listeners.open]) fn();
|
|
52
53
|
};
|
|
53
54
|
ws.onclose = (event) => {
|
|
54
|
-
|
|
55
|
+
const isCurrent = handle.ws === ws;
|
|
56
|
+
if (isCurrent) {
|
|
55
57
|
handle.ws = null;
|
|
56
58
|
handle.url = null;
|
|
57
59
|
}
|
|
60
|
+
if (closeDelivered.has(ws)) return;
|
|
61
|
+
closeDelivered.add(ws);
|
|
62
|
+
if (!isCurrent) return;
|
|
58
63
|
if (isAuthCloseEvent(event)) emitter.emit("auth-error", { message: event.reason || `ws close code ${event.code}` });
|
|
59
64
|
const info = {
|
|
60
65
|
code: event.code,
|
|
@@ -86,6 +91,8 @@ function createWsTransport(options = {}) {
|
|
|
86
91
|
handle.ws = null;
|
|
87
92
|
handle.url = null;
|
|
88
93
|
}
|
|
94
|
+
if (closeDelivered.has(ws)) return;
|
|
95
|
+
closeDelivered.add(ws);
|
|
89
96
|
const info = {
|
|
90
97
|
code,
|
|
91
98
|
reason,
|
package/dist/worker.js
CHANGED
|
@@ -20,7 +20,7 @@ function createWorkerKernelMirror(options) {
|
|
|
20
20
|
lastGeneration = msg.generation;
|
|
21
21
|
const prev = current;
|
|
22
22
|
current = msg.phase;
|
|
23
|
-
if (prev
|
|
23
|
+
if (phaseEquals(prev, current)) return;
|
|
24
24
|
for (const l of [...listeners]) try {
|
|
25
25
|
l(current, prev);
|
|
26
26
|
} catch {}
|
|
@@ -31,6 +31,13 @@ function createWorkerKernelMirror(options) {
|
|
|
31
31
|
if (p.kind === "reconnecting") return p.lastTarget;
|
|
32
32
|
return null;
|
|
33
33
|
}
|
|
34
|
+
function phaseEquals(a, b) {
|
|
35
|
+
if (a.kind !== b.kind) return false;
|
|
36
|
+
const ta = targetOf(a);
|
|
37
|
+
const tb = targetOf(b);
|
|
38
|
+
if (ta === null || tb === null) return ta === tb;
|
|
39
|
+
return ta.endpoint.url === tb.endpoint.url && ta.endpoint.mode === tb.endpoint.mode && ta.tokens.access === tb.tokens.access && ta.tokens.proxySession === tb.tokens.proxySession && ta.tokens.refresh === tb.tokens.refresh;
|
|
40
|
+
}
|
|
34
41
|
return {
|
|
35
42
|
get phase() {
|
|
36
43
|
return current;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camera.ui/transport",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
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",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@camera.ui/logger": ">=0.0.3",
|
|
59
|
-
"@camera.ui/rpc": ">=1.0.
|
|
59
|
+
"@camera.ui/rpc": ">=1.0.8",
|
|
60
60
|
"axios": ">=1.18.1",
|
|
61
61
|
"socket.io-client": ">=4.8.3"
|
|
62
62
|
},
|
|
@@ -73,8 +73,8 @@
|
|
|
73
73
|
"typescript": "5.9.3",
|
|
74
74
|
"typescript-eslint": "^8.62.1",
|
|
75
75
|
"unplugin-dts": "^1.0.3",
|
|
76
|
-
"updates": "^17.18.
|
|
77
|
-
"vite": "^8.1.
|
|
76
|
+
"updates": "^17.18.2",
|
|
77
|
+
"vite": "^8.1.3",
|
|
78
78
|
"vitest": "^4.1.9"
|
|
79
79
|
},
|
|
80
80
|
"overrides": {
|