@sparkvault/sdk 1.11.1 → 1.11.3

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;
@@ -1648,6 +1686,7 @@ function getStyles(options) {
1648
1686
  padding: 14px 20px;
1649
1687
  border-bottom: 1px solid ${tokens.border};
1650
1688
  background: ${tokens.bg};
1689
+ border-radius: 16px 16px 0 0;
1651
1690
  flex-shrink: 0;
1652
1691
  }
1653
1692
 
@@ -2540,6 +2579,8 @@ function getStyles(options) {
2540
2579
  justify-content: center;
2541
2580
  min-height: 200px;
2542
2581
  flex: 1;
2582
+ padding-left: 32px;
2583
+ padding-right: 32px;
2543
2584
  }
2544
2585
 
2545
2586
  /* ========================================
@@ -4428,8 +4469,11 @@ class IdentityRenderer {
4428
4469
  }
4429
4470
  /**
4430
4471
  * Close the modal and clean up.
4472
+ * Cancels all pending API requests to prevent orphaned operations.
4431
4473
  */
4432
4474
  close() {
4475
+ // Cancel all pending API requests first
4476
+ this.api.abort();
4433
4477
  if (this.focusTimeoutId !== null) {
4434
4478
  clearTimeout(this.focusTimeoutId);
4435
4479
  this.focusTimeoutId = null;
@@ -5615,9 +5659,30 @@ function isInitiateUploadResponse(data) {
5615
5659
  }
5616
5660
  class UploadApi {
5617
5661
  constructor(config) {
5662
+ /** Abort controller for cancelling all pending requests on close */
5663
+ this.closeController = new AbortController();
5618
5664
  this.config = config;
5619
5665
  this.timeoutMs = config.timeout ?? DEFAULT_TIMEOUT_MS;
5620
5666
  }
5667
+ /**
5668
+ * Abort all pending requests.
5669
+ * Call this when the renderer is closed to prevent orphaned requests.
5670
+ */
5671
+ abort() {
5672
+ this.closeController.abort();
5673
+ }
5674
+ /**
5675
+ * Check if the API has been aborted.
5676
+ */
5677
+ isAborted() {
5678
+ return this.closeController.signal.aborted;
5679
+ }
5680
+ /**
5681
+ * Get the abort signal for external use (e.g., XHR uploads).
5682
+ */
5683
+ getAbortSignal() {
5684
+ return this.closeController.signal;
5685
+ }
5621
5686
  /**
5622
5687
  * Get vault upload info (public endpoint).
5623
5688
  */
@@ -5670,8 +5735,26 @@ class UploadApi {
5670
5735
  * Internal request method with timeout handling and error context.
5671
5736
  */
5672
5737
  async request(url, options) {
5673
- const controller = new AbortController();
5674
- const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
5738
+ // Don't start new requests if already aborted (renderer closed)
5739
+ if (this.closeController.signal.aborted) {
5740
+ throw new UploadApiError('Request cancelled', 'cancelled', 0);
5741
+ }
5742
+ const timeoutController = new AbortController();
5743
+ const timeoutId = setTimeout(() => timeoutController.abort(), this.timeoutMs);
5744
+ // Combine timeout and close signals - abort on either
5745
+ let combinedSignal;
5746
+ if ('any' in AbortSignal) {
5747
+ combinedSignal = AbortSignal.any([
5748
+ timeoutController.signal,
5749
+ this.closeController.signal,
5750
+ ]);
5751
+ }
5752
+ else {
5753
+ // Fallback for older browsers: link close signal to timeout controller
5754
+ combinedSignal = timeoutController.signal;
5755
+ const onClose = () => timeoutController.abort();
5756
+ this.closeController.signal.addEventListener('abort', onClose);
5757
+ }
5675
5758
  try {
5676
5759
  const response = await fetch(url, {
5677
5760
  method: options.method,
@@ -5680,7 +5763,7 @@ class UploadApi {
5680
5763
  'Accept': 'application/json',
5681
5764
  },
5682
5765
  body: options.body,
5683
- signal: controller.signal,
5766
+ signal: combinedSignal,
5684
5767
  });
5685
5768
  const json = await response.json();
5686
5769
  if (!response.ok) {
@@ -5706,8 +5789,12 @@ class UploadApi {
5706
5789
  if (error instanceof SparkVaultError) {
5707
5790
  throw error;
5708
5791
  }
5709
- // Convert AbortError to TimeoutError
5792
+ // Convert AbortError to appropriate error
5710
5793
  if (error instanceof DOMException && error.name === 'AbortError') {
5794
+ // Check which signal triggered the abort
5795
+ if (this.closeController.signal.aborted) {
5796
+ throw new UploadApiError('Request cancelled', 'cancelled', 0);
5797
+ }
5711
5798
  throw new TimeoutError(`Request timed out after ${this.timeoutMs}ms`);
5712
5799
  }
5713
5800
  // Network errors with context
@@ -7569,6 +7656,8 @@ class UploadRenderer {
7569
7656
  this.uploadModal = null;
7570
7657
  this.isInlineMode = false;
7571
7658
  this.hybridModalInitialized = false;
7659
+ // Track active XHR for cancellation on close
7660
+ this.activeXhr = null;
7572
7661
  this.pasteHandler = null;
7573
7662
  this.container = container;
7574
7663
  this.api = api;
@@ -7601,8 +7690,16 @@ class UploadRenderer {
7601
7690
  }
7602
7691
  /**
7603
7692
  * Close the upload flow.
7693
+ * Cancels all pending API requests and active uploads to prevent orphaned operations.
7604
7694
  */
7605
7695
  close() {
7696
+ // Cancel all pending API requests first
7697
+ this.api.abort();
7698
+ // Abort any active XHR upload
7699
+ if (this.activeXhr) {
7700
+ this.activeXhr.abort();
7701
+ this.activeXhr = null;
7702
+ }
7606
7703
  this.cleanupFileInput();
7607
7704
  this.cleanupPasteHandler();
7608
7705
  // Clean up modal if open (hybrid mode)
@@ -8201,11 +8298,19 @@ class UploadRenderer {
8201
8298
  }
8202
8299
  }
8203
8300
  /**
8204
- * Upload a single chunk using XMLHttpRequest for progress tracking
8301
+ * Upload a single chunk using XMLHttpRequest for progress tracking.
8302
+ * Tracks the XHR so it can be cancelled if the renderer is closed.
8205
8303
  */
8206
8304
  uploadChunkWithProgress(uploadUrl, chunk, chunkStart, totalSize, tusVersion, file, ingotId, requestId) {
8207
8305
  return new Promise((resolve, reject) => {
8306
+ // Check if already aborted before starting
8307
+ if (this.api.isAborted()) {
8308
+ reject(new Error('Upload cancelled'));
8309
+ return;
8310
+ }
8208
8311
  const xhr = new XMLHttpRequest();
8312
+ // Track this XHR so it can be cancelled on close
8313
+ this.activeXhr = xhr;
8209
8314
  // Track progress within this chunk
8210
8315
  xhr.upload.onprogress = (e) => {
8211
8316
  if (e.lengthComputable) {
@@ -8229,6 +8334,7 @@ class UploadRenderer {
8229
8334
  }
8230
8335
  };
8231
8336
  xhr.onload = () => {
8337
+ this.activeXhr = null;
8232
8338
  if (xhr.status >= 200 && xhr.status < 300) {
8233
8339
  // Get new offset from server response
8234
8340
  const newOffsetHeader = xhr.getResponseHeader('Upload-Offset');
@@ -8239,8 +8345,18 @@ class UploadRenderer {
8239
8345
  reject(new Error(`Chunk upload failed with status ${xhr.status}`));
8240
8346
  }
8241
8347
  };
8242
- xhr.onerror = () => reject(new Error('Chunk upload failed'));
8243
- xhr.ontimeout = () => reject(new Error('Chunk upload timed out'));
8348
+ xhr.onerror = () => {
8349
+ this.activeXhr = null;
8350
+ reject(new Error('Chunk upload failed'));
8351
+ };
8352
+ xhr.ontimeout = () => {
8353
+ this.activeXhr = null;
8354
+ reject(new Error('Chunk upload timed out'));
8355
+ };
8356
+ xhr.onabort = () => {
8357
+ this.activeXhr = null;
8358
+ reject(new Error('Upload cancelled'));
8359
+ };
8244
8360
  xhr.open('PATCH', uploadUrl);
8245
8361
  xhr.setRequestHeader('Tus-Resumable', tusVersion);
8246
8362
  xhr.setRequestHeader('Upload-Offset', String(chunkStart));