@camera.ui/transport 0.0.26 → 0.0.27
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/effects/workerBridge.d.ts +8 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +41 -5
- package/dist/transports/http.d.ts +4 -2
- package/dist/transports/http.js +2 -1
- package/dist/transports/nats.d.ts +9 -1
- package/dist/transports/nats.js +74 -4
- package/dist/transports/socketio.d.ts +9 -1
- package/dist/transports/socketio.js +58 -5
- package/dist/transports/ws.d.ts +8 -1
- package/dist/transports/ws.js +35 -5
- package/dist/worker/index.d.ts +2 -2
- package/dist/worker/mirror.d.ts +7 -0
- package/dist/worker/protocol.d.ts +14 -1
- package/dist/worker.js +39 -0
- package/package.json +8 -8
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { Kernel } from '../core/kernel.js';
|
|
2
|
+
import { ConnectionTarget } from '../core/types.js';
|
|
2
3
|
import { WorkerHost } from '../worker/protocol.js';
|
|
3
4
|
export type Detach = () => void;
|
|
5
|
+
export interface BridgeResolveContext {
|
|
6
|
+
readonly target: ConnectionTarget;
|
|
7
|
+
readonly connId: string;
|
|
8
|
+
readonly defaultServers: readonly string[];
|
|
9
|
+
}
|
|
4
10
|
export interface WorkerBridgeOptions {
|
|
5
11
|
readonly kernel: Kernel;
|
|
6
|
-
readonly hosts: () => Iterable<WorkerHost>;
|
|
7
12
|
readonly listenForResyncRequests?: boolean;
|
|
13
|
+
readonly hosts: () => Iterable<WorkerHost>;
|
|
14
|
+
readonly resolveServers?: (ctx: BridgeResolveContext) => Promise<string[]>;
|
|
8
15
|
readonly onBroadcast?: (generation: number, hostCount: number) => void;
|
|
9
16
|
readonly onSyncHost?: (generation: number) => void;
|
|
10
17
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -28,10 +28,10 @@ export type { PresenceCallback, PresenceOptions, VisibilitySource } from './effe
|
|
|
28
28
|
export type { ProbeContext, ProbeFailure, ProbeFailureKind, ProbeLoopOptions } from './effects/probeLoop.js';
|
|
29
29
|
export type { Detach, RefreshReason, TokenLifecycle, TokenLifecycleOptions } from './effects/tokenLifecycle.js';
|
|
30
30
|
export type { TransportSyncOptions } from './effects/transportSync.js';
|
|
31
|
-
export type { WorkerBridge, WorkerBridgeOptions } from './effects/workerBridge.js';
|
|
31
|
+
export type { BridgeResolveContext, WorkerBridge, WorkerBridgeOptions } from './effects/workerBridge.js';
|
|
32
32
|
export type { ConnectionJournal, JournalEntry, JournalOptions } from './journal.js';
|
|
33
33
|
export type { RaceCandidate, RaceFirstOptions, RaceFirstResult, TimeoutByModeFn } from './race.js';
|
|
34
34
|
export type { ConnectionSignal, ConnectionSignalHandle, ConnectionSignalOptions } from './signal.js';
|
|
35
35
|
export type { CloseClass, CloseLike } from './transports/closeCodes.js';
|
|
36
36
|
export type { PerResourceTransport, Transport, TransportEvent, TransportEventHandler, TransportEventPayload } from './transports/contract.js';
|
|
37
|
-
export type { KernelSyncMessage, KernelSyncRequestMessage, MessageSource, WorkerHost, WorkerMessage } from './worker/protocol.js';
|
|
37
|
+
export type { KernelResolveReplyMessage, KernelResolveRequestMessage, KernelSyncMessage, KernelSyncRequestMessage, MessageSource, WorkerHost, WorkerMessage, } from './worker/protocol.js';
|
package/dist/index.js
CHANGED
|
@@ -785,8 +785,10 @@ function attachProbeLoop(options) {
|
|
|
785
785
|
return tokens;
|
|
786
786
|
} catch (err) {
|
|
787
787
|
let finalErr = err;
|
|
788
|
-
if (signal.aborted && !ctrl.signal.aborted && !isProbeFailure(err))
|
|
789
|
-
|
|
788
|
+
if (signal.aborted && !ctrl.signal.aborted && !isProbeFailure(err)) {
|
|
789
|
+
if (signal.reason === "race-settled") finalErr = makeProbeFailure("transient", "superseded");
|
|
790
|
+
else finalErr = makeProbeFailure("transient", `timeout (${(options.timeoutByMode ?? ((m) => DEFAULT_RACE_TIMEOUT_BY_MODE[m] ?? 5e3))(endpoint.mode)}ms)`);
|
|
791
|
+
}
|
|
790
792
|
options.onProbeError?.(endpoint, finalErr);
|
|
791
793
|
throw finalErr;
|
|
792
794
|
}
|
|
@@ -1124,7 +1126,8 @@ function attachWorkerBridge(options) {
|
|
|
1124
1126
|
return {
|
|
1125
1127
|
type: "kernel-sync",
|
|
1126
1128
|
generation,
|
|
1127
|
-
phase
|
|
1129
|
+
phase,
|
|
1130
|
+
resolver: options.resolveServers !== void 0
|
|
1128
1131
|
};
|
|
1129
1132
|
}
|
|
1130
1133
|
function broadcast(phase) {
|
|
@@ -1158,13 +1161,46 @@ function attachWorkerBridge(options) {
|
|
|
1158
1161
|
});
|
|
1159
1162
|
}
|
|
1160
1163
|
}
|
|
1164
|
+
async function answerResolve(host, req) {
|
|
1165
|
+
const reply = (msg) => {
|
|
1166
|
+
try {
|
|
1167
|
+
host.postMessage({
|
|
1168
|
+
type: "kernel-resolve-reply",
|
|
1169
|
+
id: req.id,
|
|
1170
|
+
...msg
|
|
1171
|
+
});
|
|
1172
|
+
} catch (err) {
|
|
1173
|
+
log.warn("resolve reply postMessage failed", err);
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
if (!options.resolveServers) {
|
|
1177
|
+
reply({ servers: [...req.defaultServers] });
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
const phase = options.kernel.phase;
|
|
1181
|
+
if (phase.kind !== "online") {
|
|
1182
|
+
reply({ error: `kernel ${phase.kind}` });
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
try {
|
|
1186
|
+
reply({ servers: await options.resolveServers({
|
|
1187
|
+
target: phase.target,
|
|
1188
|
+
connId: req.connId,
|
|
1189
|
+
defaultServers: req.defaultServers
|
|
1190
|
+
}) });
|
|
1191
|
+
} catch (err) {
|
|
1192
|
+
reply({ error: err instanceof Error ? err.message : String(err) });
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1161
1195
|
function maybeAttachHostListener(host) {
|
|
1162
|
-
if (!options.listenForResyncRequests) return;
|
|
1196
|
+
if (!options.listenForResyncRequests && !options.resolveServers) return;
|
|
1163
1197
|
if (!host.addEventListener) return;
|
|
1164
1198
|
if (hostListenerCleanups.has(host)) return;
|
|
1165
1199
|
const listener = (event) => {
|
|
1166
1200
|
if (detached) return;
|
|
1167
|
-
|
|
1201
|
+
const msg = event.data;
|
|
1202
|
+
if (msg?.type === "kernel-sync-request" && options.listenForResyncRequests) syncOne(host);
|
|
1203
|
+
else if (msg?.type === "kernel-resolve-request") answerResolve(host, msg);
|
|
1168
1204
|
};
|
|
1169
1205
|
host.addEventListener("message", listener);
|
|
1170
1206
|
hostListenerCleanups.set(host, () => {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { Logger } from '@camera.ui/logger';
|
|
2
|
-
import { AxiosInstance, AxiosRequestConfig } from 'axios';
|
|
3
|
-
import { TransportSpec } from '../core/types.js';
|
|
2
|
+
import { AxiosInstance, AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios';
|
|
3
|
+
import { ConnectionTarget, TransportSpec } from '../core/types.js';
|
|
4
4
|
import { Transport } from './contract.js';
|
|
5
|
+
export type HttpAuthorizer = (config: InternalAxiosRequestConfig, target: ConnectionTarget) => void | Promise<void>;
|
|
5
6
|
export interface HttpTransportOptions {
|
|
6
7
|
readonly apiPrefix?: string;
|
|
7
8
|
readonly timeoutMs?: number;
|
|
8
9
|
readonly targetWaitMs?: number;
|
|
9
10
|
readonly spec?: Partial<TransportSpec>;
|
|
11
|
+
readonly authorize?: HttpAuthorizer;
|
|
10
12
|
readonly logger?: Logger;
|
|
11
13
|
}
|
|
12
14
|
export interface HttpTransport extends Transport {
|
package/dist/transports/http.js
CHANGED
|
@@ -57,7 +57,8 @@ function createHttpTransport(options = {}) {
|
|
|
57
57
|
const target = currentTarget;
|
|
58
58
|
if (!target) throw new axios.Cancel("http-transport: no target");
|
|
59
59
|
if (!config.baseURL) config.baseURL = `${target.endpoint.url}${apiPrefix}`;
|
|
60
|
-
|
|
60
|
+
if (options.authorize) await options.authorize(config, target);
|
|
61
|
+
else config.headers.set("Authorization", `Bearer ${target.tokens.access}`);
|
|
61
62
|
if (target.tokens.proxySession) config.headers.set("X-Proxy-Session", target.tokens.proxySession);
|
|
62
63
|
return config;
|
|
63
64
|
});
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { Logger } from '@camera.ui/logger';
|
|
2
2
|
import { RPCClient } from '@camera.ui/rpc';
|
|
3
|
-
import { TransportSpec, TransportStatus } from '../core/types.js';
|
|
3
|
+
import { ConnectionTarget, TransportSpec, TransportStatus } from '../core/types.js';
|
|
4
4
|
import { Transport, Unsubscribe } from './contract.js';
|
|
5
|
+
export interface ResolveServersContext {
|
|
6
|
+
readonly target: ConnectionTarget;
|
|
7
|
+
readonly connId: string;
|
|
8
|
+
readonly defaultServers: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
export type ServersResolver = (ctx: ResolveServersContext) => Promise<string[]>;
|
|
5
11
|
export interface NatsTransportOptions {
|
|
6
12
|
readonly spec?: Partial<TransportSpec>;
|
|
7
13
|
readonly proxyPath?: string;
|
|
14
|
+
readonly resolveRefreshMs?: number;
|
|
8
15
|
readonly clientName?: string;
|
|
9
16
|
readonly natsUser?: string;
|
|
10
17
|
readonly natsPassword?: string;
|
|
@@ -17,6 +24,7 @@ export interface NatsTransportOptions {
|
|
|
17
24
|
readonly pingTimeout?: number;
|
|
18
25
|
readonly maxPingOut?: number;
|
|
19
26
|
readonly logger?: Logger;
|
|
27
|
+
readonly resolveServers?: ServersResolver;
|
|
20
28
|
}
|
|
21
29
|
export type NatsClientListener = (client: RPCClient | null) => void;
|
|
22
30
|
export interface NatsTransport extends Transport {
|
package/dist/transports/nats.js
CHANGED
|
@@ -35,8 +35,11 @@ function createNatsTransport(options = {}) {
|
|
|
35
35
|
const pingTimeout = options.pingTimeout ?? 2e4;
|
|
36
36
|
const maxPingOut = options.maxPingOut ?? 1;
|
|
37
37
|
const logger = options.logger;
|
|
38
|
+
const resolveServers = options.resolveServers;
|
|
39
|
+
const resolveRefreshMs = options.resolveRefreshMs ?? 0;
|
|
38
40
|
const emitter = new TransportEmitter();
|
|
39
41
|
const clientListeners = /* @__PURE__ */ new Set();
|
|
42
|
+
let refreshTimer = null;
|
|
40
43
|
let proxy = null;
|
|
41
44
|
let currentTarget = null;
|
|
42
45
|
let status = { up: false };
|
|
@@ -55,6 +58,53 @@ function createNatsTransport(options = {}) {
|
|
|
55
58
|
if (connId) params.set("connId", connId);
|
|
56
59
|
return [`${wsProtocol}//${url.hostname}:${port}${prefix}${proxyPath}?${params.toString()}`];
|
|
57
60
|
}
|
|
61
|
+
async function serversFor(target) {
|
|
62
|
+
const defaultServers = buildServers(target);
|
|
63
|
+
if (!resolveServers) return defaultServers;
|
|
64
|
+
return resolveServers({
|
|
65
|
+
target,
|
|
66
|
+
connId: connId ?? "",
|
|
67
|
+
defaultServers
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
async function refreshPool(p) {
|
|
71
|
+
const target = currentTarget;
|
|
72
|
+
if (!resolveServers || !target || disposed) return;
|
|
73
|
+
try {
|
|
74
|
+
const servers = await serversFor(target);
|
|
75
|
+
if (proxy === p && !disposed) p.setServers(servers);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
logger?.warn(`server refresh failed: ${stringifyError(err)}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function stopRefresh() {
|
|
81
|
+
if (refreshTimer) {
|
|
82
|
+
clearInterval(refreshTimer);
|
|
83
|
+
refreshTimer = null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function startRefresh(p) {
|
|
87
|
+
stopRefresh();
|
|
88
|
+
if (!resolveServers || resolveRefreshMs <= 0) return;
|
|
89
|
+
refreshTimer = setInterval(() => void refreshPool(p), resolveRefreshMs);
|
|
90
|
+
}
|
|
91
|
+
async function resign(reason) {
|
|
92
|
+
const target = currentTarget;
|
|
93
|
+
if (!proxy || !target || disposed) return;
|
|
94
|
+
logger?.debug(`resign (${reason})`);
|
|
95
|
+
const epoch = ++applyEpoch;
|
|
96
|
+
stopStatusMonitor();
|
|
97
|
+
stopRefresh();
|
|
98
|
+
try {
|
|
99
|
+
proxy.abortClose();
|
|
100
|
+
} catch {}
|
|
101
|
+
proxy = null;
|
|
102
|
+
notifyClient();
|
|
103
|
+
markDown(`signature-expired: ${reason}`);
|
|
104
|
+
try {
|
|
105
|
+
await rebuildClient(target, epoch);
|
|
106
|
+
} catch {}
|
|
107
|
+
}
|
|
58
108
|
function notifyClient() {
|
|
59
109
|
const next = status.up ? proxy : null;
|
|
60
110
|
for (const fn of [...clientListeners]) try {
|
|
@@ -98,6 +148,7 @@ function createNatsTransport(options = {}) {
|
|
|
98
148
|
case "disconnect":
|
|
99
149
|
markDown("disconnect");
|
|
100
150
|
notifyClient();
|
|
151
|
+
if (resolveServers) refreshPool(p);
|
|
101
152
|
break;
|
|
102
153
|
case "staleConnection":
|
|
103
154
|
markDown("staleConnection");
|
|
@@ -119,6 +170,10 @@ function createNatsTransport(options = {}) {
|
|
|
119
170
|
message
|
|
120
171
|
} : message);
|
|
121
172
|
if (cls === "auth-expired") {
|
|
173
|
+
if (resolveServers) {
|
|
174
|
+
resign(message);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
122
177
|
emitter.emit("auth-error", { message });
|
|
123
178
|
return;
|
|
124
179
|
}
|
|
@@ -133,6 +188,7 @@ function createNatsTransport(options = {}) {
|
|
|
133
188
|
logger?.debug(`rebuildClient start (epoch=${epoch})`);
|
|
134
189
|
const t0 = Date.now();
|
|
135
190
|
stopStatusMonitor();
|
|
191
|
+
stopRefresh();
|
|
136
192
|
pendingConnectAbort?.abort();
|
|
137
193
|
if (proxy) {
|
|
138
194
|
try {
|
|
@@ -142,7 +198,17 @@ function createNatsTransport(options = {}) {
|
|
|
142
198
|
notifyClient();
|
|
143
199
|
}
|
|
144
200
|
connId = newConnId();
|
|
145
|
-
|
|
201
|
+
let servers;
|
|
202
|
+
try {
|
|
203
|
+
servers = await serversFor(target);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
if (disposed || epoch !== applyEpoch) return;
|
|
206
|
+
const msg = stringifyError(err);
|
|
207
|
+
logger?.debug(`rebuildClient resolve FAILED: ${msg}`);
|
|
208
|
+
markDown(msg);
|
|
209
|
+
throw err;
|
|
210
|
+
}
|
|
211
|
+
if (disposed || epoch !== applyEpoch) return;
|
|
146
212
|
const next = createRPCClient({
|
|
147
213
|
servers,
|
|
148
214
|
name: clientName,
|
|
@@ -174,7 +240,7 @@ function createNatsTransport(options = {}) {
|
|
|
174
240
|
if (disposed || epoch !== applyEpoch) return;
|
|
175
241
|
const msg = err instanceof Error ? err.message : String(err);
|
|
176
242
|
logger?.debug(`rebuildClient connect FAILED after ${Date.now() - t0}ms: ${msg}`);
|
|
177
|
-
if (classifyClose(err instanceof Error ? err : msg) === "auth-expired") emitter.emit("auth-error", { message: msg });
|
|
243
|
+
if (!resolveServers && classifyClose(err instanceof Error ? err : msg) === "auth-expired") emitter.emit("auth-error", { message: msg });
|
|
178
244
|
markDown(msg);
|
|
179
245
|
throw err;
|
|
180
246
|
} finally {
|
|
@@ -191,6 +257,7 @@ function createNatsTransport(options = {}) {
|
|
|
191
257
|
markUp();
|
|
192
258
|
notifyClient();
|
|
193
259
|
startStatusMonitor(next);
|
|
260
|
+
startRefresh(next);
|
|
194
261
|
}
|
|
195
262
|
async function apply(target) {
|
|
196
263
|
if (disposed) throw new Error("nats-transport disposed");
|
|
@@ -200,6 +267,7 @@ function createNatsTransport(options = {}) {
|
|
|
200
267
|
currentTarget = target;
|
|
201
268
|
if (!target) {
|
|
202
269
|
stopStatusMonitor();
|
|
270
|
+
stopRefresh();
|
|
203
271
|
pendingConnectAbort?.abort();
|
|
204
272
|
pendingConnectAbort = null;
|
|
205
273
|
if (proxy) {
|
|
@@ -221,8 +289,9 @@ function createNatsTransport(options = {}) {
|
|
|
221
289
|
}
|
|
222
290
|
return;
|
|
223
291
|
}
|
|
224
|
-
const
|
|
225
|
-
|
|
292
|
+
const p = proxy;
|
|
293
|
+
const newServers = await serversFor(target);
|
|
294
|
+
if (proxy === p && epoch === applyEpoch) p.setServers(newServers);
|
|
226
295
|
}
|
|
227
296
|
function health() {
|
|
228
297
|
if (status.up && proxy?.isStale) return {
|
|
@@ -238,6 +307,7 @@ function createNatsTransport(options = {}) {
|
|
|
238
307
|
disposed = true;
|
|
239
308
|
applyEpoch++;
|
|
240
309
|
stopStatusMonitor();
|
|
310
|
+
stopRefresh();
|
|
241
311
|
pendingConnectAbort?.abort();
|
|
242
312
|
pendingConnectAbort = null;
|
|
243
313
|
if (proxy) {
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { Manager, Socket } from 'socket.io-client';
|
|
2
2
|
import { Logger } from '@camera.ui/logger';
|
|
3
|
-
import { TransportSpec, TransportStatus } from '../core/types.js';
|
|
3
|
+
import { ConnectionTarget, TransportSpec, TransportStatus } from '../core/types.js';
|
|
4
4
|
import { Transport } from './contract.js';
|
|
5
5
|
export type { Socket };
|
|
6
|
+
export interface ResolveSocketQueryContext {
|
|
7
|
+
readonly target: ConnectionTarget;
|
|
8
|
+
readonly path: string;
|
|
9
|
+
readonly query: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
export type SocketQueryResolver = (ctx: ResolveSocketQueryContext) => Promise<Record<string, string>>;
|
|
6
12
|
export interface SocketioTransportOptions {
|
|
7
13
|
readonly spec?: Partial<TransportSpec>;
|
|
8
14
|
readonly path?: string;
|
|
15
|
+
readonly resolveQuery?: SocketQueryResolver;
|
|
16
|
+
readonly resolveRefreshMs?: number;
|
|
9
17
|
readonly mainNamespace?: string;
|
|
10
18
|
readonly reconnection?: boolean;
|
|
11
19
|
readonly reconnectionDelay?: number;
|
|
@@ -19,10 +19,14 @@ function createSocketioTransport(options = {}) {
|
|
|
19
19
|
const reconnectionDelayMax = options.reconnectionDelayMax ?? 5e3;
|
|
20
20
|
const timeout = options.timeout ?? 2e4;
|
|
21
21
|
const logger = options.logger;
|
|
22
|
+
const resolveQuery = options.resolveQuery;
|
|
23
|
+
const resolveRefreshMs = options.resolveRefreshMs ?? 0;
|
|
22
24
|
const emitter = new TransportEmitter();
|
|
23
25
|
const sockets = /* @__PURE__ */ new Map();
|
|
24
26
|
let manager = null;
|
|
25
27
|
let currentTarget = null;
|
|
28
|
+
let currentQuery = {};
|
|
29
|
+
let refreshTimer = null;
|
|
26
30
|
let status = { up: false };
|
|
27
31
|
let disposed = false;
|
|
28
32
|
const staleAfterMs = options.staleAfterMs ?? 6e4;
|
|
@@ -43,6 +47,38 @@ function createSocketioTransport(options = {}) {
|
|
|
43
47
|
if (target.tokens.proxySession) q.session = target.tokens.proxySession;
|
|
44
48
|
return q;
|
|
45
49
|
}
|
|
50
|
+
async function queryFor(target) {
|
|
51
|
+
const base = buildQuery(target);
|
|
52
|
+
if (!resolveQuery) return base;
|
|
53
|
+
return resolveQuery({
|
|
54
|
+
target,
|
|
55
|
+
path: socketPath(target),
|
|
56
|
+
query: base
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async function refreshQuery() {
|
|
60
|
+
const target = currentTarget;
|
|
61
|
+
if (!resolveQuery || !target || disposed) return;
|
|
62
|
+
try {
|
|
63
|
+
const query = await queryFor(target);
|
|
64
|
+
if (currentTarget !== target || disposed) return;
|
|
65
|
+
currentQuery = query;
|
|
66
|
+
for (const sock of sockets.values()) sock.io.opts.query = query;
|
|
67
|
+
} catch (err) {
|
|
68
|
+
logger?.warn(`query refresh failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function stopRefresh() {
|
|
72
|
+
if (refreshTimer) {
|
|
73
|
+
clearInterval(refreshTimer);
|
|
74
|
+
refreshTimer = null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function startRefresh() {
|
|
78
|
+
stopRefresh();
|
|
79
|
+
if (!resolveQuery || resolveRefreshMs <= 0) return;
|
|
80
|
+
refreshTimer = setInterval(() => void refreshQuery(), resolveRefreshMs);
|
|
81
|
+
}
|
|
46
82
|
function markUp() {
|
|
47
83
|
if (status.up) return;
|
|
48
84
|
status = { up: true };
|
|
@@ -63,7 +99,10 @@ function createSocketioTransport(options = {}) {
|
|
|
63
99
|
}
|
|
64
100
|
function bindMainSocketEvents(socket) {
|
|
65
101
|
socket.on("connect", () => markUp());
|
|
66
|
-
socket.on("disconnect", (reason) =>
|
|
102
|
+
socket.on("disconnect", (reason) => {
|
|
103
|
+
markDown(reason);
|
|
104
|
+
if (resolveQuery) refreshQuery();
|
|
105
|
+
});
|
|
67
106
|
socket.on("connect_error", (err) => {
|
|
68
107
|
const msg = err?.message ?? "connect_error";
|
|
69
108
|
logger?.debug(`connect_error (main): ${msg}`);
|
|
@@ -91,7 +130,7 @@ function createSocketioTransport(options = {}) {
|
|
|
91
130
|
const sock = io(url, {
|
|
92
131
|
path: socketPath(target),
|
|
93
132
|
auth: buildAuth(target),
|
|
94
|
-
query:
|
|
133
|
+
query: currentQuery,
|
|
95
134
|
reconnection,
|
|
96
135
|
reconnectionDelay,
|
|
97
136
|
reconnectionDelayMax,
|
|
@@ -102,8 +141,18 @@ function createSocketioTransport(options = {}) {
|
|
|
102
141
|
sockets.set(namespace, sock);
|
|
103
142
|
return sock;
|
|
104
143
|
}
|
|
105
|
-
function rebuildManager(target) {
|
|
144
|
+
async function rebuildManager(target) {
|
|
145
|
+
stopRefresh();
|
|
146
|
+
let query;
|
|
147
|
+
try {
|
|
148
|
+
query = await queryFor(target);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
markDown(err instanceof Error ? err.message : String(err));
|
|
151
|
+
throw err;
|
|
152
|
+
}
|
|
153
|
+
if (currentTarget !== target || disposed) return;
|
|
106
154
|
closeAllSockets();
|
|
155
|
+
currentQuery = query;
|
|
107
156
|
manager = new Manager(socketOrigin(target), {
|
|
108
157
|
path: socketPath(target),
|
|
109
158
|
autoConnect: false,
|
|
@@ -116,6 +165,7 @@ function createSocketioTransport(options = {}) {
|
|
|
116
165
|
});
|
|
117
166
|
const main = openSocket(mainNs, target);
|
|
118
167
|
bindMainSocketEvents(main);
|
|
168
|
+
startRefresh();
|
|
119
169
|
lastActivityAt = Date.now();
|
|
120
170
|
main.io.on("open", touchActivity);
|
|
121
171
|
main.io.on("ping", touchActivity);
|
|
@@ -146,12 +196,13 @@ function createSocketioTransport(options = {}) {
|
|
|
146
196
|
currentTarget = target;
|
|
147
197
|
if (!target) {
|
|
148
198
|
closeAllSockets();
|
|
199
|
+
stopRefresh();
|
|
149
200
|
lastActivityAt = 0;
|
|
150
201
|
markDown("detached");
|
|
151
202
|
return;
|
|
152
203
|
}
|
|
153
204
|
if (endpointChanged || !manager) {
|
|
154
|
-
rebuildManager(target);
|
|
205
|
+
await rebuildManager(target);
|
|
155
206
|
return;
|
|
156
207
|
}
|
|
157
208
|
rebindAuth(target);
|
|
@@ -169,6 +220,7 @@ function createSocketioTransport(options = {}) {
|
|
|
169
220
|
async function dispose() {
|
|
170
221
|
disposed = true;
|
|
171
222
|
closeAllSockets();
|
|
223
|
+
stopRefresh();
|
|
172
224
|
currentTarget = null;
|
|
173
225
|
status = { up: false };
|
|
174
226
|
emitter.clear();
|
|
@@ -177,7 +229,7 @@ function createSocketioTransport(options = {}) {
|
|
|
177
229
|
return sockets.get(namespace) ?? null;
|
|
178
230
|
}
|
|
179
231
|
function ensureSocket(namespace) {
|
|
180
|
-
if (!currentTarget) return null;
|
|
232
|
+
if (!currentTarget || !manager) return null;
|
|
181
233
|
let sock = sockets.get(namespace);
|
|
182
234
|
if (!sock) {
|
|
183
235
|
sock = openSocket(namespace, currentTarget);
|
|
@@ -204,6 +256,7 @@ function createSocketioTransport(options = {}) {
|
|
|
204
256
|
}
|
|
205
257
|
async function ensureAlive() {
|
|
206
258
|
if (disposed || !currentTarget) return health();
|
|
259
|
+
if (resolveQuery) await refreshQuery();
|
|
207
260
|
reviveDeadSockets();
|
|
208
261
|
return health();
|
|
209
262
|
}
|
package/dist/transports/ws.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Logger } from '@camera.ui/logger';
|
|
2
|
-
import { TransportSpec } from '../core/types.js';
|
|
2
|
+
import { ConnectionTarget, TransportSpec } from '../core/types.js';
|
|
3
3
|
import { PerResourceTransport, Unsubscribe } from './contract.js';
|
|
4
4
|
export interface WsHandleSpec {
|
|
5
5
|
readonly path: string;
|
|
@@ -23,12 +23,19 @@ export interface WsHandle {
|
|
|
23
23
|
on<E extends WsEvent>(event: E, handler: WsEventHandler<E>): Unsubscribe;
|
|
24
24
|
dispose(): void;
|
|
25
25
|
}
|
|
26
|
+
export interface ResolveWsUrlContext {
|
|
27
|
+
readonly target: ConnectionTarget;
|
|
28
|
+
readonly spec: WsHandleSpec;
|
|
29
|
+
readonly defaultUrl: string;
|
|
30
|
+
}
|
|
31
|
+
export type WsUrlResolver = (ctx: ResolveWsUrlContext) => Promise<string>;
|
|
26
32
|
export interface WsTransportOptions {
|
|
27
33
|
readonly spec?: Partial<TransportSpec>;
|
|
28
34
|
readonly webSocketCtor?: typeof WebSocket;
|
|
29
35
|
readonly tokenParam?: string;
|
|
30
36
|
readonly sessionParam?: string;
|
|
31
37
|
readonly logger?: Logger;
|
|
38
|
+
readonly resolveUrl?: WsUrlResolver;
|
|
32
39
|
}
|
|
33
40
|
export interface WsTransport extends PerResourceTransport<WsHandle, WsHandleSpec> {
|
|
34
41
|
readonly handleCount: number;
|
package/dist/transports/ws.js
CHANGED
|
@@ -9,6 +9,7 @@ var WS_SPEC = {
|
|
|
9
9
|
var WS_CLOSE_TARGET_CHANGED = 4e3;
|
|
10
10
|
var WS_CLOSE_DETACHED = 4001;
|
|
11
11
|
var WS_CLOSE_DISPOSED = 4002;
|
|
12
|
+
var WS_CLOSE_RESOLVE_FAILED = 4003;
|
|
12
13
|
function createWsTransport(options = {}) {
|
|
13
14
|
const spec = {
|
|
14
15
|
...WS_SPEC,
|
|
@@ -17,6 +18,7 @@ function createWsTransport(options = {}) {
|
|
|
17
18
|
const tokenParam = options.tokenParam ?? "token";
|
|
18
19
|
const sessionParam = options.sessionParam ?? "session";
|
|
19
20
|
const WsCtor = options.webSocketCtor ?? (typeof WebSocket !== "undefined" ? WebSocket : void 0);
|
|
21
|
+
const resolveUrl = options.resolveUrl;
|
|
20
22
|
const logger = options.logger;
|
|
21
23
|
const emitter = new TransportEmitter();
|
|
22
24
|
const handles = /* @__PURE__ */ new Set();
|
|
@@ -83,6 +85,7 @@ function createWsTransport(options = {}) {
|
|
|
83
85
|
};
|
|
84
86
|
}
|
|
85
87
|
function closeWs(handle, code, reason) {
|
|
88
|
+
handle.openSeq++;
|
|
86
89
|
const ws = handle.ws;
|
|
87
90
|
if (!ws) return;
|
|
88
91
|
const Ctor = ensureCtor();
|
|
@@ -105,16 +108,42 @@ function createWsTransport(options = {}) {
|
|
|
105
108
|
};
|
|
106
109
|
for (const fn of [...handle.listeners.close]) fn(info);
|
|
107
110
|
}
|
|
108
|
-
function
|
|
109
|
-
if (handle.disposed) return;
|
|
110
|
-
if (!currentTarget) return;
|
|
111
|
-
const url = buildUrl(currentTarget, handle.spec);
|
|
111
|
+
function connectWs(handle, url) {
|
|
112
112
|
const ws = new (ensureCtor())(url, handle.spec.protocols);
|
|
113
113
|
if (handle.spec.binaryType) ws.binaryType = handle.spec.binaryType;
|
|
114
114
|
handle.ws = ws;
|
|
115
115
|
handle.url = url;
|
|
116
116
|
bindWs(handle, ws);
|
|
117
117
|
}
|
|
118
|
+
function openWs(handle) {
|
|
119
|
+
if (handle.disposed) return;
|
|
120
|
+
if (!currentTarget) return;
|
|
121
|
+
const target = currentTarget;
|
|
122
|
+
const defaultUrl = buildUrl(target, handle.spec);
|
|
123
|
+
if (!resolveUrl) {
|
|
124
|
+
connectWs(handle, defaultUrl);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const seq = ++handle.openSeq;
|
|
128
|
+
resolveUrl({
|
|
129
|
+
target,
|
|
130
|
+
spec: handle.spec,
|
|
131
|
+
defaultUrl
|
|
132
|
+
}).then((url) => {
|
|
133
|
+
if (handle.disposed || seq !== handle.openSeq || currentTarget !== target) return;
|
|
134
|
+
connectWs(handle, url);
|
|
135
|
+
}, (err) => {
|
|
136
|
+
if (handle.disposed || seq !== handle.openSeq) return;
|
|
137
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
138
|
+
logger?.warn(`resolveUrl failed for ${handle.spec.path}: ${reason}`);
|
|
139
|
+
const info = {
|
|
140
|
+
code: WS_CLOSE_RESOLVE_FAILED,
|
|
141
|
+
reason: "resolve-failed",
|
|
142
|
+
wasClean: false
|
|
143
|
+
};
|
|
144
|
+
for (const fn of [...handle.listeners.close]) fn(info);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
118
147
|
function recycleAll(code, reason) {
|
|
119
148
|
const snapshot = [...handles];
|
|
120
149
|
logger?.debug(`recycleAll (${reason}) handles=${snapshot.length}`);
|
|
@@ -178,7 +207,8 @@ function createWsTransport(options = {}) {
|
|
|
178
207
|
},
|
|
179
208
|
ws: null,
|
|
180
209
|
url: null,
|
|
181
|
-
disposed: false
|
|
210
|
+
disposed: false,
|
|
211
|
+
openSeq: 0
|
|
182
212
|
};
|
|
183
213
|
function send(data) {
|
|
184
214
|
if (handle.disposed) throw new Error("ws-handle disposed");
|
package/dist/worker/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { createWorkerKernelMirror } from './mirror.js';
|
|
2
|
-
export type { MirrorListener, Unsubscribe, WorkerKernelMirror, WorkerKernelMirrorOptions } from './mirror.js';
|
|
3
|
-
export type { KernelSyncMessage, KernelSyncRequestMessage, MessageSource, WorkerHost, WorkerMessage } from './protocol.js';
|
|
2
|
+
export type { MirrorListener, MirrorResolveContext, Unsubscribe, WorkerKernelMirror, WorkerKernelMirrorOptions } from './mirror.js';
|
|
3
|
+
export type { KernelResolveReplyMessage, KernelResolveRequestMessage, KernelSyncMessage, KernelSyncRequestMessage, MessageSource, WorkerHost, WorkerMessage, } from './protocol.js';
|
package/dist/worker/mirror.d.ts
CHANGED
|
@@ -2,16 +2,23 @@ import { ConnectionPhase, ConnectionTarget } from '../core/types.js';
|
|
|
2
2
|
import { MessageSource } from './protocol.js';
|
|
3
3
|
export type Unsubscribe = () => void;
|
|
4
4
|
export type MirrorListener = (next: ConnectionPhase, prev: ConnectionPhase) => void;
|
|
5
|
+
export interface MirrorResolveContext {
|
|
6
|
+
readonly connId: string;
|
|
7
|
+
readonly defaultServers: readonly string[];
|
|
8
|
+
}
|
|
5
9
|
export interface WorkerKernelMirror {
|
|
6
10
|
readonly phase: ConnectionPhase;
|
|
7
11
|
readonly target: ConnectionTarget | null;
|
|
12
|
+
readonly hasResolver: boolean;
|
|
8
13
|
subscribe(listener: MirrorListener): Unsubscribe;
|
|
9
14
|
onRevalidate(listener: () => void): Unsubscribe;
|
|
10
15
|
requestSync(): void;
|
|
16
|
+
resolveServers(ctx: MirrorResolveContext): Promise<string[]>;
|
|
11
17
|
dispose(): void;
|
|
12
18
|
}
|
|
13
19
|
export interface WorkerKernelMirrorOptions {
|
|
14
20
|
readonly source: MessageSource;
|
|
15
21
|
readonly initial?: ConnectionPhase;
|
|
22
|
+
readonly resolveTimeoutMs?: number;
|
|
16
23
|
}
|
|
17
24
|
export declare function createWorkerKernelMirror(options: WorkerKernelMirrorOptions): WorkerKernelMirror;
|
|
@@ -3,6 +3,7 @@ export interface KernelSyncMessage {
|
|
|
3
3
|
readonly type: 'kernel-sync';
|
|
4
4
|
readonly generation: number;
|
|
5
5
|
readonly phase: ConnectionPhase;
|
|
6
|
+
readonly resolver?: boolean;
|
|
6
7
|
}
|
|
7
8
|
export interface KernelSyncRequestMessage {
|
|
8
9
|
readonly type: 'kernel-sync-request';
|
|
@@ -10,7 +11,19 @@ export interface KernelSyncRequestMessage {
|
|
|
10
11
|
export interface KernelRevalidateMessage {
|
|
11
12
|
readonly type: 'kernel-revalidate';
|
|
12
13
|
}
|
|
13
|
-
export
|
|
14
|
+
export interface KernelResolveRequestMessage {
|
|
15
|
+
readonly type: 'kernel-resolve-request';
|
|
16
|
+
readonly id: number;
|
|
17
|
+
readonly connId: string;
|
|
18
|
+
readonly defaultServers: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
export interface KernelResolveReplyMessage {
|
|
21
|
+
readonly type: 'kernel-resolve-reply';
|
|
22
|
+
readonly id: number;
|
|
23
|
+
readonly servers?: string[];
|
|
24
|
+
readonly error?: string;
|
|
25
|
+
}
|
|
26
|
+
export type WorkerMessage = KernelSyncMessage | KernelSyncRequestMessage | KernelRevalidateMessage | KernelResolveRequestMessage | KernelResolveReplyMessage;
|
|
14
27
|
export interface MessageSource {
|
|
15
28
|
postMessage(message: WorkerMessage): void;
|
|
16
29
|
addEventListener(type: 'message', listener: (event: MessageEvent<WorkerMessage>) => void): void;
|
package/dist/worker.js
CHANGED
|
@@ -2,13 +2,29 @@
|
|
|
2
2
|
function createWorkerKernelMirror(options) {
|
|
3
3
|
let current = options.initial ?? { kind: "idle" };
|
|
4
4
|
let lastGeneration = -1;
|
|
5
|
+
let hasResolver = false;
|
|
5
6
|
const listeners = /* @__PURE__ */ new Set();
|
|
6
7
|
const revalidateListeners = /* @__PURE__ */ new Set();
|
|
8
|
+
const pending = /* @__PURE__ */ new Map();
|
|
9
|
+
const resolveTimeoutMs = options.resolveTimeoutMs ?? 15e3;
|
|
10
|
+
let nextResolveId = 1;
|
|
7
11
|
let disposed = false;
|
|
12
|
+
function settleResolve(id, servers, error) {
|
|
13
|
+
const entry = pending.get(id);
|
|
14
|
+
if (!entry) return;
|
|
15
|
+
pending.delete(id);
|
|
16
|
+
clearTimeout(entry.timer);
|
|
17
|
+
if (error !== void 0 || !servers) entry.reject(new Error(error ?? "resolve failed"));
|
|
18
|
+
else entry.resolve(servers);
|
|
19
|
+
}
|
|
8
20
|
function handler(event) {
|
|
9
21
|
if (disposed) return;
|
|
10
22
|
const msg = event.data;
|
|
11
23
|
if (!msg) return;
|
|
24
|
+
if (msg.type === "kernel-resolve-reply") {
|
|
25
|
+
settleResolve(msg.id, msg.servers, msg.error);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
12
28
|
if (msg.type === "kernel-revalidate") {
|
|
13
29
|
for (const l of [...revalidateListeners]) try {
|
|
14
30
|
l();
|
|
@@ -18,6 +34,7 @@ function createWorkerKernelMirror(options) {
|
|
|
18
34
|
if (msg.type !== "kernel-sync") return;
|
|
19
35
|
if (msg.generation <= lastGeneration) return;
|
|
20
36
|
lastGeneration = msg.generation;
|
|
37
|
+
hasResolver = msg.resolver === true;
|
|
21
38
|
const prev = current;
|
|
22
39
|
current = msg.phase;
|
|
23
40
|
if (phaseEquals(prev, current)) return;
|
|
@@ -44,6 +61,9 @@ function createWorkerKernelMirror(options) {
|
|
|
44
61
|
get target() {
|
|
45
62
|
return targetOf(current);
|
|
46
63
|
},
|
|
64
|
+
get hasResolver() {
|
|
65
|
+
return hasResolver;
|
|
66
|
+
},
|
|
47
67
|
subscribe(listener) {
|
|
48
68
|
listeners.add(listener);
|
|
49
69
|
return () => {
|
|
@@ -60,11 +80,30 @@ function createWorkerKernelMirror(options) {
|
|
|
60
80
|
if (disposed) return;
|
|
61
81
|
options.source.postMessage({ type: "kernel-sync-request" });
|
|
62
82
|
},
|
|
83
|
+
resolveServers(ctx) {
|
|
84
|
+
if (disposed) return Promise.reject(/* @__PURE__ */ new Error("mirror disposed"));
|
|
85
|
+
const id = nextResolveId++;
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
const timer = setTimeout(() => settleResolve(id, void 0, "resolve timeout"), resolveTimeoutMs);
|
|
88
|
+
pending.set(id, {
|
|
89
|
+
resolve,
|
|
90
|
+
reject,
|
|
91
|
+
timer
|
|
92
|
+
});
|
|
93
|
+
options.source.postMessage({
|
|
94
|
+
type: "kernel-resolve-request",
|
|
95
|
+
id,
|
|
96
|
+
connId: ctx.connId,
|
|
97
|
+
defaultServers: [...ctx.defaultServers]
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
},
|
|
63
101
|
dispose() {
|
|
64
102
|
disposed = true;
|
|
65
103
|
options.source.removeEventListener("message", handler);
|
|
66
104
|
listeners.clear();
|
|
67
105
|
revalidateListeners.clear();
|
|
106
|
+
for (const id of [...pending.keys()]) settleResolve(id, void 0, "mirror disposed");
|
|
68
107
|
}
|
|
69
108
|
};
|
|
70
109
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camera.ui/transport",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.27",
|
|
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",
|
|
@@ -57,25 +57,25 @@
|
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@camera.ui/logger": ">=0.0.3",
|
|
59
59
|
"@camera.ui/rpc": ">=1.0.11",
|
|
60
|
-
"axios": ">=1.
|
|
60
|
+
"axios": ">=1.20.0",
|
|
61
61
|
"socket.io-client": ">=4.8.3"
|
|
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.68.0",
|
|
67
|
+
"@vue/language-core": "^3.3.11",
|
|
68
68
|
"eslint": "9.39.2",
|
|
69
69
|
"globals": "^17.11.0",
|
|
70
70
|
"jiti": "^2.7.0",
|
|
71
71
|
"prettier": "^3.9.6",
|
|
72
72
|
"rimraf": "^6.1.3",
|
|
73
73
|
"typescript": "5.9.3",
|
|
74
|
-
"typescript-eslint": "^8.
|
|
74
|
+
"typescript-eslint": "^8.68.0",
|
|
75
75
|
"unplugin-dts": "^1.0.3",
|
|
76
|
-
"updates": "^18.0.
|
|
77
|
-
"vite": "^8.2.
|
|
78
|
-
"vitest": "^4.1.
|
|
76
|
+
"updates": "^18.0.7",
|
|
77
|
+
"vite": "^8.2.2",
|
|
78
|
+
"vitest": "^4.1.11"
|
|
79
79
|
},
|
|
80
80
|
"overrides": {
|
|
81
81
|
"@vue/language-core": "$@vue/language-core"
|