@clipto/upload-browser 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/CHANGELOG.md +11 -0
- package/README.md +60 -0
- package/dist/index.cjs +614 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +75 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +576 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
- 提供基于 XHR adapter 的浏览器上传客户端,将无 HTTP response 的模糊错误统一为 `ERR_NETWORK`,并保留 HTTP、超时与取消语义。
|
|
6
|
+
- 为 PUT 请求增加 15 分钟硬超时,为 multipart part 签名和 single PUT 签名增加独立超时。
|
|
7
|
+
- 支持 multipart 网络错误、超时、429 与 5xx 重试,并保证分片重试最少退避 12 秒;明确 4xx 不重试。
|
|
8
|
+
- single PUT 遇到无 response 的瞬时 `ERR_NETWORK` 时,在限定窗口内重新签名重试。
|
|
9
|
+
- 提供 Safari ArrayBuffer 分片缓存和跨刷新虚拟源恢复,恢复时只读取当前分片而不拼接完整 Blob。
|
|
10
|
+
- 通过可注入的 `UploadPartStorage` 隔离具体持久化实现,并在写入后回读校验分片长度。
|
|
11
|
+
- 保留用户取消的 `UploadCanceledError` 语义,将缺失或损坏的缓存分片归一为不可重试的 `source_unreadable`。
|
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# @clipto/upload-browser
|
|
2
|
+
|
|
3
|
+
Clipto 浏览器上传适配包,为 `@clipto/uploader@0.3.0` 提供 XHR 传输错误归一、Blob 直传,以及可注入存储的 Safari multipart 字节缓存。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @clipto/upload-browser @clipto/uploader@0.3.0 axios@1.20.0
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 使用
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import {
|
|
15
|
+
createBrowserUploadHttpClient,
|
|
16
|
+
createLocalBlobUploader,
|
|
17
|
+
createSafariUploadCacheManager,
|
|
18
|
+
} from "@clipto/upload-browser";
|
|
19
|
+
|
|
20
|
+
const cache = createSafariUploadCacheManager({
|
|
21
|
+
storage: {
|
|
22
|
+
get: (key) => indexedStorage.get(key),
|
|
23
|
+
set: (key, value) => indexedStorage.set(key, value),
|
|
24
|
+
remove: (key) => indexedStorage.remove(key),
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const uploader = createLocalBlobUploader(
|
|
29
|
+
{
|
|
30
|
+
signatureApi,
|
|
31
|
+
httpClient: createBrowserUploadHttpClient(),
|
|
32
|
+
},
|
|
33
|
+
{ safariCache: cache },
|
|
34
|
+
);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
签名接口、任务持久化、业务上报与具体 IndexedDB 实现由宿主注入,本包不包含这些职责。Safari descriptor 保持 `version: 1` wire shape;恢复源应同时向 uploader 注入创建它的 cache manager。
|
|
38
|
+
|
|
39
|
+
## 公共接口
|
|
40
|
+
|
|
41
|
+
运行时导出:
|
|
42
|
+
|
|
43
|
+
- `createBrowserUploadHttpClient`
|
|
44
|
+
- `createLocalBlobUploader`
|
|
45
|
+
- `createSafariUploadCacheManager`
|
|
46
|
+
- `createSafariUploadSource`
|
|
47
|
+
- `isSafariUploadSource`
|
|
48
|
+
- `isSafariUploadCacheDescriptor`
|
|
49
|
+
- `isSafariMultipartCacheEligible`
|
|
50
|
+
|
|
51
|
+
类型导出:
|
|
52
|
+
|
|
53
|
+
- `UploadPartStorage`
|
|
54
|
+
- `SafariUploadCacheDescriptor`
|
|
55
|
+
- `SafariUploadSource`
|
|
56
|
+
- `SafariUploadCacheManager`
|
|
57
|
+
|
|
58
|
+
## 风险
|
|
59
|
+
|
|
60
|
+
Blob 直传、multipart 重试分类及 Safari 虚拟源读取依赖 `@clipto/uploader@0.3.0` 的私有方法。接点缺失时会尽量降级为普通 SDK 实例,但升级 uploader 前必须重新验证真实 XHR、重试和恢复流程。
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
createBrowserUploadHttpClient: () => createBrowserUploadHttpClient,
|
|
34
|
+
createLocalBlobUploader: () => createLocalBlobUploader,
|
|
35
|
+
createSafariUploadCacheManager: () => createSafariUploadCacheManager,
|
|
36
|
+
createSafariUploadSource: () => createSafariUploadSource,
|
|
37
|
+
isSafariMultipartCacheEligible: () => isSafariMultipartCacheEligible,
|
|
38
|
+
isSafariUploadCacheDescriptor: () => isSafariUploadCacheDescriptor,
|
|
39
|
+
isSafariUploadSource: () => isSafariUploadSource
|
|
40
|
+
});
|
|
41
|
+
module.exports = __toCommonJS(index_exports);
|
|
42
|
+
|
|
43
|
+
// src/browser-upload-http.ts
|
|
44
|
+
var import_axios = __toESM(require("axios"), 1);
|
|
45
|
+
function normalizeTransportError(cause, request) {
|
|
46
|
+
if (import_axios.default.isCancel(cause)) return cause;
|
|
47
|
+
if (!import_axios.default.isAxiosError(cause)) return cause;
|
|
48
|
+
const axiosError = cause;
|
|
49
|
+
if (axiosError.response) return axiosError;
|
|
50
|
+
if (request.signal?.aborted || axiosError.code === "ERR_CANCELED") {
|
|
51
|
+
return new import_axios.default.CanceledError();
|
|
52
|
+
}
|
|
53
|
+
if (["ECONNABORTED", "ETIMEDOUT"].includes(axiosError.code || "")) {
|
|
54
|
+
return axiosError;
|
|
55
|
+
}
|
|
56
|
+
const error = new import_axios.default.AxiosError(
|
|
57
|
+
"Browser upload request failed without an HTTP response",
|
|
58
|
+
"ERR_NETWORK",
|
|
59
|
+
request,
|
|
60
|
+
axiosError.request
|
|
61
|
+
);
|
|
62
|
+
error.cause = axiosError;
|
|
63
|
+
return error;
|
|
64
|
+
}
|
|
65
|
+
function withNormalizedTransportErrors(adapter) {
|
|
66
|
+
return async (request) => {
|
|
67
|
+
try {
|
|
68
|
+
return await adapter(request);
|
|
69
|
+
} catch (cause) {
|
|
70
|
+
throw normalizeTransportError(cause, request);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function createBrowserUploadHttpClient(readRestoredParts = false) {
|
|
75
|
+
if (typeof XMLHttpRequest === "undefined") return void 0;
|
|
76
|
+
const client = import_axios.default.create({
|
|
77
|
+
adapter: "xhr",
|
|
78
|
+
timeout: 0,
|
|
79
|
+
maxBodyLength: Infinity,
|
|
80
|
+
maxContentLength: Infinity,
|
|
81
|
+
proxy: false
|
|
82
|
+
});
|
|
83
|
+
const nativeAdapter = withNormalizedTransportErrors(
|
|
84
|
+
import_axios.default.getAdapter(client.defaults.adapter)
|
|
85
|
+
);
|
|
86
|
+
const guardedRestore = readRestoredParts && typeof navigator !== "undefined" && /Safari/.test(navigator.userAgent) && !/Chrome|Chromium|CriOS|Edg|Android/.test(navigator.userAgent);
|
|
87
|
+
if (guardedRestore) {
|
|
88
|
+
let readFailure;
|
|
89
|
+
client.defaults.adapter = async (request) => {
|
|
90
|
+
if (request.signal?.aborted) throw new import_axios.default.CanceledError();
|
|
91
|
+
if (readFailure) throw readFailure;
|
|
92
|
+
if (request.method !== "put" || !(request.data instanceof Blob) || request.data.size === 0) {
|
|
93
|
+
return nativeAdapter(request);
|
|
94
|
+
}
|
|
95
|
+
if (request.data.size > 5 * 1024 * 1024) {
|
|
96
|
+
throw new import_axios.default.AxiosError(
|
|
97
|
+
"Restored part exceeds read budget",
|
|
98
|
+
"UPLOAD_RESTORE_READ_LIMIT"
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const blob = request.data;
|
|
102
|
+
const failedRead = (cause) => {
|
|
103
|
+
const error = new import_axios.default.AxiosError(
|
|
104
|
+
"Cached upload part could not be read. Please select the file again.",
|
|
105
|
+
"UPLOAD_RESTORE_READ_FAILED"
|
|
106
|
+
);
|
|
107
|
+
error.cause = cause;
|
|
108
|
+
readFailure = error;
|
|
109
|
+
return error;
|
|
110
|
+
};
|
|
111
|
+
const buffer = await new Promise((resolve, reject) => {
|
|
112
|
+
const reader = new FileReader();
|
|
113
|
+
let done = false;
|
|
114
|
+
const finish = (error) => {
|
|
115
|
+
if (done) return;
|
|
116
|
+
done = true;
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
request.signal?.removeEventListener?.("abort", onAbort);
|
|
119
|
+
if (error) {
|
|
120
|
+
reject(error);
|
|
121
|
+
if (reader.readyState === 1) reader.abort();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const result = reader.result;
|
|
125
|
+
if (!(result instanceof ArrayBuffer) || result.byteLength !== blob.size) {
|
|
126
|
+
reject(
|
|
127
|
+
failedRead(
|
|
128
|
+
new DOMException("Incomplete part read", "NotReadableError")
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
resolve(result);
|
|
134
|
+
};
|
|
135
|
+
const onAbort = () => finish(new import_axios.default.CanceledError());
|
|
136
|
+
const timer = setTimeout(
|
|
137
|
+
() => finish(
|
|
138
|
+
new import_axios.default.AxiosError(
|
|
139
|
+
"Cached part read timed out",
|
|
140
|
+
"UPLOAD_RESTORE_READ_TIMEOUT"
|
|
141
|
+
)
|
|
142
|
+
),
|
|
143
|
+
6e4
|
|
144
|
+
);
|
|
145
|
+
reader.onload = () => finish();
|
|
146
|
+
reader.onerror = () => finish(failedRead(reader.error));
|
|
147
|
+
reader.onabort = onAbort;
|
|
148
|
+
request.signal?.addEventListener?.("abort", onAbort);
|
|
149
|
+
if (request.signal?.aborted) return onAbort();
|
|
150
|
+
try {
|
|
151
|
+
reader.readAsArrayBuffer(blob);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
finish(failedRead(error));
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
if (request.signal?.aborted) throw new import_axios.default.CanceledError();
|
|
157
|
+
if (readFailure) throw readFailure;
|
|
158
|
+
return nativeAdapter({ ...request, data: buffer });
|
|
159
|
+
};
|
|
160
|
+
} else {
|
|
161
|
+
client.defaults.adapter = nativeAdapter;
|
|
162
|
+
}
|
|
163
|
+
client.interceptors.request.use((config) => {
|
|
164
|
+
config.headers.delete("content-length");
|
|
165
|
+
return config;
|
|
166
|
+
});
|
|
167
|
+
return client;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/local-blob-upload.ts
|
|
171
|
+
var import_axios2 = __toESM(require("axios"), 1);
|
|
172
|
+
var import_uploader = require("@clipto/uploader");
|
|
173
|
+
var PUT_REQUEST_TIMEOUT_MS = 15 * 6e4;
|
|
174
|
+
var PART_SIGNATURE_TIMEOUT_MS = 3e4;
|
|
175
|
+
var SINGLE_SIGNATURE_TIMEOUT_MS = 3e4;
|
|
176
|
+
var PART_RETRY_DELAY_MS = 12e3;
|
|
177
|
+
var normalizeRetryMessage = (message) => message.replace(
|
|
178
|
+
/^(part .* attempt \d+ failed, retry in )\d+ms:/,
|
|
179
|
+
`$1${PART_RETRY_DELAY_MS}ms:`
|
|
180
|
+
);
|
|
181
|
+
async function withUploadWatchdog(signal, timeoutMs, code, execute) {
|
|
182
|
+
if (signal.aborted) throw new import_uploader.UploadCanceledError();
|
|
183
|
+
const controller = new AbortController();
|
|
184
|
+
let timer;
|
|
185
|
+
let onAbort = () => {
|
|
186
|
+
};
|
|
187
|
+
let reset = () => {
|
|
188
|
+
};
|
|
189
|
+
let finished = false;
|
|
190
|
+
const disarm = () => clearTimeout(timer);
|
|
191
|
+
const stopped = new Promise((_, reject) => {
|
|
192
|
+
onAbort = () => {
|
|
193
|
+
disarm();
|
|
194
|
+
reject(new import_uploader.UploadCanceledError());
|
|
195
|
+
controller.abort();
|
|
196
|
+
};
|
|
197
|
+
reset = () => {
|
|
198
|
+
if (finished || controller.signal.aborted) return;
|
|
199
|
+
disarm();
|
|
200
|
+
timer = setTimeout(() => {
|
|
201
|
+
reject(new import_axios2.default.AxiosError("Upload made no progress", code));
|
|
202
|
+
controller.abort();
|
|
203
|
+
}, timeoutMs);
|
|
204
|
+
};
|
|
205
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
206
|
+
reset();
|
|
207
|
+
});
|
|
208
|
+
try {
|
|
209
|
+
return await Promise.race([
|
|
210
|
+
execute(controller.signal, reset, disarm),
|
|
211
|
+
stopped
|
|
212
|
+
]);
|
|
213
|
+
} finally {
|
|
214
|
+
finished = true;
|
|
215
|
+
disarm();
|
|
216
|
+
signal.removeEventListener("abort", onAbort);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
var isRecoverableUploadError = (error) => {
|
|
220
|
+
const cause = (0, import_uploader.isUploadError)(error) ? error.cause : error;
|
|
221
|
+
if (!import_axios2.default.isAxiosError(cause) || cause.code === "ERR_CANCELED") return false;
|
|
222
|
+
const status = cause.response?.status;
|
|
223
|
+
if (status) return status === 429 || status >= 500 && status <= 599;
|
|
224
|
+
return [
|
|
225
|
+
"ERR_NETWORK",
|
|
226
|
+
"ECONNABORTED",
|
|
227
|
+
"ETIMEDOUT",
|
|
228
|
+
"UPLOAD_PUT_TIMEOUT"
|
|
229
|
+
].includes(cause.code || "");
|
|
230
|
+
};
|
|
231
|
+
var isTransientSingleNetworkError = (error) => (0, import_uploader.isUploadError)(error) && error.phase === "putSingle" && import_axios2.default.isAxiosError(error.cause) && error.cause.code === "ERR_NETWORK" && !error.cause.response;
|
|
232
|
+
var isSafariPartRef = (value) => Boolean(
|
|
233
|
+
value && typeof value === "object" && "kind" in value && value.kind === "safari-array-buffer-part"
|
|
234
|
+
);
|
|
235
|
+
function createLocalBlobUploader(config, options = {}) {
|
|
236
|
+
const uploader = new import_uploader.CliptoUploader({ ...config, maxPartRetries: 3 });
|
|
237
|
+
const internals = uploader;
|
|
238
|
+
if (typeof internals.blobToArrayBuffer !== "function") return uploader;
|
|
239
|
+
Object.defineProperty(uploader, "blobToArrayBuffer", {
|
|
240
|
+
value: async (blob) => blob,
|
|
241
|
+
configurable: false,
|
|
242
|
+
writable: false
|
|
243
|
+
});
|
|
244
|
+
const uploadSingle = typeof internals.uploadSingle === "function" ? internals.uploadSingle.bind(uploader) : void 0;
|
|
245
|
+
const putData = typeof internals.putData === "function" ? internals.putData.bind(uploader) : void 0;
|
|
246
|
+
if (!uploadSingle || !putData) return uploader;
|
|
247
|
+
const beforeRetryPut = /* @__PURE__ */ new WeakMap();
|
|
248
|
+
const beforePartPut = /* @__PURE__ */ new WeakMap();
|
|
249
|
+
if (typeof internals.uploadPartOnce === "function" && typeof internals.sleep === "function" && typeof internals.emitWarning === "function" && typeof internals.isRetryable === "function") {
|
|
250
|
+
const uploadPartOnce = internals.uploadPartOnce.bind(uploader);
|
|
251
|
+
const sleep = internals.sleep.bind(uploader);
|
|
252
|
+
const emitWarning = internals.emitWarning.bind(uploader);
|
|
253
|
+
const logger = config.logger || console;
|
|
254
|
+
Object.defineProperty(uploader, "logger", {
|
|
255
|
+
value: {
|
|
256
|
+
info: logger.info.bind(logger),
|
|
257
|
+
error: logger.error.bind(logger),
|
|
258
|
+
warn: (message, ...args) => logger.warn(
|
|
259
|
+
message === void 0 ? message : normalizeRetryMessage(message),
|
|
260
|
+
...args
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
Object.defineProperty(uploader, "emitWarning", {
|
|
265
|
+
value: (onWarning, warning) => emitWarning(
|
|
266
|
+
onWarning,
|
|
267
|
+
warning.code === "PART_RETRY" ? { ...warning, message: normalizeRetryMessage(warning.message) } : warning
|
|
268
|
+
)
|
|
269
|
+
});
|
|
270
|
+
Object.defineProperty(uploader, "isRetryable", {
|
|
271
|
+
value: isRecoverableUploadError
|
|
272
|
+
});
|
|
273
|
+
Object.defineProperty(uploader, "sleep", {
|
|
274
|
+
value: (ms, signal) => sleep(Math.max(ms, PART_RETRY_DELAY_MS), signal)
|
|
275
|
+
});
|
|
276
|
+
Object.defineProperty(uploader, "uploadPartOnce", {
|
|
277
|
+
value: (...args) => withUploadWatchdog(
|
|
278
|
+
args[6].signal,
|
|
279
|
+
PART_SIGNATURE_TIMEOUT_MS,
|
|
280
|
+
"UPLOAD_PART_SIGNATURE_TIMEOUT",
|
|
281
|
+
async (signal, _reset, disarm) => {
|
|
282
|
+
beforePartPut.set(signal, disarm);
|
|
283
|
+
try {
|
|
284
|
+
const nextArgs = [...args];
|
|
285
|
+
if (isSafariPartRef(nextArgs[0])) {
|
|
286
|
+
try {
|
|
287
|
+
if (!options.safariCache)
|
|
288
|
+
throw new Error("Missing Safari cache");
|
|
289
|
+
nextArgs[0] = await options.safariCache.getPart(
|
|
290
|
+
nextArgs[0].descriptor,
|
|
291
|
+
nextArgs[3],
|
|
292
|
+
nextArgs[4]
|
|
293
|
+
);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
throw new import_uploader.UploadError({
|
|
296
|
+
code: "INVALID_ARGUMENT",
|
|
297
|
+
message: "source_unreadable",
|
|
298
|
+
phase: "prepare",
|
|
299
|
+
cause: error,
|
|
300
|
+
objectKey: nextArgs[1],
|
|
301
|
+
uploadId: nextArgs[2],
|
|
302
|
+
partNumber: nextArgs[3],
|
|
303
|
+
resumable: false,
|
|
304
|
+
retryable: false
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
nextArgs[6] = { signal };
|
|
309
|
+
return await uploadPartOnce(...nextArgs);
|
|
310
|
+
} finally {
|
|
311
|
+
beforePartPut.delete(signal);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
).catch((error) => {
|
|
315
|
+
if (import_axios2.default.isCancel(error) || import_axios2.default.isAxiosError(error) && error.code === "ERR_CANCELED") {
|
|
316
|
+
throw new import_uploader.UploadCanceledError(args[1]);
|
|
317
|
+
}
|
|
318
|
+
if (import_axios2.default.isAxiosError(error) && error.code === "UPLOAD_PART_SIGNATURE_TIMEOUT") {
|
|
319
|
+
throw new import_uploader.UploadError({
|
|
320
|
+
code: "SIGNATURE_API_ERROR",
|
|
321
|
+
message: "Part signature timed out",
|
|
322
|
+
phase: "signPart",
|
|
323
|
+
cause: error,
|
|
324
|
+
objectKey: args[1],
|
|
325
|
+
uploadId: args[2],
|
|
326
|
+
partNumber: args[3],
|
|
327
|
+
resumable: true,
|
|
328
|
+
retryable: false
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
throw error;
|
|
332
|
+
})
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
Object.defineProperty(uploader, "putData", {
|
|
336
|
+
value: (...args) => {
|
|
337
|
+
if (args[5].aborted) throw new import_uploader.UploadCanceledError();
|
|
338
|
+
beforeRetryPut.get(args[5])?.();
|
|
339
|
+
beforePartPut.get(args[5])?.();
|
|
340
|
+
return withUploadWatchdog(
|
|
341
|
+
args[5],
|
|
342
|
+
PUT_REQUEST_TIMEOUT_MS,
|
|
343
|
+
"UPLOAD_PUT_TIMEOUT",
|
|
344
|
+
(signal) => {
|
|
345
|
+
const nextArgs = [...args];
|
|
346
|
+
nextArgs[5] = signal;
|
|
347
|
+
nextArgs[6] = (loaded) => {
|
|
348
|
+
if (signal.aborted) return;
|
|
349
|
+
args[6]?.(loaded);
|
|
350
|
+
};
|
|
351
|
+
return putData(...nextArgs);
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
Object.defineProperty(uploader, "uploadSingle", {
|
|
357
|
+
value: async (...args) => {
|
|
358
|
+
const signal = args[6].signal;
|
|
359
|
+
let deadline = 0;
|
|
360
|
+
let lastError;
|
|
361
|
+
for (let attempt = 0; attempt <= 2; attempt += 1) {
|
|
362
|
+
if (signal.aborted) throw new import_uploader.UploadCanceledError(args[1]);
|
|
363
|
+
if (attempt === 0) {
|
|
364
|
+
try {
|
|
365
|
+
return await withUploadWatchdog(
|
|
366
|
+
signal,
|
|
367
|
+
SINGLE_SIGNATURE_TIMEOUT_MS,
|
|
368
|
+
"UPLOAD_SINGLE_SIGNATURE_TIMEOUT",
|
|
369
|
+
async (attemptSignal, _reset, disarm) => {
|
|
370
|
+
beforeRetryPut.set(attemptSignal, disarm);
|
|
371
|
+
try {
|
|
372
|
+
const nextArgs = [...args];
|
|
373
|
+
nextArgs[6] = { signal: attemptSignal };
|
|
374
|
+
return await uploadSingle(...nextArgs);
|
|
375
|
+
} finally {
|
|
376
|
+
beforeRetryPut.delete(attemptSignal);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
if (import_axios2.default.isAxiosError(error) && error.code === "UPLOAD_SINGLE_SIGNATURE_TIMEOUT") {
|
|
382
|
+
throw new import_uploader.UploadError({
|
|
383
|
+
code: "SIGNATURE_API_ERROR",
|
|
384
|
+
message: "Single upload signature timed out",
|
|
385
|
+
phase: "signSingle",
|
|
386
|
+
cause: error,
|
|
387
|
+
objectKey: args[1],
|
|
388
|
+
retryable: false
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
if (!isTransientSingleNetworkError(error)) throw error;
|
|
392
|
+
lastError = error;
|
|
393
|
+
deadline = Date.now() + 15e3;
|
|
394
|
+
}
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
if (Date.now() >= deadline) throw lastError;
|
|
398
|
+
const controller = new AbortController();
|
|
399
|
+
let timer;
|
|
400
|
+
let delayTimer;
|
|
401
|
+
let onAbort = () => {
|
|
402
|
+
};
|
|
403
|
+
try {
|
|
404
|
+
const stopped = new Promise((_, reject) => {
|
|
405
|
+
onAbort = () => {
|
|
406
|
+
controller.abort();
|
|
407
|
+
reject(new import_uploader.UploadCanceledError(args[1]));
|
|
408
|
+
};
|
|
409
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
410
|
+
timer = setTimeout(
|
|
411
|
+
() => {
|
|
412
|
+
controller.abort();
|
|
413
|
+
reject(lastError);
|
|
414
|
+
},
|
|
415
|
+
Math.max(0, deadline - Date.now())
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
await Promise.race([
|
|
419
|
+
new Promise((resolve) => {
|
|
420
|
+
delayTimer = setTimeout(resolve, 500 * attempt);
|
|
421
|
+
}),
|
|
422
|
+
stopped
|
|
423
|
+
]);
|
|
424
|
+
if (signal.aborted) throw new import_uploader.UploadCanceledError(args[1]);
|
|
425
|
+
if (Date.now() >= deadline) throw lastError;
|
|
426
|
+
args[5].reset();
|
|
427
|
+
args[5].report(0);
|
|
428
|
+
const retryArgs = [...args];
|
|
429
|
+
retryArgs[6] = { signal: controller.signal };
|
|
430
|
+
beforeRetryPut.set(controller.signal, () => {
|
|
431
|
+
if (signal.aborted) throw new import_uploader.UploadCanceledError(args[1]);
|
|
432
|
+
if (controller.signal.aborted || Date.now() >= deadline)
|
|
433
|
+
throw lastError;
|
|
434
|
+
clearTimeout(timer);
|
|
435
|
+
});
|
|
436
|
+
return await Promise.race([uploadSingle(...retryArgs), stopped]);
|
|
437
|
+
} catch (error) {
|
|
438
|
+
if (signal.aborted) throw new import_uploader.UploadCanceledError(args[1]);
|
|
439
|
+
if (!isTransientSingleNetworkError(error)) throw error;
|
|
440
|
+
lastError = error;
|
|
441
|
+
} finally {
|
|
442
|
+
beforeRetryPut.delete(controller.signal);
|
|
443
|
+
clearTimeout(timer);
|
|
444
|
+
clearTimeout(delayTimer);
|
|
445
|
+
signal.removeEventListener("abort", onAbort);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
throw lastError;
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
return uploader;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// src/safari-upload-part-cache.ts
|
|
455
|
+
var DEFAULT_PART_SIZE = 5 * 1024 * 1024;
|
|
456
|
+
var CACHE_VERSION = 1;
|
|
457
|
+
var CACHE_PREFIX = "safari_upload_part_cache";
|
|
458
|
+
var SafariUploadSourceUnreadableError = class extends Error {
|
|
459
|
+
constructor() {
|
|
460
|
+
super("source_unreadable");
|
|
461
|
+
this.code = "SOURCE_UNREADABLE";
|
|
462
|
+
this.retryable = false;
|
|
463
|
+
this.name = "SafariUploadSourceUnreadableError";
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
var manifestKey = (cacheId) => `${CACHE_PREFIX}:${cacheId}:manifest`;
|
|
467
|
+
var partKey = (cacheId, partNumber) => `${CACHE_PREFIX}:${cacheId}:part:${partNumber}`;
|
|
468
|
+
function isSafariMultipartCacheEligible(size, userAgent = typeof navigator === "undefined" ? "" : navigator.userAgent) {
|
|
469
|
+
return size > DEFAULT_PART_SIZE && /Safari/.test(userAgent) && !/Chrome|Chromium|CriOS|Edg|Android/.test(userAgent);
|
|
470
|
+
}
|
|
471
|
+
function isSafariUploadCacheDescriptor(value) {
|
|
472
|
+
if (!value || typeof value !== "object") return false;
|
|
473
|
+
const descriptor = value;
|
|
474
|
+
return descriptor.kind === "safari-array-buffer-parts" && descriptor.version === CACHE_VERSION && typeof descriptor.cacheId === "string" && descriptor.cacheId.length > 0 && Number.isSafeInteger(descriptor.totalSize) && (descriptor.totalSize || 0) > 0 && Number.isSafeInteger(descriptor.partSize) && (descriptor.partSize || 0) > 0 && descriptor.partCount === Math.ceil((descriptor.totalSize || 0) / (descriptor.partSize || 1)) && Boolean(descriptor.file) && typeof descriptor.file?.name === "string" && typeof descriptor.file?.type === "string" && typeof descriptor.file?.lastModified === "number";
|
|
475
|
+
}
|
|
476
|
+
function createSafariUploadSource(descriptor) {
|
|
477
|
+
return {
|
|
478
|
+
kind: "safari-array-buffer-source",
|
|
479
|
+
descriptor,
|
|
480
|
+
size: descriptor.totalSize,
|
|
481
|
+
type: descriptor.file.type,
|
|
482
|
+
name: descriptor.file.name,
|
|
483
|
+
lastModified: descriptor.file.lastModified,
|
|
484
|
+
slice(start = 0, end = descriptor.totalSize) {
|
|
485
|
+
const partNumber = Math.floor(start / descriptor.partSize) + 1;
|
|
486
|
+
return createPartRef(descriptor, partNumber, end - start);
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function isSafariUploadSource(value) {
|
|
491
|
+
return Boolean(
|
|
492
|
+
value && typeof value === "object" && value.kind === "safari-array-buffer-source"
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
function createSafariUploadCacheManager({
|
|
496
|
+
storage,
|
|
497
|
+
logger = console
|
|
498
|
+
}) {
|
|
499
|
+
const cleanup = async (descriptor) => {
|
|
500
|
+
await Promise.allSettled([
|
|
501
|
+
...Array.from(
|
|
502
|
+
{ length: descriptor.partCount },
|
|
503
|
+
(_, index) => storage.remove(partKey(descriptor.cacheId, index + 1))
|
|
504
|
+
),
|
|
505
|
+
storage.remove(manifestKey(descriptor.cacheId))
|
|
506
|
+
]);
|
|
507
|
+
};
|
|
508
|
+
const create = async (file, cacheId, options) => {
|
|
509
|
+
const partSize = options?.partSize ?? DEFAULT_PART_SIZE;
|
|
510
|
+
const descriptor = {
|
|
511
|
+
kind: "safari-array-buffer-parts",
|
|
512
|
+
version: CACHE_VERSION,
|
|
513
|
+
cacheId,
|
|
514
|
+
totalSize: file.size,
|
|
515
|
+
partSize,
|
|
516
|
+
partCount: Math.ceil(file.size / partSize),
|
|
517
|
+
file: options?.metadata ?? {
|
|
518
|
+
name: "name" in file && typeof file.name === "string" ? file.name : "upload",
|
|
519
|
+
type: file.type,
|
|
520
|
+
lastModified: "lastModified" in file && typeof file.lastModified === "number" ? file.lastModified : Date.now()
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
try {
|
|
524
|
+
for (let partNumber = 1; partNumber <= descriptor.partCount; partNumber++) {
|
|
525
|
+
const expected = expectedPartSize(descriptor, partNumber);
|
|
526
|
+
const start = (partNumber - 1) * partSize;
|
|
527
|
+
const bytes = await readBlobPart(file.slice(start, start + expected));
|
|
528
|
+
if (bytes.byteLength !== expected)
|
|
529
|
+
throw new Error("Invalid source part");
|
|
530
|
+
const key = partKey(cacheId, partNumber);
|
|
531
|
+
await storage.set(key, bytes);
|
|
532
|
+
const stored = await storage.get(key);
|
|
533
|
+
if (!(stored instanceof ArrayBuffer) || stored.byteLength !== expected) {
|
|
534
|
+
throw new Error("Invalid persisted part");
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
await storage.set(manifestKey(cacheId), descriptor);
|
|
538
|
+
const manifest = await storage.get(manifestKey(cacheId));
|
|
539
|
+
if (!isSafariUploadCacheDescriptor(manifest)) {
|
|
540
|
+
throw new Error("Invalid persisted manifest");
|
|
541
|
+
}
|
|
542
|
+
return descriptor;
|
|
543
|
+
} catch (error) {
|
|
544
|
+
await cleanup(descriptor);
|
|
545
|
+
logger.warn(
|
|
546
|
+
"Safari upload byte cache unavailable, falling back to Blob",
|
|
547
|
+
error
|
|
548
|
+
);
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
const validate = async (descriptor) => {
|
|
553
|
+
const stored = await storage.get(
|
|
554
|
+
manifestKey(descriptor.cacheId)
|
|
555
|
+
);
|
|
556
|
+
return isSafariUploadCacheDescriptor(stored) && stored.totalSize === descriptor.totalSize && stored.partSize === descriptor.partSize && stored.partCount === descriptor.partCount;
|
|
557
|
+
};
|
|
558
|
+
const getPart = async (descriptor, partNumber, expectedLength) => {
|
|
559
|
+
if (partNumber < 1 || partNumber > descriptor.partCount || expectedLength !== expectedPartSize(descriptor, partNumber)) {
|
|
560
|
+
throw new SafariUploadSourceUnreadableError();
|
|
561
|
+
}
|
|
562
|
+
try {
|
|
563
|
+
const bytes = await storage.get(
|
|
564
|
+
partKey(descriptor.cacheId, partNumber)
|
|
565
|
+
);
|
|
566
|
+
if (!(bytes instanceof ArrayBuffer) || bytes.byteLength !== expectedLength) {
|
|
567
|
+
throw new SafariUploadSourceUnreadableError();
|
|
568
|
+
}
|
|
569
|
+
return bytes;
|
|
570
|
+
} catch (error) {
|
|
571
|
+
if (error instanceof SafariUploadSourceUnreadableError) throw error;
|
|
572
|
+
throw new SafariUploadSourceUnreadableError();
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
return { create, validate, cleanup, getPart };
|
|
576
|
+
}
|
|
577
|
+
function readBlobPart(blob) {
|
|
578
|
+
if (typeof blob.arrayBuffer === "function") return blob.arrayBuffer();
|
|
579
|
+
return new Promise((resolve, reject) => {
|
|
580
|
+
const reader = new FileReader();
|
|
581
|
+
reader.onload = () => resolve(reader.result);
|
|
582
|
+
reader.onerror = () => reject(reader.error);
|
|
583
|
+
reader.readAsArrayBuffer(blob);
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
function expectedPartSize(descriptor, partNumber) {
|
|
587
|
+
return Math.min(
|
|
588
|
+
descriptor.partSize,
|
|
589
|
+
descriptor.totalSize - (partNumber - 1) * descriptor.partSize
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
function createPartRef(descriptor, partNumber, size) {
|
|
593
|
+
return {
|
|
594
|
+
kind: "safari-array-buffer-part",
|
|
595
|
+
descriptor,
|
|
596
|
+
partNumber,
|
|
597
|
+
size,
|
|
598
|
+
type: descriptor.file.type,
|
|
599
|
+
slice(start = 0, end = size) {
|
|
600
|
+
return createPartRef(descriptor, partNumber, end - start);
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
605
|
+
0 && (module.exports = {
|
|
606
|
+
createBrowserUploadHttpClient,
|
|
607
|
+
createLocalBlobUploader,
|
|
608
|
+
createSafariUploadCacheManager,
|
|
609
|
+
createSafariUploadSource,
|
|
610
|
+
isSafariMultipartCacheEligible,
|
|
611
|
+
isSafariUploadCacheDescriptor,
|
|
612
|
+
isSafariUploadSource
|
|
613
|
+
});
|
|
614
|
+
//# sourceMappingURL=index.cjs.map
|