@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
package/dist/signal.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type ConnectionSignal = {
|
|
|
8
8
|
} | {
|
|
9
9
|
readonly kind: 'degraded';
|
|
10
10
|
readonly channels: readonly TransportId[];
|
|
11
|
+
readonly reason?: 'first-connect';
|
|
11
12
|
} | {
|
|
12
13
|
readonly kind: 'offline';
|
|
13
14
|
readonly retryAt?: number;
|
|
@@ -18,6 +19,7 @@ export interface ConnectionSignalOptions {
|
|
|
18
19
|
readonly kernel: Kernel;
|
|
19
20
|
readonly transports: readonly Transport[];
|
|
20
21
|
readonly debounceMs?: number;
|
|
22
|
+
readonly firstConnectTimeoutMs?: number;
|
|
21
23
|
}
|
|
22
24
|
export interface ConnectionSignalHandle {
|
|
23
25
|
readonly current: ConnectionSignal;
|
|
@@ -8,6 +8,7 @@ export declare class FakeTransport implements Transport {
|
|
|
8
8
|
readonly spec: TransportSpec;
|
|
9
9
|
readonly applyCalls: (ConnectionTarget | null)[];
|
|
10
10
|
failNextApplies: number;
|
|
11
|
+
failWith: unknown;
|
|
11
12
|
ensureAliveCalls: number;
|
|
12
13
|
private readonly emitter;
|
|
13
14
|
private readonly applyDelayMs;
|
|
@@ -22,6 +23,6 @@ export declare class FakeTransport implements Transport {
|
|
|
22
23
|
dispose(): Promise<void>;
|
|
23
24
|
emitDown(reason: string): void;
|
|
24
25
|
emitUp(): void;
|
|
25
|
-
emitAuthError(status?: number): void;
|
|
26
|
+
emitAuthError(status?: number, access?: string): void;
|
|
26
27
|
get target(): ConnectionTarget | null;
|
|
27
28
|
}
|
package/dist/testing.js
CHANGED
|
@@ -4,6 +4,7 @@ var FakeTransport = class {
|
|
|
4
4
|
spec;
|
|
5
5
|
applyCalls = [];
|
|
6
6
|
failNextApplies = 0;
|
|
7
|
+
failWith;
|
|
7
8
|
ensureAliveCalls = 0;
|
|
8
9
|
emitter = new TransportEmitter();
|
|
9
10
|
applyDelayMs;
|
|
@@ -24,7 +25,7 @@ var FakeTransport = class {
|
|
|
24
25
|
up: false,
|
|
25
26
|
lastError: "apply failed"
|
|
26
27
|
};
|
|
27
|
-
throw new Error("apply failed");
|
|
28
|
+
throw this.failWith ?? /* @__PURE__ */ new Error("apply failed");
|
|
28
29
|
}
|
|
29
30
|
this.currentTarget = target;
|
|
30
31
|
this.currentStatus = target ? { up: true } : { up: false };
|
|
@@ -56,8 +57,11 @@ var FakeTransport = class {
|
|
|
56
57
|
this.currentStatus = { up: true };
|
|
57
58
|
this.emitter.emit("up", void 0);
|
|
58
59
|
}
|
|
59
|
-
emitAuthError(status) {
|
|
60
|
-
this.emitter.emit("auth-error", { status }
|
|
60
|
+
emitAuthError(status, access) {
|
|
61
|
+
this.emitter.emit("auth-error", access === void 0 ? { status } : {
|
|
62
|
+
status,
|
|
63
|
+
access
|
|
64
|
+
});
|
|
61
65
|
}
|
|
62
66
|
get target() {
|
|
63
67
|
return this.currentTarget;
|
|
@@ -4,5 +4,7 @@ export interface CloseLike {
|
|
|
4
4
|
readonly wsCloseCode?: number;
|
|
5
5
|
readonly message?: string;
|
|
6
6
|
readonly reason?: string;
|
|
7
|
+
readonly data?: unknown;
|
|
7
8
|
}
|
|
8
9
|
export declare function classifyClose(input: CloseLike | string): CloseClass;
|
|
10
|
+
export declare function refusedByServer(input: unknown): boolean;
|
|
@@ -5,6 +5,8 @@ export type TransportEventPayload<E extends TransportEvent> = E extends 'down' ?
|
|
|
5
5
|
} : E extends 'auth-error' ? {
|
|
6
6
|
readonly status?: number;
|
|
7
7
|
readonly message?: string;
|
|
8
|
+
readonly access?: string;
|
|
9
|
+
readonly holdFor?: (refresh: Promise<boolean>) => void;
|
|
8
10
|
} : void;
|
|
9
11
|
export type TransportEventHandler<E extends TransportEvent> = (payload: TransportEventPayload<E>) => void;
|
|
10
12
|
export type Unsubscribe = () => void;
|
|
@@ -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
|
@@ -6,6 +6,11 @@ var HTTP_SPEC = {
|
|
|
6
6
|
kind: "request",
|
|
7
7
|
phaseGating: false
|
|
8
8
|
};
|
|
9
|
+
var TOKEN_REJECTED_MESSAGES = /* @__PURE__ */ new Set([
|
|
10
|
+
"Unauthorized",
|
|
11
|
+
"Token expired",
|
|
12
|
+
"Token revoked"
|
|
13
|
+
]);
|
|
9
14
|
function createHttpTransport(options = {}) {
|
|
10
15
|
const spec = {
|
|
11
16
|
...HTTP_SPEC,
|
|
@@ -13,12 +18,14 @@ function createHttpTransport(options = {}) {
|
|
|
13
18
|
};
|
|
14
19
|
const apiPrefix = options.apiPrefix ?? "/api";
|
|
15
20
|
const targetWaitMs = options.targetWaitMs ?? 15e3;
|
|
21
|
+
const authRetryWaitMs = options.authRetryWaitMs ?? 1e4;
|
|
16
22
|
const logger = options.logger;
|
|
17
23
|
const emitter = new TransportEmitter();
|
|
18
24
|
let currentTarget = null;
|
|
19
25
|
let status = { up: false };
|
|
20
26
|
let disposed = false;
|
|
21
27
|
const targetWaiters = /* @__PURE__ */ new Set();
|
|
28
|
+
const accessWaiters = /* @__PURE__ */ new Set();
|
|
22
29
|
const client = axios.create({ timeout: options.timeoutMs ?? 3e4 });
|
|
23
30
|
function waitForTarget(signal) {
|
|
24
31
|
if (currentTarget) return Promise.resolve();
|
|
@@ -52,14 +59,50 @@ function createHttpTransport(options = {}) {
|
|
|
52
59
|
if (!currentTarget) return;
|
|
53
60
|
for (const waiter of [...targetWaiters]) waiter.resolve();
|
|
54
61
|
}
|
|
62
|
+
function waitForNewAccess(staleAccess, signal, refresh) {
|
|
63
|
+
if (currentTarget && currentTarget.tokens.access !== staleAccess) return Promise.resolve(true);
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const waiter = {
|
|
66
|
+
staleAccess,
|
|
67
|
+
resolve: (fresh) => {
|
|
68
|
+
cleanup();
|
|
69
|
+
resolve(fresh);
|
|
70
|
+
},
|
|
71
|
+
reject: (reason) => {
|
|
72
|
+
cleanup();
|
|
73
|
+
reject(reason);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const timer = setTimeout(() => waiter.reject(/* @__PURE__ */ new Error("http-transport: token refresh timed out")), authRetryWaitMs);
|
|
77
|
+
const onAbort = () => waiter.reject(new axios.Cancel("http-transport: aborted"));
|
|
78
|
+
function cleanup() {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
accessWaiters.delete(waiter);
|
|
81
|
+
signal?.removeEventListener?.("abort", onAbort);
|
|
82
|
+
}
|
|
83
|
+
if (signal?.aborted) {
|
|
84
|
+
waiter.reject(new axios.Cancel("http-transport: aborted"));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
signal?.addEventListener?.("abort", onAbort);
|
|
88
|
+
accessWaiters.add(waiter);
|
|
89
|
+
refresh?.then((coming) => {
|
|
90
|
+
if (!coming) waiter.resolve(false);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function flushAccessWaiters() {
|
|
95
|
+
for (const waiter of [...accessWaiters]) if (!currentTarget) waiter.reject(new axios.Cancel("http-transport: no target"));
|
|
96
|
+
else if (currentTarget.tokens.access !== waiter.staleAccess) waiter.resolve(true);
|
|
97
|
+
}
|
|
55
98
|
client.interceptors.request.use(async (config) => {
|
|
56
99
|
if (!currentTarget) await waitForTarget(config.signal);
|
|
57
100
|
const target = currentTarget;
|
|
58
101
|
if (!target) throw new axios.Cancel("http-transport: no target");
|
|
102
|
+
config._sentAccess = target.tokens.access;
|
|
59
103
|
if (!config.baseURL) config.baseURL = `${target.endpoint.url}${apiPrefix}`;
|
|
60
104
|
if (options.authorize) await options.authorize(config, target);
|
|
61
105
|
else config.headers.set("Authorization", `Bearer ${target.tokens.access}`);
|
|
62
|
-
if (target.tokens.proxySession) config.headers.set("X-Proxy-Session", target.tokens.proxySession);
|
|
63
106
|
return config;
|
|
64
107
|
});
|
|
65
108
|
client.interceptors.response.use((response) => {
|
|
@@ -69,7 +112,7 @@ function createHttpTransport(options = {}) {
|
|
|
69
112
|
emitter.emit("up", void 0);
|
|
70
113
|
}
|
|
71
114
|
return response;
|
|
72
|
-
}, (error) => {
|
|
115
|
+
}, async (error) => {
|
|
73
116
|
if (axios.isCancel(error)) return Promise.reject(error);
|
|
74
117
|
if (!error.response) {
|
|
75
118
|
markDown(error.message ?? "network");
|
|
@@ -77,13 +120,34 @@ function createHttpTransport(options = {}) {
|
|
|
77
120
|
}
|
|
78
121
|
if (error.response.status === 401) {
|
|
79
122
|
logger?.debug(`auth-error 401 (${error.config?.url ?? "unknown-url"})`);
|
|
123
|
+
const refreshes = [];
|
|
124
|
+
const config = error.config;
|
|
125
|
+
const message = extractMessage(error);
|
|
126
|
+
const tokenRejected = !options.authorize && message !== void 0 && TOKEN_REJECTED_MESSAGES.has(message);
|
|
80
127
|
emitter.emit("auth-error", {
|
|
81
128
|
status: 401,
|
|
82
|
-
message
|
|
129
|
+
message,
|
|
130
|
+
access: tokenRejected ? config?._sentAccess : void 0,
|
|
131
|
+
holdFor: (refresh) => refreshes.push(refresh)
|
|
83
132
|
});
|
|
133
|
+
if (config?._sentAccess && !config._authRetried && !disposed) {
|
|
134
|
+
config._authRetried = true;
|
|
135
|
+
if (!await shouldRetryAuth(config._sentAccess, refreshes, config.signal)) return Promise.reject(error);
|
|
136
|
+
logger?.debug(`retry after 401 (${config.url ?? "unknown-url"})`);
|
|
137
|
+
return client(config);
|
|
138
|
+
}
|
|
84
139
|
}
|
|
85
140
|
return Promise.reject(error);
|
|
86
141
|
});
|
|
142
|
+
async function shouldRetryAuth(sentAccess, refreshes, signal) {
|
|
143
|
+
if (refreshes.length === 0 && options.authorize) return true;
|
|
144
|
+
const refresh = refreshes.length > 0 ? Promise.all(refreshes).then((results) => results.some(Boolean), () => false) : void 0;
|
|
145
|
+
try {
|
|
146
|
+
return await waitForNewAccess(sentAccess, signal, refresh) || Boolean(options.authorize);
|
|
147
|
+
} catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
87
151
|
function markDown(reason) {
|
|
88
152
|
if (status.up || status.lastError !== reason) {
|
|
89
153
|
status = {
|
|
@@ -102,10 +166,12 @@ function createHttpTransport(options = {}) {
|
|
|
102
166
|
if (!target) {
|
|
103
167
|
status = { up: false };
|
|
104
168
|
emitter.emit("down", { reason: "detached" });
|
|
169
|
+
flushAccessWaiters();
|
|
105
170
|
return;
|
|
106
171
|
}
|
|
107
172
|
if (endpointChanged) status = { up: false };
|
|
108
173
|
flushTargetWaiters();
|
|
174
|
+
flushAccessWaiters();
|
|
109
175
|
}
|
|
110
176
|
function health() {
|
|
111
177
|
return status;
|
|
@@ -118,6 +184,7 @@ function createHttpTransport(options = {}) {
|
|
|
118
184
|
currentTarget = null;
|
|
119
185
|
status = { up: false };
|
|
120
186
|
for (const waiter of [...targetWaiters]) waiter.reject(new axios.Cancel("http-transport: disposed"));
|
|
187
|
+
for (const waiter of [...accessWaiters]) waiter.reject(new axios.Cancel("http-transport: disposed"));
|
|
121
188
|
emitter.clear();
|
|
122
189
|
}
|
|
123
190
|
return {
|
|
@@ -6,6 +6,7 @@ export interface NativeHttpRequestInit {
|
|
|
6
6
|
headers: Record<string, string>;
|
|
7
7
|
params?: Record<string, string>;
|
|
8
8
|
data?: unknown;
|
|
9
|
+
dataType?: 'file' | 'formData';
|
|
9
10
|
responseType: NativeResponseType;
|
|
10
11
|
connectTimeout?: number;
|
|
11
12
|
readTimeout?: number;
|
|
@@ -24,10 +25,11 @@ export interface NativeHttpBinding {
|
|
|
24
25
|
export declare function isAbsoluteURL(url: string): boolean;
|
|
25
26
|
export declare function buildFullUrl(config: InternalAxiosRequestConfig): string;
|
|
26
27
|
export declare function isPublicFqdnUrl(url: string): boolean;
|
|
27
|
-
export declare function shouldUseNativeHttp(config: InternalAxiosRequestConfig): boolean;
|
|
28
|
+
export declare function shouldUseNativeHttp(config: InternalAxiosRequestConfig, exclude?: (url: string) => boolean): boolean;
|
|
28
29
|
export declare function createNativeHttpAdapter(binding: NativeHttpBinding): AxiosAdapter;
|
|
29
30
|
export declare function applyNativeHttp(client: AxiosInstance, opts: {
|
|
30
31
|
enabled: boolean;
|
|
32
|
+
exclude?: (url: string) => boolean;
|
|
31
33
|
} & NativeHttpBinding): void;
|
|
32
34
|
export declare function normalizeBody(config: InternalAxiosRequestConfig): unknown;
|
|
33
35
|
export declare function mapResponseType(type?: ResponseType): NativeResponseType;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import axios, { AxiosError, AxiosHeaders, CanceledError } from "axios";
|
|
2
2
|
//#region src/transports/nativeHttp.ts
|
|
3
|
+
var BASE64_CHUNK = 24576;
|
|
3
4
|
function isAbsoluteURL(url) {
|
|
4
5
|
return /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url);
|
|
5
6
|
}
|
|
@@ -21,37 +22,44 @@ function isPublicFqdnUrl(url) {
|
|
|
21
22
|
if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(host)) return false;
|
|
22
23
|
return host.includes(".");
|
|
23
24
|
}
|
|
24
|
-
function shouldUseNativeHttp(config) {
|
|
25
|
-
|
|
25
|
+
function shouldUseNativeHttp(config, exclude) {
|
|
26
|
+
if (config.withCredentials) return false;
|
|
27
|
+
const url = buildFullUrl(config);
|
|
28
|
+
return isPublicFqdnUrl(url) && !exclude?.(url);
|
|
26
29
|
}
|
|
27
30
|
function createNativeHttpAdapter(binding) {
|
|
28
31
|
return async (config) => {
|
|
29
32
|
throwIfAborted(config.signal);
|
|
30
33
|
const headers = flattenHeaders(config.headers);
|
|
31
34
|
if (binding.extraHeaders) Object.assign(headers, binding.extraHeaders);
|
|
35
|
+
const body = await encodeBody(config, headers);
|
|
36
|
+
throwIfAborted(config.signal);
|
|
37
|
+
if (body.contentType) setContentType(headers, body.contentType);
|
|
32
38
|
const timeout = typeof config.timeout === "number" && config.timeout > 0 ? config.timeout : void 0;
|
|
33
39
|
const pending = binding.request({
|
|
34
40
|
url: buildFullUrl(config),
|
|
35
41
|
method: (config.method ?? "get").toUpperCase(),
|
|
36
42
|
headers,
|
|
37
43
|
params: normalizeParams(config.params),
|
|
38
|
-
data:
|
|
44
|
+
data: body.data,
|
|
45
|
+
dataType: body.dataType,
|
|
39
46
|
responseType: mapResponseType(config.responseType),
|
|
40
47
|
connectTimeout: timeout,
|
|
41
48
|
readTimeout: timeout
|
|
42
49
|
});
|
|
43
50
|
let res;
|
|
44
51
|
try {
|
|
45
|
-
res = await
|
|
52
|
+
res = await raceNative(pending, config, timeout);
|
|
46
53
|
} catch (err) {
|
|
47
|
-
if (err instanceof
|
|
54
|
+
if (err instanceof AxiosError) throw err;
|
|
48
55
|
throw new AxiosError(err instanceof Error ? err.message : "Network Error", AxiosError.ERR_NETWORK, config, pending);
|
|
49
56
|
}
|
|
57
|
+
const responseHeaders = AxiosHeaders.from(res.headers ?? {});
|
|
50
58
|
const response = {
|
|
51
|
-
data: res.data,
|
|
59
|
+
data: decodeBinaryResponse(res.data, config.responseType, String(responseHeaders.get("Content-Type") ?? "")),
|
|
52
60
|
status: res.status,
|
|
53
61
|
statusText: "",
|
|
54
|
-
headers:
|
|
62
|
+
headers: responseHeaders,
|
|
55
63
|
config,
|
|
56
64
|
request: pending
|
|
57
65
|
};
|
|
@@ -63,18 +71,30 @@ function applyNativeHttp(client, opts) {
|
|
|
63
71
|
if (!opts.enabled) return;
|
|
64
72
|
const fallback = axios.getAdapter(client.defaults.adapter ?? axios.defaults.adapter);
|
|
65
73
|
const native = createNativeHttpAdapter(opts);
|
|
66
|
-
client.defaults.adapter = (config) => shouldUseNativeHttp(config) ? native(config) : fallback(config);
|
|
74
|
+
client.defaults.adapter = (config) => shouldUseNativeHttp(config, opts.exclude) ? native(config) : fallback(config);
|
|
67
75
|
}
|
|
68
76
|
function throwIfAborted(signal) {
|
|
69
77
|
if (signal?.aborted) throw new CanceledError("canceled");
|
|
70
78
|
}
|
|
71
|
-
function
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
function raceNative(pending, config, timeout) {
|
|
80
|
+
const signal = config.signal;
|
|
81
|
+
const abortable = !!signal && typeof signal.addEventListener === "function";
|
|
82
|
+
if (!abortable && !timeout) return pending;
|
|
83
|
+
if (signal?.aborted) return Promise.reject(new CanceledError("canceled"));
|
|
74
84
|
return new Promise((resolve, reject) => {
|
|
75
|
-
const onAbort = () =>
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
const onAbort = () => {
|
|
86
|
+
cleanup();
|
|
87
|
+
reject(new CanceledError("canceled"));
|
|
88
|
+
};
|
|
89
|
+
const timer = timeout ? setTimeout(() => {
|
|
90
|
+
cleanup();
|
|
91
|
+
reject(timeoutError(config, timeout, pending));
|
|
92
|
+
}, timeout) : void 0;
|
|
93
|
+
if (abortable) signal.addEventListener("abort", onAbort, { once: true });
|
|
94
|
+
function cleanup() {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
signal?.removeEventListener?.("abort", onAbort);
|
|
97
|
+
}
|
|
78
98
|
pending.then((value) => {
|
|
79
99
|
cleanup();
|
|
80
100
|
resolve(value);
|
|
@@ -84,6 +104,10 @@ function raceAbort(pending, signal) {
|
|
|
84
104
|
});
|
|
85
105
|
});
|
|
86
106
|
}
|
|
107
|
+
function timeoutError(config, timeout, request) {
|
|
108
|
+
const code = config.transitional?.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED;
|
|
109
|
+
return new AxiosError(config.timeoutErrorMessage || `timeout of ${timeout}ms exceeded`, code, config, request);
|
|
110
|
+
}
|
|
87
111
|
function flattenHeaders(headers) {
|
|
88
112
|
const out = {};
|
|
89
113
|
if (!headers) return out;
|
|
@@ -109,6 +133,86 @@ function normalizeBody(config) {
|
|
|
109
133
|
} catch {}
|
|
110
134
|
return data;
|
|
111
135
|
}
|
|
136
|
+
async function encodeBody(config, headers) {
|
|
137
|
+
const data = config.data;
|
|
138
|
+
if (data instanceof FormData) return {
|
|
139
|
+
data: await encodeFormData(data),
|
|
140
|
+
dataType: "formData",
|
|
141
|
+
contentType: "multipart/form-data"
|
|
142
|
+
};
|
|
143
|
+
const bytes = await binaryOf(data);
|
|
144
|
+
if (!bytes) return { data: normalizeBody(config) };
|
|
145
|
+
const explicit = headerValue(headers, "Content-Type");
|
|
146
|
+
const contentType = explicit && !explicit.includes("application/x-www-form-urlencoded") ? explicit : data instanceof Blob && data.type || "application/octet-stream";
|
|
147
|
+
return {
|
|
148
|
+
data: bytesToBase64(bytes),
|
|
149
|
+
dataType: "file",
|
|
150
|
+
contentType
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
async function encodeFormData(form) {
|
|
154
|
+
const pairs = [];
|
|
155
|
+
form.forEach((value, key) => pairs.push([key, value]));
|
|
156
|
+
const entries = [];
|
|
157
|
+
for (const [key, value] of pairs) {
|
|
158
|
+
if (typeof value === "string") {
|
|
159
|
+
entries.push({
|
|
160
|
+
key,
|
|
161
|
+
value,
|
|
162
|
+
type: "string"
|
|
163
|
+
});
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
entries.push({
|
|
167
|
+
key,
|
|
168
|
+
value: bytesToBase64(new Uint8Array(await value.arrayBuffer())),
|
|
169
|
+
type: "base64File",
|
|
170
|
+
fileName: value.name || "blob",
|
|
171
|
+
contentType: value.type || "application/octet-stream"
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return entries;
|
|
175
|
+
}
|
|
176
|
+
async function binaryOf(data) {
|
|
177
|
+
if (data instanceof Blob) return new Uint8Array(await data.arrayBuffer());
|
|
178
|
+
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
179
|
+
if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
function headerValue(headers, name) {
|
|
183
|
+
const wanted = name.toLowerCase();
|
|
184
|
+
for (const key of Object.keys(headers)) if (key.toLowerCase() === wanted) return headers[key];
|
|
185
|
+
}
|
|
186
|
+
function setContentType(headers, value) {
|
|
187
|
+
for (const key of Object.keys(headers)) if (key.toLowerCase() === "content-type") delete headers[key];
|
|
188
|
+
headers["Content-Type"] = value;
|
|
189
|
+
}
|
|
190
|
+
function decodeBinaryResponse(data, type, contentType) {
|
|
191
|
+
if (type !== "arraybuffer" && type !== "blob" || typeof data !== "string" || contentType.includes("application/json")) return data;
|
|
192
|
+
if (/[ \t\f]/.test(data)) return data;
|
|
193
|
+
let bytes;
|
|
194
|
+
try {
|
|
195
|
+
bytes = base64ToBytes(data);
|
|
196
|
+
} catch {
|
|
197
|
+
return data;
|
|
198
|
+
}
|
|
199
|
+
return type === "blob" ? new Blob([bytes], { type: contentType }) : bytes.buffer;
|
|
200
|
+
}
|
|
201
|
+
function bytesToBase64(bytes) {
|
|
202
|
+
const native = bytes.toBase64;
|
|
203
|
+
if (typeof native === "function") return native.call(bytes);
|
|
204
|
+
let out = "";
|
|
205
|
+
for (let i = 0; i < bytes.length; i += BASE64_CHUNK) out += btoa(String.fromCharCode.apply(null, bytes.subarray(i, i + BASE64_CHUNK)));
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
function base64ToBytes(text) {
|
|
209
|
+
const native = Uint8Array.fromBase64;
|
|
210
|
+
if (typeof native === "function") return native(text);
|
|
211
|
+
const binary = atob(text);
|
|
212
|
+
const bytes = new Uint8Array(binary.length);
|
|
213
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
214
|
+
return bytes;
|
|
215
|
+
}
|
|
112
216
|
function mapResponseType(type) {
|
|
113
217
|
switch (type) {
|
|
114
218
|
case "blob": return "blob";
|