@minhvuong/pirate-storage-client 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/README.md +18 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +393 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# @minhvuong/pirate-storage-client
|
|
2
|
+
|
|
3
|
+
Browser uploader for the consumer-owned `@minhvuong/pirate-storage-server` endpoint.
|
|
4
|
+
|
|
5
|
+
- file slicing below platform request-body limits
|
|
6
|
+
- bounded parallel part uploads
|
|
7
|
+
- whole-file SHA-256 before session creation or resume, so a same-name/size replacement is rejected
|
|
8
|
+
- progress, active-request pause/resume, cancellation, and retry
|
|
9
|
+
- resumable session IDs
|
|
10
|
+
- confirmed rollback: `cancel()` surfaces cleanup failures instead of claiming success
|
|
11
|
+
- optional separately-installed preview generators
|
|
12
|
+
|
|
13
|
+
The browser never receives Discord credentials and never uploads directly to Discord.
|
|
14
|
+
|
|
15
|
+
`pause()` aborts active browser multipart requests without aborting the durable session. The server
|
|
16
|
+
finishes safely tracking any provider request already accepted; `resume()` reuses those journaled
|
|
17
|
+
parts and sends only missing work. Once final manifest commit begins, the task is intentionally no
|
|
18
|
+
longer pausable.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { GeneratedPreview, PreviewGenerator, StorageReceipt } from "@minhvuong/pirate-storage";
|
|
2
|
+
export interface ClientUploadResult<ServerData = unknown> {
|
|
3
|
+
receipt: StorageReceipt;
|
|
4
|
+
serverData: ServerData;
|
|
5
|
+
}
|
|
6
|
+
export interface ClientUploadProgress {
|
|
7
|
+
phase: "preview" | "hashing" | "uploading" | "finalizing";
|
|
8
|
+
uploadedBytes: number;
|
|
9
|
+
hashedBytes: number;
|
|
10
|
+
totalBytes: number;
|
|
11
|
+
completedParts: number;
|
|
12
|
+
totalParts: number;
|
|
13
|
+
percentage: number;
|
|
14
|
+
}
|
|
15
|
+
export interface CreateUploaderOptions {
|
|
16
|
+
endpoint?: string;
|
|
17
|
+
headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
|
18
|
+
fetch?: typeof globalThis.fetch;
|
|
19
|
+
previewGenerators?: readonly PreviewGenerator[];
|
|
20
|
+
}
|
|
21
|
+
export interface ClientUploadOptions<Input = unknown> {
|
|
22
|
+
input?: Input;
|
|
23
|
+
sessionId?: string;
|
|
24
|
+
concurrency?: number;
|
|
25
|
+
retries?: number;
|
|
26
|
+
preview?: GeneratedPreview | false;
|
|
27
|
+
previewGenerator?: string | false;
|
|
28
|
+
onProgress?: (progress: ClientUploadProgress) => void;
|
|
29
|
+
onPreviewProgress?: (progress: {
|
|
30
|
+
stage: string;
|
|
31
|
+
percentage: number;
|
|
32
|
+
}) => void;
|
|
33
|
+
}
|
|
34
|
+
export interface UploadTask<Result> {
|
|
35
|
+
readonly done: Promise<Result>;
|
|
36
|
+
readonly sessionId?: string;
|
|
37
|
+
readonly state: "running" | "paused" | "failed" | "complete" | "cancelled";
|
|
38
|
+
pause(): void;
|
|
39
|
+
resume(): void;
|
|
40
|
+
cancel(): Promise<void>;
|
|
41
|
+
retryFailed(): Promise<Result>;
|
|
42
|
+
}
|
|
43
|
+
export declare class StorageUploader {
|
|
44
|
+
#private;
|
|
45
|
+
constructor(options?: CreateUploaderOptions);
|
|
46
|
+
createUpload<ServerData = unknown, Input = unknown>(route: string, file: File, options?: ClientUploadOptions<Input>): UploadTask<ClientUploadResult<ServerData>>;
|
|
47
|
+
uploadFiles<ServerData = unknown, Input = unknown>(route: string, files: readonly File[], options?: ClientUploadOptions<Input> & {
|
|
48
|
+
fileConcurrency?: number;
|
|
49
|
+
}): Promise<Array<ClientUploadResult<ServerData>>>;
|
|
50
|
+
uploadDirect<ServerData = unknown, Input = unknown>(route: string, file: File, options?: Pick<ClientUploadOptions<Input>, "input">): Promise<ClientUploadResult<ServerData>>;
|
|
51
|
+
}
|
|
52
|
+
export declare function createUploader(options?: CreateUploaderOptions): StorageUploader;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
3
|
+
import { bytesToHex } from "@noble/hashes/utils.js";
|
|
4
|
+
|
|
5
|
+
class MultipartUploadTask {
|
|
6
|
+
done;
|
|
7
|
+
#state = "running";
|
|
8
|
+
#sessionId;
|
|
9
|
+
#abortController = new AbortController;
|
|
10
|
+
#pauseController = new AbortController;
|
|
11
|
+
#pausable = true;
|
|
12
|
+
#resumeWaiters = [];
|
|
13
|
+
#execute;
|
|
14
|
+
#abortRemote;
|
|
15
|
+
constructor(execute, abortRemote) {
|
|
16
|
+
this.#execute = () => execute(this);
|
|
17
|
+
this.#abortRemote = abortRemote;
|
|
18
|
+
this.done = Promise.resolve().then(() => this.#run());
|
|
19
|
+
}
|
|
20
|
+
get sessionId() {
|
|
21
|
+
return this.#sessionId;
|
|
22
|
+
}
|
|
23
|
+
get state() {
|
|
24
|
+
return this.#state;
|
|
25
|
+
}
|
|
26
|
+
get signal() {
|
|
27
|
+
return this.#abortController.signal;
|
|
28
|
+
}
|
|
29
|
+
requestSignal() {
|
|
30
|
+
return AbortSignal.any([
|
|
31
|
+
this.#abortController.signal,
|
|
32
|
+
this.#pauseController.signal
|
|
33
|
+
]);
|
|
34
|
+
}
|
|
35
|
+
setSessionId(value) {
|
|
36
|
+
this.#sessionId = value;
|
|
37
|
+
}
|
|
38
|
+
pause() {
|
|
39
|
+
if (this.#state !== "running" || !this.#pausable)
|
|
40
|
+
return;
|
|
41
|
+
this.#state = "paused";
|
|
42
|
+
this.#pauseController.abort(new UploadPausedError);
|
|
43
|
+
}
|
|
44
|
+
resume() {
|
|
45
|
+
if (this.#state !== "paused")
|
|
46
|
+
return;
|
|
47
|
+
this.#pauseController = new AbortController;
|
|
48
|
+
this.#state = "running";
|
|
49
|
+
for (const resolve of this.#resumeWaiters.splice(0))
|
|
50
|
+
resolve();
|
|
51
|
+
}
|
|
52
|
+
async waitIfPaused() {
|
|
53
|
+
if (this.#state !== "paused")
|
|
54
|
+
return;
|
|
55
|
+
await new Promise((resolve, reject) => {
|
|
56
|
+
this.#resumeWaiters.push(resolve);
|
|
57
|
+
this.signal.addEventListener("abort", () => reject(this.signal.reason), {
|
|
58
|
+
once: true
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
markFinalizing() {
|
|
63
|
+
this.#pausable = false;
|
|
64
|
+
}
|
|
65
|
+
async cancel() {
|
|
66
|
+
if (this.#state === "complete" || this.#state === "cancelled")
|
|
67
|
+
return;
|
|
68
|
+
this.#state = "cancelled";
|
|
69
|
+
this.#abortController.abort(new DOMException("Upload cancelled", "AbortError"));
|
|
70
|
+
for (const resolve of this.#resumeWaiters.splice(0))
|
|
71
|
+
resolve();
|
|
72
|
+
await this.#abortRemote();
|
|
73
|
+
}
|
|
74
|
+
async retryFailed() {
|
|
75
|
+
if (this.#state !== "failed")
|
|
76
|
+
return this.done;
|
|
77
|
+
this.#state = "running";
|
|
78
|
+
this.#abortController = new AbortController;
|
|
79
|
+
this.#pauseController = new AbortController;
|
|
80
|
+
this.#pausable = true;
|
|
81
|
+
return this.#run();
|
|
82
|
+
}
|
|
83
|
+
async#run() {
|
|
84
|
+
try {
|
|
85
|
+
const result = await this.#execute();
|
|
86
|
+
this.#state = "complete";
|
|
87
|
+
return result;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (this.#state !== "cancelled")
|
|
90
|
+
this.#state = "failed";
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
class StorageUploader {
|
|
97
|
+
#endpoint;
|
|
98
|
+
#headers;
|
|
99
|
+
#fetch;
|
|
100
|
+
#previewGenerators;
|
|
101
|
+
constructor(options = {}) {
|
|
102
|
+
this.#endpoint = (options.endpoint ?? "/api/storage").replace(/\/$/, "");
|
|
103
|
+
this.#headers = options.headers;
|
|
104
|
+
this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
105
|
+
this.#previewGenerators = options.previewGenerators ?? [];
|
|
106
|
+
}
|
|
107
|
+
createUpload(route, file, options = {}) {
|
|
108
|
+
let task;
|
|
109
|
+
task = new MultipartUploadTask(() => this.#runMultipart(task, route, file, options), async () => {
|
|
110
|
+
if (!task.sessionId)
|
|
111
|
+
return;
|
|
112
|
+
const response = await this.#request(`${route}/multipart/${task.sessionId}`, {
|
|
113
|
+
method: "DELETE",
|
|
114
|
+
headers: inputHeader(options.input)
|
|
115
|
+
});
|
|
116
|
+
await ensureOk(response);
|
|
117
|
+
});
|
|
118
|
+
return task;
|
|
119
|
+
}
|
|
120
|
+
async uploadFiles(route, files, options = {}) {
|
|
121
|
+
const concurrency = positiveInteger(options.fileConcurrency ?? 2, "fileConcurrency");
|
|
122
|
+
const results = [];
|
|
123
|
+
results.length = files.length;
|
|
124
|
+
let cursor = 0;
|
|
125
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, files.length) }, async () => {
|
|
126
|
+
while (cursor < files.length) {
|
|
127
|
+
const index = cursor++;
|
|
128
|
+
const file = files[index];
|
|
129
|
+
if (file)
|
|
130
|
+
results[index] = await this.createUpload(route, file, options).done;
|
|
131
|
+
}
|
|
132
|
+
}));
|
|
133
|
+
return results;
|
|
134
|
+
}
|
|
135
|
+
async uploadDirect(route, file, options = {}) {
|
|
136
|
+
const response = await this.#request(route, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: {
|
|
139
|
+
...inputHeader(options.input),
|
|
140
|
+
"content-type": file.type || "application/octet-stream",
|
|
141
|
+
"x-storage-file-name": encodeURIComponent(file.name),
|
|
142
|
+
"x-storage-file-size": String(file.size)
|
|
143
|
+
},
|
|
144
|
+
body: file
|
|
145
|
+
});
|
|
146
|
+
return parseResponse(response);
|
|
147
|
+
}
|
|
148
|
+
async#runMultipart(task, route, file, options) {
|
|
149
|
+
const previewPromise = this.#preparePreview(file, options, task.signal);
|
|
150
|
+
const fileSha256 = await hashFile(file, task.signal, () => task.waitIfPaused(), (hashedBytes2) => {
|
|
151
|
+
options.onProgress?.({
|
|
152
|
+
phase: "hashing",
|
|
153
|
+
uploadedBytes: 0,
|
|
154
|
+
hashedBytes: hashedBytes2,
|
|
155
|
+
totalBytes: file.size,
|
|
156
|
+
completedParts: 0,
|
|
157
|
+
totalParts: 0,
|
|
158
|
+
percentage: Math.round(hashedBytes2 / file.size * 100)
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
let session;
|
|
162
|
+
if (task.sessionId ?? options.sessionId) {
|
|
163
|
+
const sessionId = task.sessionId ?? options.sessionId;
|
|
164
|
+
task.setSessionId(sessionId);
|
|
165
|
+
const response2 = await this.#request(`${route}/multipart/${sessionId}`, {
|
|
166
|
+
method: "GET",
|
|
167
|
+
headers: {
|
|
168
|
+
...inputHeader(options.input),
|
|
169
|
+
"x-storage-file-sha256": fileSha256
|
|
170
|
+
},
|
|
171
|
+
signal: task.signal
|
|
172
|
+
});
|
|
173
|
+
session = await parseResponse(response2);
|
|
174
|
+
if (session.status === "complete" && session.result) {
|
|
175
|
+
return session.result;
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
const response2 = await this.#request(`${route}/multipart`, {
|
|
179
|
+
method: "POST",
|
|
180
|
+
headers: { "content-type": "application/json" },
|
|
181
|
+
body: JSON.stringify({
|
|
182
|
+
file: {
|
|
183
|
+
name: file.name,
|
|
184
|
+
size: file.size,
|
|
185
|
+
contentType: file.type || "application/octet-stream",
|
|
186
|
+
sha256: fileSha256
|
|
187
|
+
},
|
|
188
|
+
input: options.input
|
|
189
|
+
}),
|
|
190
|
+
signal: task.signal
|
|
191
|
+
});
|
|
192
|
+
session = await parseResponse(response2);
|
|
193
|
+
task.setSessionId(session.sessionId);
|
|
194
|
+
}
|
|
195
|
+
const totalParts = Math.ceil(file.size / session.partSize);
|
|
196
|
+
const completed = new Set(session.completedParts);
|
|
197
|
+
const hashedBytes = file.size;
|
|
198
|
+
let uploadedBytes = [...completed].reduce((total, index) => total + Math.min(session.partSize, file.size - index * session.partSize), 0);
|
|
199
|
+
const concurrency = positiveInteger(options.concurrency ?? ("maxConcurrency" in session ? session.maxConcurrency ?? 3 : 3), "concurrency");
|
|
200
|
+
const retries = nonNegativeInteger(options.retries ?? 3, "retries");
|
|
201
|
+
const inFlight = new Set;
|
|
202
|
+
const allOperations = [];
|
|
203
|
+
const report = (phase) => {
|
|
204
|
+
options.onProgress?.({
|
|
205
|
+
phase,
|
|
206
|
+
uploadedBytes,
|
|
207
|
+
hashedBytes,
|
|
208
|
+
totalBytes: file.size,
|
|
209
|
+
completedParts: completed.size,
|
|
210
|
+
totalParts,
|
|
211
|
+
percentage: phase === "hashing" ? Math.round(Math.min(hashedBytes, file.size) / file.size * 100) : phase === "finalizing" ? 100 : Math.round(Math.min(uploadedBytes, file.size) / file.size * 100)
|
|
212
|
+
});
|
|
213
|
+
};
|
|
214
|
+
report("uploading");
|
|
215
|
+
for (let index = 0;index < totalParts; index += 1) {
|
|
216
|
+
await task.waitIfPaused();
|
|
217
|
+
task.signal.throwIfAborted();
|
|
218
|
+
const start = index * session.partSize;
|
|
219
|
+
const end = Math.min(file.size, start + session.partSize);
|
|
220
|
+
const bytes = new Uint8Array(await file.slice(start, end).arrayBuffer());
|
|
221
|
+
if (completed.has(index))
|
|
222
|
+
continue;
|
|
223
|
+
const operation = retryPart(async (signal) => {
|
|
224
|
+
await task.waitIfPaused();
|
|
225
|
+
const response2 = await this.#request(`${route}/multipart/${session.sessionId}/${index}`, {
|
|
226
|
+
method: "PUT",
|
|
227
|
+
headers: {
|
|
228
|
+
...inputHeader(options.input),
|
|
229
|
+
"content-type": "application/octet-stream"
|
|
230
|
+
},
|
|
231
|
+
body: bytes,
|
|
232
|
+
signal
|
|
233
|
+
});
|
|
234
|
+
await ensureOk(response2);
|
|
235
|
+
completed.add(index);
|
|
236
|
+
uploadedBytes += bytes.byteLength;
|
|
237
|
+
report("uploading");
|
|
238
|
+
}, retries, task);
|
|
239
|
+
inFlight.add(operation);
|
|
240
|
+
allOperations.push(operation);
|
|
241
|
+
operation.then(() => inFlight.delete(operation), () => inFlight.delete(operation));
|
|
242
|
+
if (inFlight.size >= concurrency)
|
|
243
|
+
await Promise.race(inFlight);
|
|
244
|
+
}
|
|
245
|
+
await Promise.all(allOperations);
|
|
246
|
+
await task.waitIfPaused();
|
|
247
|
+
const preview = await previewPromise;
|
|
248
|
+
await task.waitIfPaused();
|
|
249
|
+
task.markFinalizing();
|
|
250
|
+
report("finalizing");
|
|
251
|
+
const headers = {
|
|
252
|
+
...inputHeader(options.input),
|
|
253
|
+
"x-storage-file-name": encodeURIComponent(file.name),
|
|
254
|
+
"x-storage-file-sha256": fileSha256
|
|
255
|
+
};
|
|
256
|
+
let body;
|
|
257
|
+
if (preview) {
|
|
258
|
+
headers["content-type"] = preview.file.type;
|
|
259
|
+
headers["x-storage-preview-width"] = String(preview.width);
|
|
260
|
+
headers["x-storage-preview-height"] = String(preview.height);
|
|
261
|
+
body = preview.file;
|
|
262
|
+
}
|
|
263
|
+
const response = await this.#request(`${route}/multipart/${session.sessionId}/complete`, {
|
|
264
|
+
method: "POST",
|
|
265
|
+
headers,
|
|
266
|
+
body,
|
|
267
|
+
signal: task.signal
|
|
268
|
+
});
|
|
269
|
+
return parseResponse(response);
|
|
270
|
+
}
|
|
271
|
+
async#preparePreview(file, options, signal) {
|
|
272
|
+
if (options.preview === false || options.previewGenerator === false)
|
|
273
|
+
return;
|
|
274
|
+
if (options.preview)
|
|
275
|
+
return options.preview;
|
|
276
|
+
const source = {
|
|
277
|
+
name: file.name,
|
|
278
|
+
contentType: file.type || "application/octet-stream",
|
|
279
|
+
size: file.size,
|
|
280
|
+
stream: () => file.stream(),
|
|
281
|
+
blob: file
|
|
282
|
+
};
|
|
283
|
+
const generators = options.previewGenerator ? this.#previewGenerators.filter((generator) => generator.id === options.previewGenerator) : this.#previewGenerators;
|
|
284
|
+
for (const generator of generators) {
|
|
285
|
+
if (await generator.supports(source)) {
|
|
286
|
+
return generator.generate(source, {
|
|
287
|
+
signal,
|
|
288
|
+
onProgress: options.onPreviewProgress
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
async#request(path, init) {
|
|
295
|
+
const common = typeof this.#headers === "function" ? await this.#headers() : this.#headers;
|
|
296
|
+
const headers = new Headers(common);
|
|
297
|
+
new Headers(init.headers).forEach((value, key) => headers.set(key, value));
|
|
298
|
+
return this.#fetch(`${this.#endpoint}/${path}`, { ...init, headers });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async function hashFile(file, signal, waitIfPaused, onProgress) {
|
|
302
|
+
const hasher = sha256.create();
|
|
303
|
+
const hashChunkSize = 4 * 1024 * 1024;
|
|
304
|
+
let hashedBytes = 0;
|
|
305
|
+
for (let start = 0;start < file.size; start += hashChunkSize) {
|
|
306
|
+
await waitIfPaused();
|
|
307
|
+
signal.throwIfAborted();
|
|
308
|
+
const bytes = new Uint8Array(await file.slice(start, Math.min(file.size, start + hashChunkSize)).arrayBuffer());
|
|
309
|
+
hasher.update(bytes);
|
|
310
|
+
hashedBytes += bytes.byteLength;
|
|
311
|
+
onProgress(hashedBytes);
|
|
312
|
+
}
|
|
313
|
+
return bytesToHex(hasher.digest());
|
|
314
|
+
}
|
|
315
|
+
function createUploader(options = {}) {
|
|
316
|
+
return new StorageUploader(options);
|
|
317
|
+
}
|
|
318
|
+
async function retryPart(operation, retries, task) {
|
|
319
|
+
let lastError;
|
|
320
|
+
let failures = 0;
|
|
321
|
+
while (failures <= retries) {
|
|
322
|
+
await task.waitIfPaused();
|
|
323
|
+
task.signal.throwIfAborted();
|
|
324
|
+
const signal = task.requestSignal();
|
|
325
|
+
try {
|
|
326
|
+
return await operation(signal);
|
|
327
|
+
} catch (error) {
|
|
328
|
+
lastError = error;
|
|
329
|
+
if (task.signal.aborted)
|
|
330
|
+
break;
|
|
331
|
+
if (isUploadPaused(error) || signal.reason instanceof UploadPausedError) {
|
|
332
|
+
await task.waitIfPaused();
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (failures === retries)
|
|
336
|
+
break;
|
|
337
|
+
await abortableDelay(Math.min(250 * 2 ** failures, 2000), task.signal);
|
|
338
|
+
failures += 1;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
throw lastError;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
class UploadPausedError extends Error {
|
|
345
|
+
name = "UploadPausedError";
|
|
346
|
+
constructor() {
|
|
347
|
+
super("Upload paused");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function isUploadPaused(error) {
|
|
351
|
+
return error instanceof UploadPausedError || error instanceof Error && error.name === "UploadPausedError";
|
|
352
|
+
}
|
|
353
|
+
async function abortableDelay(ms, signal) {
|
|
354
|
+
await new Promise((resolve, reject) => {
|
|
355
|
+
const timeout = setTimeout(resolve, ms);
|
|
356
|
+
signal.addEventListener("abort", () => {
|
|
357
|
+
clearTimeout(timeout);
|
|
358
|
+
reject(signal.reason);
|
|
359
|
+
}, { once: true });
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
function inputHeader(value) {
|
|
363
|
+
return value === undefined ? {} : { "x-storage-input": encodeURIComponent(JSON.stringify(value)) };
|
|
364
|
+
}
|
|
365
|
+
async function parseResponse(response) {
|
|
366
|
+
await ensureOk(response);
|
|
367
|
+
return response.json();
|
|
368
|
+
}
|
|
369
|
+
async function ensureOk(response) {
|
|
370
|
+
if (response.ok)
|
|
371
|
+
return;
|
|
372
|
+
let message = `Storage request failed (${response.status})`;
|
|
373
|
+
try {
|
|
374
|
+
const body = await response.clone().json();
|
|
375
|
+
if (body.error)
|
|
376
|
+
message = body.error;
|
|
377
|
+
} catch {}
|
|
378
|
+
throw new Error(message);
|
|
379
|
+
}
|
|
380
|
+
function positiveInteger(value, name) {
|
|
381
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
382
|
+
throw new Error(`${name} must be positive`);
|
|
383
|
+
return value;
|
|
384
|
+
}
|
|
385
|
+
function nonNegativeInteger(value, name) {
|
|
386
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
387
|
+
throw new Error(`${name} cannot be negative`);
|
|
388
|
+
return value;
|
|
389
|
+
}
|
|
390
|
+
export {
|
|
391
|
+
createUploader,
|
|
392
|
+
StorageUploader
|
|
393
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@minhvuong/pirate-storage-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"type": "module",
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "bun build src/index.ts --outdir dist --target browser --packages external && tsc -p tsconfig.build.json",
|
|
21
|
+
"check-types": "tsc --noEmit",
|
|
22
|
+
"test": "bun test"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@minhvuong/pirate-storage": "workspace:*",
|
|
26
|
+
"@noble/hashes": "^2.0.1"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@minhvuong/pirate-storage-server": "workspace:*",
|
|
30
|
+
"@types/bun": "^1.3.10",
|
|
31
|
+
"typescript": "catalog:"
|
|
32
|
+
}
|
|
33
|
+
}
|