@sparkvault/sdk 1.11.0 → 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;
@@ -2076,9 +2114,7 @@ function getStyles(options) {
2076
2114
 
2077
2115
  .sv-footer {
2078
2116
  padding: 10px 20px;
2079
- border-top: 1px solid ${tokens.border};
2080
2117
  text-align: center;
2081
- background: ${tokens.bgSubtle};
2082
2118
  flex-shrink: 0;
2083
2119
  }
2084
2120
 
@@ -2542,6 +2578,8 @@ function getStyles(options) {
2542
2578
  justify-content: center;
2543
2579
  min-height: 200px;
2544
2580
  flex: 1;
2581
+ padding-left: 32px;
2582
+ padding-right: 32px;
2545
2583
  }
2546
2584
 
2547
2585
  /* ========================================
@@ -4430,8 +4468,11 @@ class IdentityRenderer {
4430
4468
  }
4431
4469
  /**
4432
4470
  * Close the modal and clean up.
4471
+ * Cancels all pending API requests to prevent orphaned operations.
4433
4472
  */
4434
4473
  close() {
4474
+ // Cancel all pending API requests first
4475
+ this.api.abort();
4435
4476
  if (this.focusTimeoutId !== null) {
4436
4477
  clearTimeout(this.focusTimeoutId);
4437
4478
  this.focusTimeoutId = null;
@@ -5617,9 +5658,30 @@ function isInitiateUploadResponse(data) {
5617
5658
  }
5618
5659
  class UploadApi {
5619
5660
  constructor(config) {
5661
+ /** Abort controller for cancelling all pending requests on close */
5662
+ this.closeController = new AbortController();
5620
5663
  this.config = config;
5621
5664
  this.timeoutMs = config.timeout ?? DEFAULT_TIMEOUT_MS;
5622
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
+ }
5623
5685
  /**
5624
5686
  * Get vault upload info (public endpoint).
5625
5687
  */
@@ -5672,8 +5734,26 @@ class UploadApi {
5672
5734
  * Internal request method with timeout handling and error context.
5673
5735
  */
5674
5736
  async request(url, options) {
5675
- const controller = new AbortController();
5676
- 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
+ }
5677
5757
  try {
5678
5758
  const response = await fetch(url, {
5679
5759
  method: options.method,
@@ -5682,7 +5762,7 @@ class UploadApi {
5682
5762
  'Accept': 'application/json',
5683
5763
  },
5684
5764
  body: options.body,
5685
- signal: controller.signal,
5765
+ signal: combinedSignal,
5686
5766
  });
5687
5767
  const json = await response.json();
5688
5768
  if (!response.ok) {
@@ -5708,8 +5788,12 @@ class UploadApi {
5708
5788
  if (error instanceof SparkVaultError) {
5709
5789
  throw error;
5710
5790
  }
5711
- // Convert AbortError to TimeoutError
5791
+ // Convert AbortError to appropriate error
5712
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
+ }
5713
5797
  throw new TimeoutError(`Request timed out after ${this.timeoutMs}ms`);
5714
5798
  }
5715
5799
  // Network errors with context
@@ -7571,6 +7655,8 @@ class UploadRenderer {
7571
7655
  this.uploadModal = null;
7572
7656
  this.isInlineMode = false;
7573
7657
  this.hybridModalInitialized = false;
7658
+ // Track active XHR for cancellation on close
7659
+ this.activeXhr = null;
7574
7660
  this.pasteHandler = null;
7575
7661
  this.container = container;
7576
7662
  this.api = api;
@@ -7603,8 +7689,16 @@ class UploadRenderer {
7603
7689
  }
7604
7690
  /**
7605
7691
  * Close the upload flow.
7692
+ * Cancels all pending API requests and active uploads to prevent orphaned operations.
7606
7693
  */
7607
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
+ }
7608
7702
  this.cleanupFileInput();
7609
7703
  this.cleanupPasteHandler();
7610
7704
  // Clean up modal if open (hybrid mode)
@@ -8203,11 +8297,19 @@ class UploadRenderer {
8203
8297
  }
8204
8298
  }
8205
8299
  /**
8206
- * 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.
8207
8302
  */
8208
8303
  uploadChunkWithProgress(uploadUrl, chunk, chunkStart, totalSize, tusVersion, file, ingotId, requestId) {
8209
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
+ }
8210
8310
  const xhr = new XMLHttpRequest();
8311
+ // Track this XHR so it can be cancelled on close
8312
+ this.activeXhr = xhr;
8211
8313
  // Track progress within this chunk
8212
8314
  xhr.upload.onprogress = (e) => {
8213
8315
  if (e.lengthComputable) {
@@ -8231,6 +8333,7 @@ class UploadRenderer {
8231
8333
  }
8232
8334
  };
8233
8335
  xhr.onload = () => {
8336
+ this.activeXhr = null;
8234
8337
  if (xhr.status >= 200 && xhr.status < 300) {
8235
8338
  // Get new offset from server response
8236
8339
  const newOffsetHeader = xhr.getResponseHeader('Upload-Offset');
@@ -8241,8 +8344,18 @@ class UploadRenderer {
8241
8344
  reject(new Error(`Chunk upload failed with status ${xhr.status}`));
8242
8345
  }
8243
8346
  };
8244
- xhr.onerror = () => reject(new Error('Chunk upload failed'));
8245
- 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
+ };
8246
8359
  xhr.open('PATCH', uploadUrl);
8247
8360
  xhr.setRequestHeader('Tus-Resumable', tusVersion);
8248
8361
  xhr.setRequestHeader('Upload-Offset', String(chunkStart));