@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.
@@ -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 controller = new AbortController();
599
- const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
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: controller.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 timeout error
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
- const controller = new AbortController();
5674
- const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
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: controller.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 TimeoutError
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 = () => reject(new Error('Chunk upload failed'));
8243
- xhr.ontimeout = () => reject(new Error('Chunk upload timed out'));
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));