@sparkvault/sdk 1.11.1 → 1.11.2
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/identity/api.d.ts +11 -0
- package/dist/identity/renderer.d.ts +1 -0
- package/dist/sparkvault.cjs.js +126 -11
- package/dist/sparkvault.cjs.js.map +1 -1
- package/dist/sparkvault.esm.js +126 -11
- package/dist/sparkvault.esm.js.map +1 -1
- package/dist/sparkvault.js +1 -1
- package/dist/sparkvault.js.map +1 -1
- package/dist/vaults/upload/api.d.ts +15 -0
- package/dist/vaults/upload/renderer.d.ts +4 -1
- package/package.json +1 -1
package/dist/sparkvault.esm.js
CHANGED
|
@@ -582,27 +582,61 @@ class IdentityApi {
|
|
|
582
582
|
constructor(config, timeoutMs = DEFAULT_TIMEOUT_MS$1) {
|
|
583
583
|
/** Cached config promise - allows preloading and deduplication */
|
|
584
584
|
this.configCache = null;
|
|
585
|
+
/** Abort controller for cancelling all pending requests on close */
|
|
586
|
+
this.closeController = new AbortController();
|
|
585
587
|
this.config = config;
|
|
586
588
|
this.timeoutMs = timeoutMs;
|
|
587
589
|
}
|
|
590
|
+
/**
|
|
591
|
+
* Abort all pending requests.
|
|
592
|
+
* Call this when the renderer is closed to prevent orphaned requests.
|
|
593
|
+
*/
|
|
594
|
+
abort() {
|
|
595
|
+
this.closeController.abort();
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Check if the API has been aborted.
|
|
599
|
+
*/
|
|
600
|
+
isAborted() {
|
|
601
|
+
return this.closeController.signal.aborted;
|
|
602
|
+
}
|
|
588
603
|
get baseUrl() {
|
|
589
604
|
return `${this.config.identityBaseUrl}/${this.config.accountId}`;
|
|
590
605
|
}
|
|
591
606
|
async request(method, endpoint, body) {
|
|
607
|
+
// Don't start new requests if already aborted (renderer closed)
|
|
608
|
+
if (this.closeController.signal.aborted) {
|
|
609
|
+
throw new IdentityApiError('Request cancelled', 'cancelled', 0);
|
|
610
|
+
}
|
|
592
611
|
const url = `${this.baseUrl}${endpoint}`;
|
|
593
612
|
const headers = {
|
|
594
613
|
'Content-Type': 'application/json',
|
|
595
614
|
'Accept': 'application/json',
|
|
596
615
|
};
|
|
597
616
|
// Create abort controller for request timeout
|
|
598
|
-
const
|
|
599
|
-
const timeoutId = setTimeout(() =>
|
|
617
|
+
const timeoutController = new AbortController();
|
|
618
|
+
const timeoutId = setTimeout(() => timeoutController.abort(), this.timeoutMs);
|
|
619
|
+
// Combine timeout and close signals - abort on either
|
|
620
|
+
// Use AbortSignal.any if available, otherwise fall back to manual linking
|
|
621
|
+
let combinedSignal;
|
|
622
|
+
if ('any' in AbortSignal) {
|
|
623
|
+
combinedSignal = AbortSignal.any([
|
|
624
|
+
timeoutController.signal,
|
|
625
|
+
this.closeController.signal,
|
|
626
|
+
]);
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
// Fallback for older browsers: link close signal to timeout controller
|
|
630
|
+
combinedSignal = timeoutController.signal;
|
|
631
|
+
const onClose = () => timeoutController.abort();
|
|
632
|
+
this.closeController.signal.addEventListener('abort', onClose);
|
|
633
|
+
}
|
|
600
634
|
try {
|
|
601
635
|
const response = await fetch(url, {
|
|
602
636
|
method,
|
|
603
637
|
headers,
|
|
604
638
|
body: body ? JSON.stringify(body) : undefined,
|
|
605
|
-
signal:
|
|
639
|
+
signal: combinedSignal,
|
|
606
640
|
});
|
|
607
641
|
const json = await response.json();
|
|
608
642
|
if (!response.ok) {
|
|
@@ -617,8 +651,12 @@ class IdentityApi {
|
|
|
617
651
|
return (json.data ?? json);
|
|
618
652
|
}
|
|
619
653
|
catch (error) {
|
|
620
|
-
// Convert AbortError to a more descriptive
|
|
654
|
+
// Convert AbortError to a more descriptive error
|
|
621
655
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
656
|
+
// Check which signal triggered the abort
|
|
657
|
+
if (this.closeController.signal.aborted) {
|
|
658
|
+
throw new IdentityApiError('Request cancelled', 'cancelled', 0);
|
|
659
|
+
}
|
|
622
660
|
throw new IdentityApiError('Request timed out', 'timeout', 0);
|
|
623
661
|
}
|
|
624
662
|
throw error;
|
|
@@ -2540,6 +2578,8 @@ function getStyles(options) {
|
|
|
2540
2578
|
justify-content: center;
|
|
2541
2579
|
min-height: 200px;
|
|
2542
2580
|
flex: 1;
|
|
2581
|
+
padding-left: 32px;
|
|
2582
|
+
padding-right: 32px;
|
|
2543
2583
|
}
|
|
2544
2584
|
|
|
2545
2585
|
/* ========================================
|
|
@@ -4428,8 +4468,11 @@ class IdentityRenderer {
|
|
|
4428
4468
|
}
|
|
4429
4469
|
/**
|
|
4430
4470
|
* Close the modal and clean up.
|
|
4471
|
+
* Cancels all pending API requests to prevent orphaned operations.
|
|
4431
4472
|
*/
|
|
4432
4473
|
close() {
|
|
4474
|
+
// Cancel all pending API requests first
|
|
4475
|
+
this.api.abort();
|
|
4433
4476
|
if (this.focusTimeoutId !== null) {
|
|
4434
4477
|
clearTimeout(this.focusTimeoutId);
|
|
4435
4478
|
this.focusTimeoutId = null;
|
|
@@ -5615,9 +5658,30 @@ function isInitiateUploadResponse(data) {
|
|
|
5615
5658
|
}
|
|
5616
5659
|
class UploadApi {
|
|
5617
5660
|
constructor(config) {
|
|
5661
|
+
/** Abort controller for cancelling all pending requests on close */
|
|
5662
|
+
this.closeController = new AbortController();
|
|
5618
5663
|
this.config = config;
|
|
5619
5664
|
this.timeoutMs = config.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
5620
5665
|
}
|
|
5666
|
+
/**
|
|
5667
|
+
* Abort all pending requests.
|
|
5668
|
+
* Call this when the renderer is closed to prevent orphaned requests.
|
|
5669
|
+
*/
|
|
5670
|
+
abort() {
|
|
5671
|
+
this.closeController.abort();
|
|
5672
|
+
}
|
|
5673
|
+
/**
|
|
5674
|
+
* Check if the API has been aborted.
|
|
5675
|
+
*/
|
|
5676
|
+
isAborted() {
|
|
5677
|
+
return this.closeController.signal.aborted;
|
|
5678
|
+
}
|
|
5679
|
+
/**
|
|
5680
|
+
* Get the abort signal for external use (e.g., XHR uploads).
|
|
5681
|
+
*/
|
|
5682
|
+
getAbortSignal() {
|
|
5683
|
+
return this.closeController.signal;
|
|
5684
|
+
}
|
|
5621
5685
|
/**
|
|
5622
5686
|
* Get vault upload info (public endpoint).
|
|
5623
5687
|
*/
|
|
@@ -5670,8 +5734,26 @@ class UploadApi {
|
|
|
5670
5734
|
* Internal request method with timeout handling and error context.
|
|
5671
5735
|
*/
|
|
5672
5736
|
async request(url, options) {
|
|
5673
|
-
|
|
5674
|
-
|
|
5737
|
+
// Don't start new requests if already aborted (renderer closed)
|
|
5738
|
+
if (this.closeController.signal.aborted) {
|
|
5739
|
+
throw new UploadApiError('Request cancelled', 'cancelled', 0);
|
|
5740
|
+
}
|
|
5741
|
+
const timeoutController = new AbortController();
|
|
5742
|
+
const timeoutId = setTimeout(() => timeoutController.abort(), this.timeoutMs);
|
|
5743
|
+
// Combine timeout and close signals - abort on either
|
|
5744
|
+
let combinedSignal;
|
|
5745
|
+
if ('any' in AbortSignal) {
|
|
5746
|
+
combinedSignal = AbortSignal.any([
|
|
5747
|
+
timeoutController.signal,
|
|
5748
|
+
this.closeController.signal,
|
|
5749
|
+
]);
|
|
5750
|
+
}
|
|
5751
|
+
else {
|
|
5752
|
+
// Fallback for older browsers: link close signal to timeout controller
|
|
5753
|
+
combinedSignal = timeoutController.signal;
|
|
5754
|
+
const onClose = () => timeoutController.abort();
|
|
5755
|
+
this.closeController.signal.addEventListener('abort', onClose);
|
|
5756
|
+
}
|
|
5675
5757
|
try {
|
|
5676
5758
|
const response = await fetch(url, {
|
|
5677
5759
|
method: options.method,
|
|
@@ -5680,7 +5762,7 @@ class UploadApi {
|
|
|
5680
5762
|
'Accept': 'application/json',
|
|
5681
5763
|
},
|
|
5682
5764
|
body: options.body,
|
|
5683
|
-
signal:
|
|
5765
|
+
signal: combinedSignal,
|
|
5684
5766
|
});
|
|
5685
5767
|
const json = await response.json();
|
|
5686
5768
|
if (!response.ok) {
|
|
@@ -5706,8 +5788,12 @@ class UploadApi {
|
|
|
5706
5788
|
if (error instanceof SparkVaultError) {
|
|
5707
5789
|
throw error;
|
|
5708
5790
|
}
|
|
5709
|
-
// Convert AbortError to
|
|
5791
|
+
// Convert AbortError to appropriate error
|
|
5710
5792
|
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
5793
|
+
// Check which signal triggered the abort
|
|
5794
|
+
if (this.closeController.signal.aborted) {
|
|
5795
|
+
throw new UploadApiError('Request cancelled', 'cancelled', 0);
|
|
5796
|
+
}
|
|
5711
5797
|
throw new TimeoutError(`Request timed out after ${this.timeoutMs}ms`);
|
|
5712
5798
|
}
|
|
5713
5799
|
// Network errors with context
|
|
@@ -7569,6 +7655,8 @@ class UploadRenderer {
|
|
|
7569
7655
|
this.uploadModal = null;
|
|
7570
7656
|
this.isInlineMode = false;
|
|
7571
7657
|
this.hybridModalInitialized = false;
|
|
7658
|
+
// Track active XHR for cancellation on close
|
|
7659
|
+
this.activeXhr = null;
|
|
7572
7660
|
this.pasteHandler = null;
|
|
7573
7661
|
this.container = container;
|
|
7574
7662
|
this.api = api;
|
|
@@ -7601,8 +7689,16 @@ class UploadRenderer {
|
|
|
7601
7689
|
}
|
|
7602
7690
|
/**
|
|
7603
7691
|
* Close the upload flow.
|
|
7692
|
+
* Cancels all pending API requests and active uploads to prevent orphaned operations.
|
|
7604
7693
|
*/
|
|
7605
7694
|
close() {
|
|
7695
|
+
// Cancel all pending API requests first
|
|
7696
|
+
this.api.abort();
|
|
7697
|
+
// Abort any active XHR upload
|
|
7698
|
+
if (this.activeXhr) {
|
|
7699
|
+
this.activeXhr.abort();
|
|
7700
|
+
this.activeXhr = null;
|
|
7701
|
+
}
|
|
7606
7702
|
this.cleanupFileInput();
|
|
7607
7703
|
this.cleanupPasteHandler();
|
|
7608
7704
|
// Clean up modal if open (hybrid mode)
|
|
@@ -8201,11 +8297,19 @@ class UploadRenderer {
|
|
|
8201
8297
|
}
|
|
8202
8298
|
}
|
|
8203
8299
|
/**
|
|
8204
|
-
* Upload a single chunk using XMLHttpRequest for progress tracking
|
|
8300
|
+
* Upload a single chunk using XMLHttpRequest for progress tracking.
|
|
8301
|
+
* Tracks the XHR so it can be cancelled if the renderer is closed.
|
|
8205
8302
|
*/
|
|
8206
8303
|
uploadChunkWithProgress(uploadUrl, chunk, chunkStart, totalSize, tusVersion, file, ingotId, requestId) {
|
|
8207
8304
|
return new Promise((resolve, reject) => {
|
|
8305
|
+
// Check if already aborted before starting
|
|
8306
|
+
if (this.api.isAborted()) {
|
|
8307
|
+
reject(new Error('Upload cancelled'));
|
|
8308
|
+
return;
|
|
8309
|
+
}
|
|
8208
8310
|
const xhr = new XMLHttpRequest();
|
|
8311
|
+
// Track this XHR so it can be cancelled on close
|
|
8312
|
+
this.activeXhr = xhr;
|
|
8209
8313
|
// Track progress within this chunk
|
|
8210
8314
|
xhr.upload.onprogress = (e) => {
|
|
8211
8315
|
if (e.lengthComputable) {
|
|
@@ -8229,6 +8333,7 @@ class UploadRenderer {
|
|
|
8229
8333
|
}
|
|
8230
8334
|
};
|
|
8231
8335
|
xhr.onload = () => {
|
|
8336
|
+
this.activeXhr = null;
|
|
8232
8337
|
if (xhr.status >= 200 && xhr.status < 300) {
|
|
8233
8338
|
// Get new offset from server response
|
|
8234
8339
|
const newOffsetHeader = xhr.getResponseHeader('Upload-Offset');
|
|
@@ -8239,8 +8344,18 @@ class UploadRenderer {
|
|
|
8239
8344
|
reject(new Error(`Chunk upload failed with status ${xhr.status}`));
|
|
8240
8345
|
}
|
|
8241
8346
|
};
|
|
8242
|
-
xhr.onerror = () =>
|
|
8243
|
-
|
|
8347
|
+
xhr.onerror = () => {
|
|
8348
|
+
this.activeXhr = null;
|
|
8349
|
+
reject(new Error('Chunk upload failed'));
|
|
8350
|
+
};
|
|
8351
|
+
xhr.ontimeout = () => {
|
|
8352
|
+
this.activeXhr = null;
|
|
8353
|
+
reject(new Error('Chunk upload timed out'));
|
|
8354
|
+
};
|
|
8355
|
+
xhr.onabort = () => {
|
|
8356
|
+
this.activeXhr = null;
|
|
8357
|
+
reject(new Error('Upload cancelled'));
|
|
8358
|
+
};
|
|
8244
8359
|
xhr.open('PATCH', uploadUrl);
|
|
8245
8360
|
xhr.setRequestHeader('Tus-Resumable', tusVersion);
|
|
8246
8361
|
xhr.setRequestHeader('Upload-Offset', String(chunkStart));
|