firebase-storage-kit 1.3.0 → 1.4.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.
@@ -0,0 +1,766 @@
1
+ import { ref, uploadBytesResumable, getDownloadURL, getMetadata, deleteObject } from 'firebase/storage';
2
+
3
+ // src/core/emitter.ts
4
+ var Emitter = class {
5
+ listeners = {};
6
+ /**
7
+ * @param event The event to listen for.
8
+ * @param listener The function to call when the event is emitted.
9
+ * @returns A function to unsubscribe from the event.
10
+ */
11
+ on(event, listener) {
12
+ let listeners = this.listeners[event];
13
+ if (!listeners) {
14
+ listeners = /* @__PURE__ */ new Set();
15
+ this.listeners[event] = listeners;
16
+ }
17
+ listeners.add(listener);
18
+ return () => {
19
+ this.listeners[event]?.delete(listener);
20
+ };
21
+ }
22
+ /**
23
+ * @param event The event to emit.
24
+ * @param payload The payload to emit.
25
+ */
26
+ emit(event, payload) {
27
+ const listeners = this.listeners[event];
28
+ if (!listeners) {
29
+ return;
30
+ }
31
+ for (const listener of listeners) {
32
+ listener(payload);
33
+ }
34
+ }
35
+ };
36
+
37
+ // src/core/batch-handle.ts
38
+ var BatchHandle = class extends Emitter {
39
+ id;
40
+ uploads;
41
+ concurrency;
42
+ continueOnError;
43
+ startNext;
44
+ onChange;
45
+ running = 0;
46
+ nextIdx = 0;
47
+ /** Indices that were started via {@link fillSlots} and still hold a concurrency slot. */
48
+ activeSlots = /* @__PURE__ */ new Set();
49
+ settledCount = 0;
50
+ succeededCount = 0;
51
+ failedCount = 0;
52
+ failedFast = false;
53
+ canceled = false;
54
+ paused = false;
55
+ terminalEmitted = false;
56
+ // Stable view of UploadItems; the items themselves mutate in place, but the
57
+ // array reference never changes, so we allocate it once instead of rebuilding
58
+ // it on every snapshot.
59
+ uploadsView;
60
+ // Running aggregates updated incrementally on each child event so snapshot()
61
+ // is O(1) instead of O(n) per progress tick.
62
+ aggregateBytesTransferred = 0;
63
+ aggregateTotalBytes = 0;
64
+ lastBytes;
65
+ lastTotal;
66
+ constructor(init) {
67
+ super();
68
+ this.id = init.id;
69
+ this.uploads = init.uploads;
70
+ this.concurrency = Math.max(1, init.options.concurrency ?? 3);
71
+ this.continueOnError = init.options.continueOnError ?? true;
72
+ this.startNext = init.startNext;
73
+ this.onChange = init.onChange;
74
+ this.uploadsView = init.uploads.map((h) => h.upload);
75
+ this.lastBytes = Array.from(
76
+ { length: init.uploads.length },
77
+ () => 0
78
+ );
79
+ this.lastTotal = Array.from(
80
+ { length: init.uploads.length },
81
+ () => 0
82
+ );
83
+ for (let i = 0; i < this.uploadsView.length; i += 1) {
84
+ const u = this.uploadsView[i];
85
+ if (!u) {
86
+ continue;
87
+ }
88
+ this.lastBytes[i] = u.bytesTransferred;
89
+ this.lastTotal[i] = u.totalBytes;
90
+ this.aggregateBytesTransferred += u.bytesTransferred;
91
+ this.aggregateTotalBytes += u.totalBytes;
92
+ }
93
+ for (let idx = 0; idx < this.uploads.length; idx += 1) {
94
+ const child = this.uploads[idx];
95
+ if (!child) {
96
+ continue;
97
+ }
98
+ child.on("progress", (upload) => {
99
+ this.handleChildProgress(idx, upload);
100
+ });
101
+ child.on("retry", (retryEvent) => {
102
+ this.handleChildRetry(idx, retryEvent);
103
+ });
104
+ child.on("success", (upload) => {
105
+ this.handleChildSettled(idx, upload, "success");
106
+ });
107
+ child.on("error", (upload) => {
108
+ this.handleChildSettled(idx, upload, "error");
109
+ });
110
+ child.on("canceled", (upload) => {
111
+ this.handleChildSettled(idx, upload, "canceled");
112
+ });
113
+ }
114
+ }
115
+ /** @internal */
116
+ _start() {
117
+ this.fillSlots();
118
+ }
119
+ cancel() {
120
+ if (this.canceled || this.terminalEmitted) {
121
+ return;
122
+ }
123
+ this.canceled = true;
124
+ for (const h of this.uploads) {
125
+ const s = h.upload.status;
126
+ if (s === "queued" || s === "uploading" || s === "retrying" || s === "paused") {
127
+ h.cancel();
128
+ }
129
+ }
130
+ this.onChange();
131
+ }
132
+ pause() {
133
+ if (this.paused || this.canceled || this.terminalEmitted) {
134
+ return;
135
+ }
136
+ this.paused = true;
137
+ for (const h of this.uploads) {
138
+ if (h.upload.status === "uploading" || h.upload.status === "retrying") {
139
+ h.pause();
140
+ }
141
+ }
142
+ this.onChange();
143
+ }
144
+ resume() {
145
+ if (!this.paused || this.canceled || this.terminalEmitted) {
146
+ return;
147
+ }
148
+ this.paused = false;
149
+ for (const h of this.uploads) {
150
+ if (h.upload.status === "paused") {
151
+ h.resume();
152
+ }
153
+ }
154
+ this.fillSlots();
155
+ this.onChange();
156
+ }
157
+ snapshot() {
158
+ const totalProgress = this.aggregateTotalBytes > 0 ? this.aggregateBytesTransferred / this.aggregateTotalBytes * 100 : 0;
159
+ return {
160
+ completedCount: this.succeededCount,
161
+ failedCount: this.failedCount,
162
+ id: this.id,
163
+ totalProgress,
164
+ uploads: this.uploadsView
165
+ };
166
+ }
167
+ updateAggregate(idx, u) {
168
+ const prevBytes = this.lastBytes[idx] ?? 0;
169
+ const prevTotal = this.lastTotal[idx] ?? 0;
170
+ this.aggregateBytesTransferred += u.bytesTransferred - prevBytes;
171
+ this.aggregateTotalBytes += u.totalBytes - prevTotal;
172
+ this.lastBytes[idx] = u.bytesTransferred;
173
+ this.lastTotal[idx] = u.totalBytes;
174
+ }
175
+ fillSlots() {
176
+ while (!this.canceled && !this.paused && !this.failedFast && this.running < this.concurrency && this.nextIdx < this.uploads.length) {
177
+ const idx = this.nextIdx;
178
+ this.nextIdx += 1;
179
+ const handle = this.uploads[idx];
180
+ if (!handle || handle.upload.status !== "queued") {
181
+ continue;
182
+ }
183
+ this.activeSlots.add(idx);
184
+ this.running += 1;
185
+ this.startNext(handle);
186
+ }
187
+ }
188
+ handleChildProgress(idx, upload) {
189
+ this.updateAggregate(idx, upload);
190
+ const snap = this.snapshot();
191
+ this.emit("progress", snap);
192
+ this.emit("change", snap);
193
+ }
194
+ handleChildRetry(idx, event) {
195
+ this.updateAggregate(idx, event.upload);
196
+ this.emit("uploadRetry", event);
197
+ const snap = this.snapshot();
198
+ this.emit("change", snap);
199
+ }
200
+ handleChildSettled(idx, upload, kind) {
201
+ this.updateAggregate(idx, upload);
202
+ if (this.activeSlots.delete(idx)) {
203
+ this.running = Math.max(0, this.running - 1);
204
+ }
205
+ this.settledCount += 1;
206
+ if (kind === "success") {
207
+ this.succeededCount += 1;
208
+ this.emit("uploadSuccess", upload);
209
+ } else if (kind === "error") {
210
+ this.failedCount += 1;
211
+ this.emit("uploadError", upload);
212
+ }
213
+ const snap = this.snapshot();
214
+ this.emit("change", snap);
215
+ if (kind === "error" && !this.continueOnError && !this.failedFast && !this.canceled) {
216
+ this.failedFast = true;
217
+ for (const h of this.uploads) {
218
+ if (h.upload.id === upload.id) {
219
+ continue;
220
+ }
221
+ const s = h.upload.status;
222
+ if (s === "queued" || s === "uploading" || s === "retrying" || s === "paused") {
223
+ h.cancel();
224
+ }
225
+ }
226
+ this.terminalEmitted = true;
227
+ this.emit("error", snap);
228
+ this.onChange();
229
+ return;
230
+ }
231
+ if (this.failedFast || this.canceled) {
232
+ this.onChange();
233
+ return;
234
+ }
235
+ this.fillSlots();
236
+ if (this.settledCount >= this.uploads.length && !this.terminalEmitted) {
237
+ this.terminalEmitted = true;
238
+ this.emit("success", snap);
239
+ }
240
+ this.onChange();
241
+ }
242
+ };
243
+
244
+ // src/core/retry.ts
245
+ var DEFAULT_RETRY_OPTIONS = {
246
+ initialDelayMs: 1e3,
247
+ jitter: true,
248
+ maxDelayMs: 3e4,
249
+ maxRetries: 3
250
+ };
251
+ var NON_RETRYABLE_CODES = /* @__PURE__ */ new Set([
252
+ "storage/canceled",
253
+ "storage/unauthorized",
254
+ "storage/unauthenticated",
255
+ "storage/quota-exceeded",
256
+ "storage/invalid-argument",
257
+ "storage/invalid-argument-count",
258
+ "storage/invalid-url",
259
+ "storage/invalid-default-bucket",
260
+ "storage/no-default-bucket",
261
+ "storage/bucket-not-found",
262
+ "storage/project-not-found",
263
+ "storage/invalid-root-operation",
264
+ "storage/invalid-event-name",
265
+ "storage/invalid-format",
266
+ "storage/no-download-url",
267
+ "storage/unauthorized-app",
268
+ "storage/unsupported-environment"
269
+ ]);
270
+ var RETRYABLE_CODES = /* @__PURE__ */ new Set([
271
+ "storage/retry-limit-exceeded",
272
+ "storage/unknown",
273
+ "storage/invalid-checksum",
274
+ "storage/cannot-slice-blob",
275
+ "storage/server-file-wrong-size"
276
+ ]);
277
+ var resolveRetryOptions = (retry) => {
278
+ if (retry === false) {
279
+ return null;
280
+ }
281
+ return {
282
+ ...DEFAULT_RETRY_OPTIONS,
283
+ ...retry
284
+ };
285
+ };
286
+ var hasStorageErrorCode = (error) => "code" in error && typeof error.code === "string";
287
+ var getStorageErrorCode = (error) => {
288
+ if (!hasStorageErrorCode(error)) {
289
+ return void 0;
290
+ }
291
+ return error.code;
292
+ };
293
+ var isRetryableStorageError = (error, options) => {
294
+ if (!options) {
295
+ return false;
296
+ }
297
+ if (options.isRetryable) {
298
+ return options.isRetryable(error);
299
+ }
300
+ const code = getStorageErrorCode(error);
301
+ if (code !== void 0) {
302
+ if (NON_RETRYABLE_CODES.has(code)) {
303
+ return false;
304
+ }
305
+ if (RETRYABLE_CODES.has(code)) {
306
+ return true;
307
+ }
308
+ return false;
309
+ }
310
+ return true;
311
+ };
312
+ var computeRetryDelay = (failedAttempt, options) => {
313
+ const base = Math.min(
314
+ options.maxDelayMs,
315
+ options.initialDelayMs * 2 ** (failedAttempt - 1)
316
+ );
317
+ if (!options.jitter) {
318
+ return base;
319
+ }
320
+ return Math.round(base * (0.5 + Math.random() * 0.5));
321
+ };
322
+
323
+ // src/core/upload-handle.ts
324
+ var UploadHandle = class extends Emitter {
325
+ upload;
326
+ task = null;
327
+ onChange;
328
+ terminated = false;
329
+ controlHooks = null;
330
+ pausedDuringRetry = false;
331
+ constructor(upload, onChange) {
332
+ super();
333
+ this.upload = upload;
334
+ this.onChange = onChange;
335
+ }
336
+ /** @internal */
337
+ _attachTask(task) {
338
+ this.task = task;
339
+ }
340
+ /** @internal */
341
+ _registerControlHooks(hooks) {
342
+ this.controlHooks = hooks;
343
+ }
344
+ /** @internal */
345
+ _clearControlHooks() {
346
+ this.controlHooks = null;
347
+ }
348
+ /** @internal */
349
+ _setStatus(status) {
350
+ if (this.upload.status === status) {
351
+ return;
352
+ }
353
+ this.upload.status = status;
354
+ this.emit("statusChange", this.upload);
355
+ }
356
+ /** @internal */
357
+ _reportProgress(bytesTransferred, totalBytes) {
358
+ if (this.terminated) {
359
+ return;
360
+ }
361
+ this.upload.bytesTransferred = bytesTransferred;
362
+ this.upload.totalBytes = totalBytes;
363
+ this.upload.progress = totalBytes > 0 ? bytesTransferred / totalBytes * 100 : 0;
364
+ if (this.upload.status !== "uploading") {
365
+ this._setStatus("uploading");
366
+ }
367
+ this.emit("progress", this.upload);
368
+ this.onChange();
369
+ }
370
+ /** @internal */
371
+ _prepareRetryAttempt(attempt) {
372
+ if (this.terminated) {
373
+ return;
374
+ }
375
+ this.upload.retryAttempt = attempt;
376
+ this.upload.bytesTransferred = 0;
377
+ this.upload.progress = 0;
378
+ this.upload.error = void 0;
379
+ this.pausedDuringRetry = false;
380
+ this._setStatus("uploading");
381
+ this.onChange();
382
+ }
383
+ /** @internal */
384
+ _enterRetryBackoff(details) {
385
+ if (this.terminated) {
386
+ return;
387
+ }
388
+ this.upload.retryAttempt = details.attempt;
389
+ this.upload.error = details.error;
390
+ this.task = null;
391
+ this._setStatus("retrying");
392
+ this.emit("retry", {
393
+ attempt: details.attempt,
394
+ delayMs: details.delayMs,
395
+ error: details.error,
396
+ maxAttempts: details.maxAttempts,
397
+ upload: this.upload
398
+ });
399
+ this.onChange();
400
+ }
401
+ /** @internal */
402
+ _reportSuccess(downloadURL) {
403
+ if (this.terminated) {
404
+ return;
405
+ }
406
+ this.terminated = true;
407
+ this.upload.downloadURL = downloadURL;
408
+ this.upload.progress = 100;
409
+ this._clearControlHooks();
410
+ this._setStatus("success");
411
+ this.emit("success", this.upload);
412
+ this.onChange();
413
+ }
414
+ /** @internal */
415
+ _reportError(error) {
416
+ if (this.terminated) {
417
+ return;
418
+ }
419
+ this.terminated = true;
420
+ this.upload.error = error;
421
+ this._clearControlHooks();
422
+ this._setStatus("error");
423
+ this.emit("error", this.upload);
424
+ this.onChange();
425
+ }
426
+ cancel() {
427
+ if (this.terminated) {
428
+ return;
429
+ }
430
+ this.terminated = true;
431
+ this.controlHooks?.abort();
432
+ this.task?.cancel();
433
+ this._clearControlHooks();
434
+ this._setStatus("canceled");
435
+ this.emit("canceled", this.upload);
436
+ this.onChange();
437
+ }
438
+ pause() {
439
+ if (this.terminated) {
440
+ return;
441
+ }
442
+ if (this.upload.status === "uploading") {
443
+ this.task?.pause?.();
444
+ this._setStatus("paused");
445
+ this.onChange();
446
+ return;
447
+ }
448
+ if (this.upload.status === "retrying") {
449
+ this.pausedDuringRetry = true;
450
+ this.controlHooks?.pauseDuringRetry?.();
451
+ this._setStatus("paused");
452
+ this.onChange();
453
+ }
454
+ }
455
+ resume() {
456
+ if (this.terminated) {
457
+ return;
458
+ }
459
+ if (this.upload.status !== "paused") {
460
+ return;
461
+ }
462
+ if (this.pausedDuringRetry) {
463
+ this.pausedDuringRetry = false;
464
+ this.controlHooks?.resumeDuringRetry?.();
465
+ this.onChange();
466
+ return;
467
+ }
468
+ this.task?.resume?.();
469
+ this._setStatus("uploading");
470
+ this.onChange();
471
+ }
472
+ };
473
+
474
+ // src/core/storage-manager.ts
475
+ var StorageManager = class {
476
+ provider;
477
+ uploadHandles = [];
478
+ batches = [];
479
+ changeListeners = /* @__PURE__ */ new Set();
480
+ cachedState = null;
481
+ uploadOptionsByHandleId = /* @__PURE__ */ new Map();
482
+ constructor(provider) {
483
+ this.provider = provider;
484
+ }
485
+ /** Current `uploads` and `batches` state. */
486
+ getState = () => {
487
+ this.cachedState ??= {
488
+ batches: this.batches.map((b) => b.snapshot()),
489
+ uploads: this.uploadHandles.map((h) => h.upload)
490
+ };
491
+ return this.cachedState;
492
+ };
493
+ /** Runs `listener` after each change; returns a function — call it to unsubscribe. */
494
+ subscribe = (listener) => {
495
+ this.changeListeners.add(listener);
496
+ return () => {
497
+ this.changeListeners.delete(listener);
498
+ };
499
+ };
500
+ /** Returns `true` if the object exists. Returns `false` only for not-found; other errors are thrown. */
501
+ async exists(path) {
502
+ return await this.provider.exists(path);
503
+ }
504
+ /** Returns metadata for the object at `path`. Throws if the object does not exist. */
505
+ async getMetadata(path) {
506
+ return await this.provider.getMetadata(path);
507
+ }
508
+ /** Returns a download URL for the object at `path`. */
509
+ async getDownloadURL(path) {
510
+ return await this.provider.getDownloadURL(path);
511
+ }
512
+ /** Deletes the object at `path`. */
513
+ async delete(path) {
514
+ await this.provider.delete(path);
515
+ }
516
+ /** Starts the file upload.`options.path` is the object path in storage (see {@link UploadOptions}).
517
+ *
518
+ * @returns An {@link UploadHandle} to control the upload: `pause`, `resume`, `cancel`, and listen for progress with `.on`.
519
+ */
520
+ uploadFile(file, options) {
521
+ const handle = this.createHandle(file, { path: options.path });
522
+ this.uploadOptionsByHandleId.set(handle.upload.id, options);
523
+ this.uploadHandles.push(handle);
524
+ this.notifyChange();
525
+ this.startUpload(handle, options);
526
+ return handle;
527
+ }
528
+ /**
529
+ * Upload several files as one batch. The optional third argument sets how many run at once
530
+ * and what happens when one file fails — see {@link BatchOptions}.
531
+ *
532
+ * `optionsFor` picks storage options per file (same as `uploadFile`); index matches `files` order.
533
+ * If `continueOnError` is `false`, the first failure cancels the other files and the batch fires `error`.
534
+ * If it stays `true` (default), other files keep going; when every file has finished, the batch fires `success`.
535
+ *
536
+ */
537
+ uploadFiles(files, optionsFor, batchOptions = {}) {
538
+ const id = crypto.randomUUID();
539
+ const handles = files.map((file, i) => {
540
+ const options = optionsFor(file, i);
541
+ const handle = this.createHandle(file, {
542
+ batchId: id,
543
+ path: options.path
544
+ });
545
+ this.uploadOptionsByHandleId.set(handle.upload.id, options);
546
+ return handle;
547
+ });
548
+ this.uploadHandles.push(...handles);
549
+ const batch = new BatchHandle({
550
+ id,
551
+ onChange: () => {
552
+ this.notifyChange();
553
+ },
554
+ options: batchOptions,
555
+ startNext: (handle) => {
556
+ const options = this.uploadOptionsByHandleId.get(handle.upload.id) ?? {
557
+ path: handle.upload.path
558
+ };
559
+ this.startUpload(handle, options);
560
+ },
561
+ uploads: handles
562
+ });
563
+ this.batches.push(batch);
564
+ this.notifyChange();
565
+ batch._start();
566
+ return batch;
567
+ }
568
+ createHandle(file, options) {
569
+ const upload = {
570
+ bytesTransferred: 0,
571
+ file,
572
+ id: crypto.randomUUID(),
573
+ path: options.path,
574
+ progress: 0,
575
+ status: "queued",
576
+ totalBytes: file.size,
577
+ ...options.batchId === void 0 ? {} : { batchId: options.batchId }
578
+ };
579
+ return new UploadHandle(upload, () => {
580
+ this.notifyChange();
581
+ });
582
+ }
583
+ startUpload(handle, options) {
584
+ const retryOptions = resolveRetryOptions(options.retry);
585
+ const maxAttempts = retryOptions ? retryOptions.maxRetries + 1 : 1;
586
+ let attempt = 0;
587
+ let retryTimeout = null;
588
+ let currentTask = null;
589
+ let aborted = false;
590
+ let pausedDuringRetry = false;
591
+ let pendingRetryError = null;
592
+ const clearRetryTimeout = () => {
593
+ if (retryTimeout !== null) {
594
+ clearTimeout(retryTimeout);
595
+ retryTimeout = null;
596
+ }
597
+ };
598
+ const abort = () => {
599
+ aborted = true;
600
+ clearRetryTimeout();
601
+ currentTask?.cancel();
602
+ currentTask = null;
603
+ pendingRetryError = null;
604
+ };
605
+ const retryController = {
606
+ runAttempt: () => {
607
+ if (aborted) {
608
+ return;
609
+ }
610
+ attempt += 1;
611
+ handle._prepareRetryAttempt(attempt);
612
+ currentTask = this.provider.upload(handle.upload.file, options, {
613
+ onError: (error) => {
614
+ currentTask = null;
615
+ if (aborted) {
616
+ return;
617
+ }
618
+ if (attempt < maxAttempts && isRetryableStorageError(error, retryOptions)) {
619
+ const delayMs = retryOptions ? computeRetryDelay(attempt, retryOptions) : 0;
620
+ retryController.scheduleRetry(error, delayMs);
621
+ return;
622
+ }
623
+ handle._reportError(error);
624
+ },
625
+ onProgress: (bytesTransferred, totalBytes) => {
626
+ handle._reportProgress(bytesTransferred, totalBytes);
627
+ },
628
+ onSuccess: (downloadURL) => {
629
+ currentTask = null;
630
+ if (aborted) {
631
+ return;
632
+ }
633
+ handle._reportSuccess(downloadURL);
634
+ }
635
+ });
636
+ handle._attachTask(currentTask);
637
+ },
638
+ scheduleRetry: (error, delayMs) => {
639
+ if (aborted) {
640
+ return;
641
+ }
642
+ pendingRetryError = error;
643
+ handle._enterRetryBackoff({
644
+ attempt: attempt + 1,
645
+ delayMs,
646
+ error,
647
+ maxAttempts
648
+ });
649
+ retryTimeout = setTimeout(() => {
650
+ retryTimeout = null;
651
+ if (aborted || pausedDuringRetry) {
652
+ return;
653
+ }
654
+ pendingRetryError = null;
655
+ retryController.runAttempt();
656
+ }, delayMs);
657
+ }
658
+ };
659
+ handle._registerControlHooks({
660
+ abort,
661
+ pauseDuringRetry: () => {
662
+ pausedDuringRetry = true;
663
+ clearRetryTimeout();
664
+ },
665
+ resumeDuringRetry: () => {
666
+ if (aborted) {
667
+ return;
668
+ }
669
+ pausedDuringRetry = false;
670
+ if (pendingRetryError) {
671
+ retryController.scheduleRetry(pendingRetryError, 0);
672
+ }
673
+ }
674
+ });
675
+ retryController.runAttempt();
676
+ }
677
+ notifyChange() {
678
+ this.cachedState = null;
679
+ const state = this.getState();
680
+ for (const listener of this.changeListeners) {
681
+ listener(state);
682
+ }
683
+ }
684
+ };
685
+ var isStorageError = (error) => typeof error === "object" && error !== null && "code" in error && typeof error.code === "string";
686
+ var toError = (error) => error instanceof Error ? error : new Error(String(error));
687
+ var FirebaseStorageProvider = class {
688
+ storage;
689
+ constructor(storage) {
690
+ this.storage = storage;
691
+ }
692
+ upload(file, options, callbacks) {
693
+ const storageRef = ref(this.storage, options.path);
694
+ const metadata = options.customMetadata ? { customMetadata: options.customMetadata } : void 0;
695
+ const task = uploadBytesResumable(storageRef, file, metadata);
696
+ task.on(
697
+ "state_changed",
698
+ (snapshot) => {
699
+ callbacks.onProgress(snapshot.bytesTransferred, snapshot.totalBytes);
700
+ },
701
+ (error) => {
702
+ callbacks.onError(error);
703
+ },
704
+ () => {
705
+ void (async () => {
706
+ try {
707
+ const downloadURL = await getDownloadURL(storageRef);
708
+ callbacks.onSuccess(downloadURL);
709
+ } catch (error) {
710
+ callbacks.onError(toError(error));
711
+ }
712
+ })();
713
+ }
714
+ );
715
+ return {
716
+ cancel: () => {
717
+ task.cancel();
718
+ },
719
+ pause: () => {
720
+ task.pause();
721
+ },
722
+ resume: () => {
723
+ task.resume();
724
+ }
725
+ };
726
+ }
727
+ async exists(path) {
728
+ try {
729
+ await getMetadata(ref(this.storage, path));
730
+ return true;
731
+ } catch (error) {
732
+ if (isStorageError(error) && error.code === "storage/object-not-found") {
733
+ return false;
734
+ }
735
+ throw error;
736
+ }
737
+ }
738
+ async getMetadata(path) {
739
+ const meta = await getMetadata(ref(this.storage, path));
740
+ return {
741
+ contentType: meta.contentType,
742
+ createdAt: new Date(meta.timeCreated),
743
+ customMetadata: meta.customMetadata,
744
+ path,
745
+ size: meta.size,
746
+ updatedAt: new Date(meta.updated)
747
+ };
748
+ }
749
+ async getDownloadURL(path) {
750
+ return await getDownloadURL(ref(this.storage, path));
751
+ }
752
+ async delete(path) {
753
+ await deleteObject(ref(this.storage, path));
754
+ }
755
+ };
756
+
757
+ // src/firebase-storage-manager.ts
758
+ var StorageManager2 = class extends StorageManager {
759
+ constructor(storage) {
760
+ super(new FirebaseStorageProvider(storage));
761
+ }
762
+ };
763
+
764
+ export { BatchHandle, DEFAULT_RETRY_OPTIONS, StorageManager2 as StorageManager, UploadHandle, computeRetryDelay, getStorageErrorCode, isRetryableStorageError, resolveRetryOptions };
765
+ //# sourceMappingURL=chunk-PBO3CMOD.js.map
766
+ //# sourceMappingURL=chunk-PBO3CMOD.js.map