@camera.ui/transport 0.0.29 → 0.0.30
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/transports/http.d.ts +1 -0
- package/dist/transports/http.js +51 -1
- package/package.json +7 -7
|
@@ -7,6 +7,7 @@ export interface HttpTransportOptions {
|
|
|
7
7
|
readonly apiPrefix?: string;
|
|
8
8
|
readonly timeoutMs?: number;
|
|
9
9
|
readonly targetWaitMs?: number;
|
|
10
|
+
readonly authRetryWaitMs?: number;
|
|
10
11
|
readonly spec?: Partial<TransportSpec>;
|
|
11
12
|
readonly authorize?: HttpAuthorizer;
|
|
12
13
|
readonly logger?: Logger;
|
package/dist/transports/http.js
CHANGED
|
@@ -13,12 +13,14 @@ function createHttpTransport(options = {}) {
|
|
|
13
13
|
};
|
|
14
14
|
const apiPrefix = options.apiPrefix ?? "/api";
|
|
15
15
|
const targetWaitMs = options.targetWaitMs ?? 15e3;
|
|
16
|
+
const authRetryWaitMs = options.authRetryWaitMs ?? 1e4;
|
|
16
17
|
const logger = options.logger;
|
|
17
18
|
const emitter = new TransportEmitter();
|
|
18
19
|
let currentTarget = null;
|
|
19
20
|
let status = { up: false };
|
|
20
21
|
let disposed = false;
|
|
21
22
|
const targetWaiters = /* @__PURE__ */ new Set();
|
|
23
|
+
const accessWaiters = /* @__PURE__ */ new Set();
|
|
22
24
|
const client = axios.create({ timeout: options.timeoutMs ?? 3e4 });
|
|
23
25
|
function waitForTarget(signal) {
|
|
24
26
|
if (currentTarget) return Promise.resolve();
|
|
@@ -52,10 +54,44 @@ function createHttpTransport(options = {}) {
|
|
|
52
54
|
if (!currentTarget) return;
|
|
53
55
|
for (const waiter of [...targetWaiters]) waiter.resolve();
|
|
54
56
|
}
|
|
57
|
+
function waitForNewAccess(staleAccess, signal) {
|
|
58
|
+
if (currentTarget && currentTarget.tokens.access !== staleAccess) return Promise.resolve();
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const waiter = {
|
|
61
|
+
staleAccess,
|
|
62
|
+
resolve: () => {
|
|
63
|
+
cleanup();
|
|
64
|
+
resolve();
|
|
65
|
+
},
|
|
66
|
+
reject: (reason) => {
|
|
67
|
+
cleanup();
|
|
68
|
+
reject(reason);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const timer = setTimeout(() => waiter.reject(/* @__PURE__ */ new Error("http-transport: token refresh timed out")), authRetryWaitMs);
|
|
72
|
+
const onAbort = () => waiter.reject(new axios.Cancel("http-transport: aborted"));
|
|
73
|
+
function cleanup() {
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
accessWaiters.delete(waiter);
|
|
76
|
+
signal?.removeEventListener?.("abort", onAbort);
|
|
77
|
+
}
|
|
78
|
+
if (signal?.aborted) {
|
|
79
|
+
waiter.reject(new axios.Cancel("http-transport: aborted"));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
signal?.addEventListener?.("abort", onAbort);
|
|
83
|
+
accessWaiters.add(waiter);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function flushAccessWaiters() {
|
|
87
|
+
for (const waiter of [...accessWaiters]) if (!currentTarget) waiter.reject(new axios.Cancel("http-transport: no target"));
|
|
88
|
+
else if (currentTarget.tokens.access !== waiter.staleAccess) waiter.resolve();
|
|
89
|
+
}
|
|
55
90
|
client.interceptors.request.use(async (config) => {
|
|
56
91
|
if (!currentTarget) await waitForTarget(config.signal);
|
|
57
92
|
const target = currentTarget;
|
|
58
93
|
if (!target) throw new axios.Cancel("http-transport: no target");
|
|
94
|
+
config._sentAccess = target.tokens.access;
|
|
59
95
|
if (!config.baseURL) config.baseURL = `${target.endpoint.url}${apiPrefix}`;
|
|
60
96
|
if (options.authorize) await options.authorize(config, target);
|
|
61
97
|
else config.headers.set("Authorization", `Bearer ${target.tokens.access}`);
|
|
@@ -69,7 +105,7 @@ function createHttpTransport(options = {}) {
|
|
|
69
105
|
emitter.emit("up", void 0);
|
|
70
106
|
}
|
|
71
107
|
return response;
|
|
72
|
-
}, (error) => {
|
|
108
|
+
}, async (error) => {
|
|
73
109
|
if (axios.isCancel(error)) return Promise.reject(error);
|
|
74
110
|
if (!error.response) {
|
|
75
111
|
markDown(error.message ?? "network");
|
|
@@ -81,6 +117,17 @@ function createHttpTransport(options = {}) {
|
|
|
81
117
|
status: 401,
|
|
82
118
|
message: extractMessage(error)
|
|
83
119
|
});
|
|
120
|
+
const config = error.config;
|
|
121
|
+
if (config?._sentAccess && !config._authRetried && !disposed) {
|
|
122
|
+
config._authRetried = true;
|
|
123
|
+
try {
|
|
124
|
+
await waitForNewAccess(config._sentAccess, config.signal);
|
|
125
|
+
} catch {
|
|
126
|
+
return Promise.reject(error);
|
|
127
|
+
}
|
|
128
|
+
logger?.debug(`retry after token refresh (${config.url ?? "unknown-url"})`);
|
|
129
|
+
return client(config);
|
|
130
|
+
}
|
|
84
131
|
}
|
|
85
132
|
return Promise.reject(error);
|
|
86
133
|
});
|
|
@@ -102,10 +149,12 @@ function createHttpTransport(options = {}) {
|
|
|
102
149
|
if (!target) {
|
|
103
150
|
status = { up: false };
|
|
104
151
|
emitter.emit("down", { reason: "detached" });
|
|
152
|
+
flushAccessWaiters();
|
|
105
153
|
return;
|
|
106
154
|
}
|
|
107
155
|
if (endpointChanged) status = { up: false };
|
|
108
156
|
flushTargetWaiters();
|
|
157
|
+
flushAccessWaiters();
|
|
109
158
|
}
|
|
110
159
|
function health() {
|
|
111
160
|
return status;
|
|
@@ -118,6 +167,7 @@ function createHttpTransport(options = {}) {
|
|
|
118
167
|
currentTarget = null;
|
|
119
168
|
status = { up: false };
|
|
120
169
|
for (const waiter of [...targetWaiters]) waiter.reject(new axios.Cancel("http-transport: disposed"));
|
|
170
|
+
for (const waiter of [...accessWaiters]) waiter.reject(new axios.Cancel("http-transport: disposed"));
|
|
121
171
|
emitter.clear();
|
|
122
172
|
}
|
|
123
173
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camera.ui/transport",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.30",
|
|
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",
|
|
@@ -52,7 +52,7 @@
|
|
|
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",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@eslint/js": "9.39.4",
|
|
65
65
|
"@stylistic/eslint-plugin": "^5.10.0",
|
|
66
|
-
"@typescript-eslint/parser": "^8.
|
|
66
|
+
"@typescript-eslint/parser": "^8.70.0",
|
|
67
67
|
"@vue/language-core": "^3.3.11",
|
|
68
68
|
"eslint": "9.39.2",
|
|
69
69
|
"globals": "^17.12.0",
|
|
@@ -71,11 +71,11 @@
|
|
|
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.70.0",
|
|
75
75
|
"unplugin-dts": "^1.1.0",
|
|
76
|
-
"updates": "^18.0
|
|
77
|
-
"vite": "^8.
|
|
78
|
-
"vitest": "^
|
|
76
|
+
"updates": "^18.1.0",
|
|
77
|
+
"vite": "^8.3.0",
|
|
78
|
+
"vitest": "^5.0.1"
|
|
79
79
|
},
|
|
80
80
|
"overrides": {
|
|
81
81
|
"@vue/language-core": "$@vue/language-core"
|