@camera.ui/transport 0.0.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.
Files changed (39) hide show
  1. package/LICENSE.md +22 -0
  2. package/README.md +10 -0
  3. package/dist/contract-d-0gfY8v.js +43 -0
  4. package/dist/core/kernel.d.ts +14 -0
  5. package/dist/core/reducer.d.ts +2 -0
  6. package/dist/core/resolver.d.ts +5 -0
  7. package/dist/core/types.d.ts +109 -0
  8. package/dist/effects/backoff.d.ts +14 -0
  9. package/dist/effects/crossTab.d.ts +14 -0
  10. package/dist/effects/networkChange.d.ts +10 -0
  11. package/dist/effects/persistence.d.ts +31 -0
  12. package/dist/effects/presence.d.ts +17 -0
  13. package/dist/effects/probeLoop.d.ts +30 -0
  14. package/dist/effects/tokenLifecycle.d.ts +39 -0
  15. package/dist/effects/transportSync.d.ts +11 -0
  16. package/dist/effects/transportWatchdog.d.ts +16 -0
  17. package/dist/effects/workerBridge.d.ts +17 -0
  18. package/dist/index.d.ts +30 -0
  19. package/dist/index.js +1101 -0
  20. package/dist/race.d.ts +23 -0
  21. package/dist/testing/fakeTransport.d.ts +24 -0
  22. package/dist/testing/index.d.ts +2 -0
  23. package/dist/testing.js +53 -0
  24. package/dist/transports/contract.d.ts +29 -0
  25. package/dist/transports/http.d.ts +14 -0
  26. package/dist/transports/http.js +131 -0
  27. package/dist/transports/nativeHttp.d.ts +33 -0
  28. package/dist/transports/nativeHttp.js +119 -0
  29. package/dist/transports/nats.d.ts +25 -0
  30. package/dist/transports/nats.js +225 -0
  31. package/dist/transports/socketio.d.ts +19 -0
  32. package/dist/transports/socketio.js +160 -0
  33. package/dist/transports/ws.d.ts +34 -0
  34. package/dist/transports/ws.js +228 -0
  35. package/dist/worker/index.d.ts +3 -0
  36. package/dist/worker/mirror.d.ts +17 -0
  37. package/dist/worker/protocol.d.ts +23 -0
  38. package/dist/worker.js +66 -0
  39. package/package.json +95 -0
package/dist/race.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { Endpoint, EndpointMode } from './core/types.js';
2
+ export type TimeoutByModeFn = (mode: EndpointMode) => number;
3
+ export declare const DEFAULT_RACE_TIMEOUT_BY_MODE: Readonly<Record<EndpointMode, number>>;
4
+ export interface RaceCandidate<T> {
5
+ readonly endpoint: Endpoint;
6
+ readonly run: (signal: AbortSignal) => Promise<T>;
7
+ }
8
+ export interface RaceFirstOptions {
9
+ readonly timeoutByMode?: TimeoutByModeFn;
10
+ readonly shortCircuit?: (err: unknown) => boolean;
11
+ readonly parentSignal?: AbortSignal;
12
+ }
13
+ export interface RaceFirstResult<T> {
14
+ readonly endpoint: Endpoint;
15
+ readonly value: T;
16
+ }
17
+ export declare class RaceFirstError extends Error {
18
+ readonly endpoint: Endpoint;
19
+ readonly cause: unknown;
20
+ readonly kind: 'short-circuit' | 'all-failed' | 'aborted';
21
+ constructor(message: string, endpoint: Endpoint, cause: unknown, kind: RaceFirstError['kind']);
22
+ }
23
+ export declare function raceFirst<T>(candidates: readonly RaceCandidate<T>[], options?: RaceFirstOptions): Promise<RaceFirstResult<T>>;
@@ -0,0 +1,24 @@
1
+ import { ConnectionTarget, TransportSpec, TransportStatus } from '../core/types.js';
2
+ import { Transport, TransportEvent, TransportEventHandler, Unsubscribe } from '../transports/contract.js';
3
+ export interface FakeTransportOptions {
4
+ readonly spec: TransportSpec;
5
+ readonly applyDelayMs?: number;
6
+ }
7
+ export declare class FakeTransport implements Transport {
8
+ readonly spec: TransportSpec;
9
+ readonly applyCalls: (ConnectionTarget | null)[];
10
+ private readonly emitter;
11
+ private readonly applyDelayMs;
12
+ private currentTarget;
13
+ private currentStatus;
14
+ private disposed;
15
+ constructor(options: FakeTransportOptions);
16
+ apply(target: ConnectionTarget | null): Promise<void>;
17
+ health(): TransportStatus;
18
+ on<E extends TransportEvent>(event: E, handler: TransportEventHandler<E>): Unsubscribe;
19
+ dispose(): Promise<void>;
20
+ emitDown(reason: string): void;
21
+ emitUp(): void;
22
+ emitAuthError(status?: number): void;
23
+ get target(): ConnectionTarget | null;
24
+ }
@@ -0,0 +1,2 @@
1
+ export { FakeTransport } from './fakeTransport.js';
2
+ export type { FakeTransportOptions } from './fakeTransport.js';
@@ -0,0 +1,53 @@
1
+ import { t as TransportEmitter } from "./contract-d-0gfY8v.js";
2
+ //#region src/testing/fakeTransport.ts
3
+ var FakeTransport = class {
4
+ spec;
5
+ applyCalls = [];
6
+ emitter = new TransportEmitter();
7
+ applyDelayMs;
8
+ currentTarget = null;
9
+ currentStatus = { up: false };
10
+ disposed = false;
11
+ constructor(options) {
12
+ this.spec = options.spec;
13
+ this.applyDelayMs = options.applyDelayMs ?? 0;
14
+ }
15
+ async apply(target) {
16
+ if (this.disposed) throw new Error("disposed");
17
+ this.applyCalls.push(target);
18
+ if (this.applyDelayMs > 0) await new Promise((resolve) => setTimeout(resolve, this.applyDelayMs));
19
+ this.currentTarget = target;
20
+ this.currentStatus = target ? { up: true } : { up: false };
21
+ if (target) this.emitter.emit("up", void 0);
22
+ else this.emitter.emit("down", { reason: "detached" });
23
+ }
24
+ health() {
25
+ return this.currentStatus;
26
+ }
27
+ on(event, handler) {
28
+ return this.emitter.on(event, handler);
29
+ }
30
+ async dispose() {
31
+ this.disposed = true;
32
+ this.emitter.clear();
33
+ }
34
+ emitDown(reason) {
35
+ this.currentStatus = {
36
+ up: false,
37
+ lastError: reason
38
+ };
39
+ this.emitter.emit("down", { reason });
40
+ }
41
+ emitUp() {
42
+ this.currentStatus = { up: true };
43
+ this.emitter.emit("up", void 0);
44
+ }
45
+ emitAuthError(status) {
46
+ this.emitter.emit("auth-error", { status });
47
+ }
48
+ get target() {
49
+ return this.currentTarget;
50
+ }
51
+ };
52
+ //#endregion
53
+ export { FakeTransport };
@@ -0,0 +1,29 @@
1
+ import { ConnectionTarget, TransportSpec, TransportStatus } from '../core/types.js';
2
+ export type TransportEvent = 'up' | 'down' | 'auth-error';
3
+ export type TransportEventPayload<E extends TransportEvent> = E extends 'down' ? {
4
+ readonly reason: string;
5
+ } : E extends 'auth-error' ? {
6
+ readonly status?: number;
7
+ readonly message?: string;
8
+ } : void;
9
+ export type TransportEventHandler<E extends TransportEvent> = (payload: TransportEventPayload<E>) => void;
10
+ export type Unsubscribe = () => void;
11
+ export interface Transport {
12
+ readonly spec: TransportSpec;
13
+ apply(target: ConnectionTarget | null): Promise<void>;
14
+ health(): TransportStatus;
15
+ on<E extends TransportEvent>(event: E, handler: TransportEventHandler<E>): Unsubscribe;
16
+ dispose(): Promise<void>;
17
+ }
18
+ export interface PerResourceTransport<R = unknown, S = unknown> extends Transport {
19
+ open(spec: S): R;
20
+ }
21
+ export declare function isSameTarget(a: ConnectionTarget | null, b: ConnectionTarget | null): boolean;
22
+ export declare function isEndpointChange(a: ConnectionTarget | null, b: ConnectionTarget | null): boolean;
23
+ export declare function isTokenOnlyChange(a: ConnectionTarget | null, b: ConnectionTarget | null): boolean;
24
+ export declare class TransportEmitter {
25
+ private readonly listeners;
26
+ on<E extends TransportEvent>(event: E, handler: TransportEventHandler<E>): Unsubscribe;
27
+ emit<E extends TransportEvent>(event: E, payload: TransportEventPayload<E>): void;
28
+ clear(): void;
29
+ }
@@ -0,0 +1,14 @@
1
+ import { AxiosInstance, AxiosRequestConfig } from 'axios';
2
+ import { TransportSpec } from '../core/types.js';
3
+ import { Transport } from './contract.js';
4
+ export interface HttpTransportOptions {
5
+ readonly apiPrefix?: string;
6
+ readonly timeoutMs?: number;
7
+ readonly targetWaitMs?: number;
8
+ readonly spec?: Partial<TransportSpec>;
9
+ }
10
+ export interface HttpTransport extends Transport {
11
+ readonly client: AxiosInstance;
12
+ }
13
+ export declare function createHttpTransport(options?: HttpTransportOptions): HttpTransport;
14
+ export type { AxiosInstance, AxiosRequestConfig };
@@ -0,0 +1,131 @@
1
+ import { n as isEndpointChange, r as isSameTarget, t as TransportEmitter } from "../contract-d-0gfY8v.js";
2
+ import axios from "axios";
3
+ //#region src/transports/http.ts
4
+ var HTTP_SPEC = {
5
+ id: "http",
6
+ kind: "request",
7
+ phaseGating: false
8
+ };
9
+ function createHttpTransport(options = {}) {
10
+ const spec = {
11
+ ...HTTP_SPEC,
12
+ ...options.spec
13
+ };
14
+ const apiPrefix = options.apiPrefix ?? "/api";
15
+ const targetWaitMs = options.targetWaitMs ?? 15e3;
16
+ const emitter = new TransportEmitter();
17
+ let currentTarget = null;
18
+ let status = { up: false };
19
+ let disposed = false;
20
+ const targetWaiters = /* @__PURE__ */ new Set();
21
+ const client = axios.create({ timeout: options.timeoutMs ?? 3e4 });
22
+ function waitForTarget(signal) {
23
+ if (currentTarget) return Promise.resolve();
24
+ return new Promise((resolve, reject) => {
25
+ const waiter = {
26
+ resolve: () => {
27
+ cleanup();
28
+ resolve();
29
+ },
30
+ reject: (reason) => {
31
+ cleanup();
32
+ reject(reason);
33
+ }
34
+ };
35
+ const timer = setTimeout(() => waiter.reject(new axios.Cancel("http-transport: no target")), targetWaitMs);
36
+ const onAbort = () => waiter.reject(new axios.Cancel("http-transport: aborted"));
37
+ function cleanup() {
38
+ clearTimeout(timer);
39
+ targetWaiters.delete(waiter);
40
+ signal?.removeEventListener?.("abort", onAbort);
41
+ }
42
+ if (signal?.aborted) {
43
+ waiter.reject(new axios.Cancel("http-transport: aborted"));
44
+ return;
45
+ }
46
+ signal?.addEventListener?.("abort", onAbort);
47
+ targetWaiters.add(waiter);
48
+ });
49
+ }
50
+ function flushTargetWaiters() {
51
+ if (!currentTarget) return;
52
+ for (const waiter of [...targetWaiters]) waiter.resolve();
53
+ }
54
+ client.interceptors.request.use(async (config) => {
55
+ if (!currentTarget) await waitForTarget(config.signal);
56
+ const target = currentTarget;
57
+ if (!target) throw new axios.Cancel("http-transport: no target");
58
+ if (!config.baseURL) config.baseURL = `${target.endpoint.url}${apiPrefix}`;
59
+ config.headers.set("Authorization", `Bearer ${target.tokens.access}`);
60
+ if (target.tokens.proxySession) config.headers.set("X-Proxy-Session", target.tokens.proxySession);
61
+ return config;
62
+ });
63
+ client.interceptors.response.use((response) => {
64
+ if (!status.up) {
65
+ status = { up: true };
66
+ emitter.emit("up", void 0);
67
+ }
68
+ return response;
69
+ }, (error) => {
70
+ if (axios.isCancel(error)) return Promise.reject(error);
71
+ if (!error.response) {
72
+ markDown(error.message ?? "network");
73
+ return Promise.reject(error);
74
+ }
75
+ if (error.response.status === 401) emitter.emit("auth-error", {
76
+ status: 401,
77
+ message: extractMessage(error)
78
+ });
79
+ return Promise.reject(error);
80
+ });
81
+ function markDown(reason) {
82
+ if (status.up || status.lastError !== reason) {
83
+ status = {
84
+ up: false,
85
+ lastError: reason
86
+ };
87
+ emitter.emit("down", { reason });
88
+ }
89
+ }
90
+ async function apply(target) {
91
+ if (disposed) throw new Error("http-transport disposed");
92
+ if (isSameTarget(currentTarget, target)) return;
93
+ const endpointChanged = isEndpointChange(currentTarget, target);
94
+ currentTarget = target;
95
+ if (!target) {
96
+ status = { up: false };
97
+ emitter.emit("down", { reason: "detached" });
98
+ return;
99
+ }
100
+ if (endpointChanged) status = { up: false };
101
+ flushTargetWaiters();
102
+ }
103
+ function health() {
104
+ return status;
105
+ }
106
+ function on(event, handler) {
107
+ return emitter.on(event, handler);
108
+ }
109
+ async function dispose() {
110
+ disposed = true;
111
+ currentTarget = null;
112
+ status = { up: false };
113
+ for (const waiter of [...targetWaiters]) waiter.reject(new axios.Cancel("http-transport: disposed"));
114
+ emitter.clear();
115
+ }
116
+ return {
117
+ spec,
118
+ client,
119
+ apply,
120
+ health,
121
+ on,
122
+ dispose
123
+ };
124
+ }
125
+ function extractMessage(error) {
126
+ const data = error.response?.data;
127
+ if (data && typeof data === "object" && "message" in data && typeof data.message === "string") return data.message;
128
+ return error.message;
129
+ }
130
+ //#endregion
131
+ export { createHttpTransport };
@@ -0,0 +1,33 @@
1
+ import { AxiosAdapter, AxiosInstance, InternalAxiosRequestConfig, ResponseType } from 'axios';
2
+ export type NativeResponseType = 'text' | 'json' | 'blob' | 'arraybuffer' | 'document';
3
+ export interface NativeHttpRequestInit {
4
+ url: string;
5
+ method: string;
6
+ headers: Record<string, string>;
7
+ params?: Record<string, string>;
8
+ data?: unknown;
9
+ responseType: NativeResponseType;
10
+ connectTimeout?: number;
11
+ readTimeout?: number;
12
+ }
13
+ export interface NativeHttpResult {
14
+ data: unknown;
15
+ status: number;
16
+ headers?: Record<string, string>;
17
+ url?: string;
18
+ }
19
+ export type NativeHttpRequest = (init: NativeHttpRequestInit) => Promise<NativeHttpResult>;
20
+ export interface NativeHttpBinding {
21
+ readonly request: NativeHttpRequest;
22
+ readonly extraHeaders?: Record<string, string>;
23
+ }
24
+ export declare function isAbsoluteURL(url: string): boolean;
25
+ export declare function buildFullUrl(config: InternalAxiosRequestConfig): string;
26
+ export declare function isPublicFqdnUrl(url: string): boolean;
27
+ export declare function shouldUseNativeHttp(config: InternalAxiosRequestConfig): boolean;
28
+ export declare function createNativeHttpAdapter(binding: NativeHttpBinding): AxiosAdapter;
29
+ export declare function applyNativeHttp(client: AxiosInstance, opts: {
30
+ enabled: boolean;
31
+ } & NativeHttpBinding): void;
32
+ export declare function normalizeBody(config: InternalAxiosRequestConfig): unknown;
33
+ export declare function mapResponseType(type?: ResponseType): NativeResponseType;
@@ -0,0 +1,119 @@
1
+ import axios, { AxiosError, AxiosHeaders, CanceledError } from "axios";
2
+ //#region src/transports/nativeHttp.ts
3
+ function isAbsoluteURL(url) {
4
+ return /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url);
5
+ }
6
+ function buildFullUrl(config) {
7
+ const url = config.url ?? "";
8
+ const base = config.baseURL ?? "";
9
+ if (base && !isAbsoluteURL(url)) return `${base.replace(/\/+$/, "")}/${url.replace(/^\/+/, "")}`;
10
+ return url;
11
+ }
12
+ function isPublicFqdnUrl(url) {
13
+ let host;
14
+ try {
15
+ host = new URL(url).hostname;
16
+ } catch {
17
+ return false;
18
+ }
19
+ if (!host || host === "localhost" || host.endsWith(".local")) return false;
20
+ if (host.includes(":")) return false;
21
+ if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(host)) return false;
22
+ return host.includes(".");
23
+ }
24
+ function shouldUseNativeHttp(config) {
25
+ return !config.withCredentials && isPublicFqdnUrl(buildFullUrl(config));
26
+ }
27
+ function createNativeHttpAdapter(binding) {
28
+ return async (config) => {
29
+ throwIfAborted(config.signal);
30
+ const headers = flattenHeaders(config.headers);
31
+ if (binding.extraHeaders) Object.assign(headers, binding.extraHeaders);
32
+ const timeout = typeof config.timeout === "number" && config.timeout > 0 ? config.timeout : void 0;
33
+ const pending = binding.request({
34
+ url: buildFullUrl(config),
35
+ method: (config.method ?? "get").toUpperCase(),
36
+ headers,
37
+ params: normalizeParams(config.params),
38
+ data: normalizeBody(config),
39
+ responseType: mapResponseType(config.responseType),
40
+ connectTimeout: timeout,
41
+ readTimeout: timeout
42
+ });
43
+ let res;
44
+ try {
45
+ res = await raceAbort(pending, config.signal);
46
+ } catch (err) {
47
+ if (err instanceof CanceledError) throw err;
48
+ throw new AxiosError(err instanceof Error ? err.message : "Network Error", AxiosError.ERR_NETWORK, config, pending);
49
+ }
50
+ const response = {
51
+ data: res.data,
52
+ status: res.status,
53
+ statusText: "",
54
+ headers: AxiosHeaders.from(res.headers ?? {}),
55
+ config,
56
+ request: pending
57
+ };
58
+ if (!config.validateStatus || config.validateStatus(res.status)) return response;
59
+ throw new AxiosError(`Request failed with status code ${res.status}`, res.status >= 500 ? AxiosError.ERR_BAD_RESPONSE : AxiosError.ERR_BAD_REQUEST, config, pending, response);
60
+ };
61
+ }
62
+ function applyNativeHttp(client, opts) {
63
+ if (!opts.enabled) return;
64
+ const fallback = axios.getAdapter(client.defaults.adapter ?? axios.defaults.adapter);
65
+ const native = createNativeHttpAdapter(opts);
66
+ client.defaults.adapter = (config) => shouldUseNativeHttp(config) ? native(config) : fallback(config);
67
+ }
68
+ function throwIfAborted(signal) {
69
+ if (signal?.aborted) throw new CanceledError("canceled");
70
+ }
71
+ function raceAbort(pending, signal) {
72
+ if (!signal || typeof signal.addEventListener !== "function") return pending;
73
+ if (signal.aborted) return Promise.reject(new CanceledError("canceled"));
74
+ return new Promise((resolve, reject) => {
75
+ const onAbort = () => reject(new CanceledError("canceled"));
76
+ signal.addEventListener("abort", onAbort, { once: true });
77
+ const cleanup = () => signal.removeEventListener?.("abort", onAbort);
78
+ pending.then((value) => {
79
+ cleanup();
80
+ resolve(value);
81
+ }, (err) => {
82
+ cleanup();
83
+ reject(err);
84
+ });
85
+ });
86
+ }
87
+ function flattenHeaders(headers) {
88
+ const out = {};
89
+ if (!headers) return out;
90
+ const json = AxiosHeaders.from(headers).toJSON();
91
+ for (const [key, value] of Object.entries(json)) {
92
+ if (value == null) continue;
93
+ out[key] = Array.isArray(value) ? value.join(", ") : String(value);
94
+ }
95
+ return out;
96
+ }
97
+ function normalizeParams(params) {
98
+ if (!params || typeof params !== "object") return void 0;
99
+ const out = {};
100
+ for (const [key, value] of Object.entries(params)) if (value != null) out[key] = String(value);
101
+ return out;
102
+ }
103
+ function normalizeBody(config) {
104
+ const data = config.data;
105
+ if (typeof data !== "string") return data;
106
+ if (String(AxiosHeaders.from(config.headers).get("Content-Type") ?? "").includes("application/json")) try {
107
+ return JSON.parse(data);
108
+ } catch {}
109
+ return data;
110
+ }
111
+ function mapResponseType(type) {
112
+ switch (type) {
113
+ case "blob": return "blob";
114
+ case "arraybuffer": return "arraybuffer";
115
+ default: return "text";
116
+ }
117
+ }
118
+ //#endregion
119
+ export { applyNativeHttp, buildFullUrl, createNativeHttpAdapter, isAbsoluteURL, isPublicFqdnUrl, mapResponseType, normalizeBody, shouldUseNativeHttp };
@@ -0,0 +1,25 @@
1
+ import { RPCClient } from '@camera.ui/rpc';
2
+ import { TransportSpec } from '../core/types.js';
3
+ import { Transport, Unsubscribe } from './contract.js';
4
+ export interface NatsTransportOptions {
5
+ readonly spec?: Partial<TransportSpec>;
6
+ readonly proxyPath?: string;
7
+ readonly clientName?: string;
8
+ readonly natsUser?: string;
9
+ readonly natsPassword?: string;
10
+ readonly reconnectTimeWait?: number;
11
+ readonly reconnectionDelayMax?: number;
12
+ readonly reconnectionRandomizationFactor?: number;
13
+ readonly timeout?: number;
14
+ readonly pingInterval?: number;
15
+ readonly pingTimeout?: number;
16
+ readonly maxPingOut?: number;
17
+ }
18
+ export type NatsClientListener = (client: RPCClient | null) => void;
19
+ export interface NatsTransport extends Transport {
20
+ getClient(): RPCClient | null;
21
+ subscribeClient(listener: NatsClientListener): Unsubscribe;
22
+ probeAlive(timeoutMs?: number): Promise<void>;
23
+ forceReconnect(): Promise<void>;
24
+ }
25
+ export declare function createNatsTransport(options?: NatsTransportOptions): NatsTransport;
@@ -0,0 +1,225 @@
1
+ import { n as isEndpointChange, r as isSameTarget, t as TransportEmitter } from "../contract-d-0gfY8v.js";
2
+ import { createRPCClient } from "@camera.ui/rpc";
3
+ //#region src/transports/nats.ts
4
+ var NATS_SPEC = {
5
+ id: "nats",
6
+ kind: "persistent",
7
+ phaseGating: true,
8
+ graceMs: 4e3
9
+ };
10
+ function createNatsTransport(options = {}) {
11
+ const spec = {
12
+ ...NATS_SPEC,
13
+ ...options.spec
14
+ };
15
+ const proxyPath = options.proxyPath ?? "/api/proxy";
16
+ const clientName = options.clientName ?? "@camera.ui/transport/nats";
17
+ const natsUser = options.natsUser ?? "secret";
18
+ const natsPassword = options.natsPassword ?? "secret";
19
+ const reconnectTimeWait = options.reconnectTimeWait ?? 1e3;
20
+ const reconnectionDelayMax = options.reconnectionDelayMax ?? 5e3;
21
+ const reconnectionRandomizationFactor = options.reconnectionRandomizationFactor ?? .5;
22
+ const timeout = options.timeout ?? 1e4;
23
+ const pingInterval = options.pingInterval ?? 25e3;
24
+ const pingTimeout = options.pingTimeout ?? 2e4;
25
+ const maxPingOut = options.maxPingOut ?? 1;
26
+ const emitter = new TransportEmitter();
27
+ const clientListeners = /* @__PURE__ */ new Set();
28
+ let proxy = null;
29
+ let currentTarget = null;
30
+ let status = { up: false };
31
+ let statusAbort = null;
32
+ let disposed = false;
33
+ function buildServers(target) {
34
+ const url = new URL(target.endpoint.url);
35
+ const wsProtocol = url.protocol === "https:" ? "wss:" : "ws:";
36
+ const port = url.port || (url.protocol === "https:" ? "443" : "80");
37
+ const params = new URLSearchParams({ token: target.tokens.access });
38
+ if (target.tokens.proxySession) params.set("session", target.tokens.proxySession);
39
+ return [`${wsProtocol}//${url.hostname}:${port}${proxyPath}?${params.toString()}`];
40
+ }
41
+ function notifyClient() {
42
+ const next = status.up ? proxy : null;
43
+ for (const fn of [...clientListeners]) try {
44
+ fn(next);
45
+ } catch {}
46
+ }
47
+ function markUp() {
48
+ if (status.up) return;
49
+ status = { up: true };
50
+ emitter.emit("up", void 0);
51
+ }
52
+ function markDown(reason) {
53
+ if (!status.up && status.lastError === reason) return;
54
+ status = {
55
+ up: false,
56
+ lastError: reason
57
+ };
58
+ emitter.emit("down", { reason });
59
+ }
60
+ function stopStatusMonitor() {
61
+ statusAbort?.abort();
62
+ statusAbort = null;
63
+ }
64
+ function startStatusMonitor(p) {
65
+ stopStatusMonitor();
66
+ const abort = new AbortController();
67
+ statusAbort = abort;
68
+ (async () => {
69
+ const iter = p.status();
70
+ if (!iter) return;
71
+ for await (const event of iter) {
72
+ if (abort.signal.aborted) break;
73
+ switch (event.type) {
74
+ case "reconnect":
75
+ markUp();
76
+ notifyClient();
77
+ break;
78
+ case "disconnect":
79
+ markDown("disconnect");
80
+ notifyClient();
81
+ break;
82
+ case "staleConnection":
83
+ markDown("staleConnection");
84
+ notifyClient();
85
+ break;
86
+ case "error":
87
+ handleErrorEvent(event);
88
+ break;
89
+ }
90
+ }
91
+ })().catch(() => {});
92
+ }
93
+ function handleErrorEvent(event) {
94
+ const message = stringifyError(event.data);
95
+ const lower = message.toLowerCase();
96
+ if (lower.includes("auth") || lower.includes("401") || lower.includes("forbidden") || lower.includes("403")) {
97
+ emitter.emit("auth-error", { message });
98
+ return;
99
+ }
100
+ markDown(message);
101
+ }
102
+ async function rebuildClient(target) {
103
+ stopStatusMonitor();
104
+ if (proxy) {
105
+ try {
106
+ proxy.abortClose();
107
+ } catch {}
108
+ proxy = null;
109
+ notifyClient();
110
+ }
111
+ const next = createRPCClient({
112
+ servers: buildServers(target),
113
+ name: clientName,
114
+ auth: {
115
+ user: natsUser,
116
+ password: natsPassword
117
+ },
118
+ reconnect: true,
119
+ maxReconnectAttempts: -1,
120
+ reconnectTimeWait,
121
+ reconnectionDelayMax,
122
+ reconnectionRandomizationFactor,
123
+ timeout,
124
+ pingInterval,
125
+ pingTimeout,
126
+ maxPingOut,
127
+ ignoreAuthErrorAbort: true
128
+ });
129
+ try {
130
+ await next.connect();
131
+ } catch (err) {
132
+ try {
133
+ next.abortClose();
134
+ } catch {}
135
+ markDown(err instanceof Error ? err.message : String(err));
136
+ throw err;
137
+ }
138
+ proxy = next;
139
+ markUp();
140
+ notifyClient();
141
+ startStatusMonitor(next);
142
+ }
143
+ async function apply(target) {
144
+ if (disposed) throw new Error("nats-transport disposed");
145
+ if (isSameTarget(currentTarget, target)) return;
146
+ const endpointChanged = isEndpointChange(currentTarget, target);
147
+ currentTarget = target;
148
+ if (!target) {
149
+ stopStatusMonitor();
150
+ if (proxy) {
151
+ try {
152
+ proxy.abortClose();
153
+ } catch {}
154
+ proxy = null;
155
+ notifyClient();
156
+ }
157
+ markDown("detached");
158
+ return;
159
+ }
160
+ if (endpointChanged || !proxy) {
161
+ await rebuildClient(target);
162
+ return;
163
+ }
164
+ const newServers = buildServers(target);
165
+ proxy.setServers(newServers);
166
+ }
167
+ function health() {
168
+ return status;
169
+ }
170
+ function on(event, handler) {
171
+ return emitter.on(event, handler);
172
+ }
173
+ async function dispose() {
174
+ disposed = true;
175
+ stopStatusMonitor();
176
+ if (proxy) {
177
+ try {
178
+ proxy.abortClose();
179
+ } catch {}
180
+ proxy = null;
181
+ notifyClient();
182
+ }
183
+ clientListeners.clear();
184
+ status = { up: false };
185
+ emitter.clear();
186
+ }
187
+ function getClient() {
188
+ return proxy;
189
+ }
190
+ function subscribeClient(listener) {
191
+ clientListeners.add(listener);
192
+ listener(proxy);
193
+ return () => clientListeners.delete(listener);
194
+ }
195
+ async function probeAlive(timeoutMs = 5e3) {
196
+ if (!proxy) throw new Error("nats-transport: no client");
197
+ await proxy.flush(timeoutMs);
198
+ }
199
+ async function forceReconnect() {
200
+ if (!proxy) return;
201
+ await proxy.forceReconnect();
202
+ }
203
+ return {
204
+ spec,
205
+ apply,
206
+ health,
207
+ on,
208
+ dispose,
209
+ getClient,
210
+ subscribeClient,
211
+ probeAlive,
212
+ forceReconnect
213
+ };
214
+ }
215
+ function stringifyError(value) {
216
+ if (value instanceof Error) return value.message;
217
+ if (typeof value === "string") return value;
218
+ try {
219
+ return JSON.stringify(value);
220
+ } catch {
221
+ return String(value);
222
+ }
223
+ }
224
+ //#endregion
225
+ export { createNatsTransport };