@camera.ui/transport 0.0.30 → 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.js +31 -14
- 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 +14 -13
- 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;
|
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,
|
|
@@ -54,14 +59,14 @@ function createHttpTransport(options = {}) {
|
|
|
54
59
|
if (!currentTarget) return;
|
|
55
60
|
for (const waiter of [...targetWaiters]) waiter.resolve();
|
|
56
61
|
}
|
|
57
|
-
function waitForNewAccess(staleAccess, signal) {
|
|
58
|
-
if (currentTarget && currentTarget.tokens.access !== staleAccess) return Promise.resolve();
|
|
62
|
+
function waitForNewAccess(staleAccess, signal, refresh) {
|
|
63
|
+
if (currentTarget && currentTarget.tokens.access !== staleAccess) return Promise.resolve(true);
|
|
59
64
|
return new Promise((resolve, reject) => {
|
|
60
65
|
const waiter = {
|
|
61
66
|
staleAccess,
|
|
62
|
-
resolve: () => {
|
|
67
|
+
resolve: (fresh) => {
|
|
63
68
|
cleanup();
|
|
64
|
-
resolve();
|
|
69
|
+
resolve(fresh);
|
|
65
70
|
},
|
|
66
71
|
reject: (reason) => {
|
|
67
72
|
cleanup();
|
|
@@ -81,11 +86,14 @@ function createHttpTransport(options = {}) {
|
|
|
81
86
|
}
|
|
82
87
|
signal?.addEventListener?.("abort", onAbort);
|
|
83
88
|
accessWaiters.add(waiter);
|
|
89
|
+
refresh?.then((coming) => {
|
|
90
|
+
if (!coming) waiter.resolve(false);
|
|
91
|
+
});
|
|
84
92
|
});
|
|
85
93
|
}
|
|
86
94
|
function flushAccessWaiters() {
|
|
87
95
|
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();
|
|
96
|
+
else if (currentTarget.tokens.access !== waiter.staleAccess) waiter.resolve(true);
|
|
89
97
|
}
|
|
90
98
|
client.interceptors.request.use(async (config) => {
|
|
91
99
|
if (!currentTarget) await waitForTarget(config.signal);
|
|
@@ -95,7 +103,6 @@ function createHttpTransport(options = {}) {
|
|
|
95
103
|
if (!config.baseURL) config.baseURL = `${target.endpoint.url}${apiPrefix}`;
|
|
96
104
|
if (options.authorize) await options.authorize(config, target);
|
|
97
105
|
else config.headers.set("Authorization", `Bearer ${target.tokens.access}`);
|
|
98
|
-
if (target.tokens.proxySession) config.headers.set("X-Proxy-Session", target.tokens.proxySession);
|
|
99
106
|
return config;
|
|
100
107
|
});
|
|
101
108
|
client.interceptors.response.use((response) => {
|
|
@@ -113,24 +120,34 @@ function createHttpTransport(options = {}) {
|
|
|
113
120
|
}
|
|
114
121
|
if (error.response.status === 401) {
|
|
115
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);
|
|
116
127
|
emitter.emit("auth-error", {
|
|
117
128
|
status: 401,
|
|
118
|
-
message
|
|
129
|
+
message,
|
|
130
|
+
access: tokenRejected ? config?._sentAccess : void 0,
|
|
131
|
+
holdFor: (refresh) => refreshes.push(refresh)
|
|
119
132
|
});
|
|
120
|
-
const config = error.config;
|
|
121
133
|
if (config?._sentAccess && !config._authRetried && !disposed) {
|
|
122
134
|
config._authRetried = true;
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
} catch {
|
|
126
|
-
return Promise.reject(error);
|
|
127
|
-
}
|
|
128
|
-
logger?.debug(`retry after token refresh (${config.url ?? "unknown-url"})`);
|
|
135
|
+
if (!await shouldRetryAuth(config._sentAccess, refreshes, config.signal)) return Promise.reject(error);
|
|
136
|
+
logger?.debug(`retry after 401 (${config.url ?? "unknown-url"})`);
|
|
129
137
|
return client(config);
|
|
130
138
|
}
|
|
131
139
|
}
|
|
132
140
|
return Promise.reject(error);
|
|
133
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
|
+
}
|
|
134
151
|
function markDown(reason) {
|
|
135
152
|
if (status.up || status.lastError !== reason) {
|
|
136
153
|
status = {
|
|
@@ -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";
|