@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.
@@ -11,7 +11,18 @@ export declare class IdentityApi {
11
11
  private readonly timeoutMs;
12
12
  /** Cached config promise - allows preloading and deduplication */
13
13
  private configCache;
14
+ /** Abort controller for cancelling all pending requests on close */
15
+ private closeController;
14
16
  constructor(config: ResolvedConfig, timeoutMs?: number);
17
+ /**
18
+ * Abort all pending requests.
19
+ * Call this when the renderer is closed to prevent orphaned requests.
20
+ */
21
+ abort(): void;
22
+ /**
23
+ * Check if the API has been aborted.
24
+ */
25
+ isAborted(): boolean;
15
26
  private get baseUrl();
16
27
  private request;
17
28
  /**
@@ -37,6 +37,7 @@ export declare class IdentityRenderer {
37
37
  start(): Promise<void>;
38
38
  /**
39
39
  * Close the modal and clean up.
40
+ * Cancels all pending API requests to prevent orphaned operations.
40
41
  */
41
42
  close(): void;
42
43
  private handleClose;
@@ -586,27 +586,61 @@ class IdentityApi {
586
586
  constructor(config, timeoutMs = DEFAULT_TIMEOUT_MS$1) {
587
587
  /** Cached config promise - allows preloading and deduplication */
588
588
  this.configCache = null;
589
+ /** Abort controller for cancelling all pending requests on close */
590
+ this.closeController = new AbortController();
589
591
  this.config = config;
590
592
  this.timeoutMs = timeoutMs;
591
593
  }
594
+ /**
595
+ * Abort all pending requests.
596
+ * Call this when the renderer is closed to prevent orphaned requests.
597
+ */
598
+ abort() {
599
+ this.closeController.abort();
600
+ }
601
+ /**
602
+ * Check if the API has been aborted.
603
+ */
604
+ isAborted() {
605
+ return this.closeController.signal.aborted;
606
+ }
592
607
  get baseUrl() {
593
608
  return `${this.config.identityBaseUrl}/${this.config.accountId}`;
594
609
  }
595
610
  async request(method, endpoint, body) {
611
+ // Don't start new requests if already aborted (renderer closed)
612
+ if (this.closeController.signal.aborted) {
613
+ throw new IdentityApiError('Request cancelled', 'cancelled', 0);
614
+ }
596
615
  const url = `${this.baseUrl}${endpoint}`;
597
616
  const headers = {
598
617
  'Content-Type': 'application/json',
599
618
  'Accept': 'application/json',
600
619
  };
601
620
  // Create abort controller for request timeout
602
- const controller = new AbortController();
603
- const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
621
+ const timeoutController = new AbortController();
622
+ const timeoutId = setTimeout(() => timeoutController.abort(), this.timeoutMs);
623
+ // Combine timeout and close signals - abort on either
624
+ // Use AbortSignal.any if available, otherwise fall back to manual linking
625
+ let combinedSignal;
626
+ if ('any' in AbortSignal) {
627
+ combinedSignal = AbortSignal.any([
628
+ timeoutController.signal,
629
+ this.closeController.signal,
630
+ ]);
631
+ }
632
+ else {
633
+ // Fallback for older browsers: link close signal to timeout controller
634
+ combinedSignal = timeoutController.signal;
635
+ const onClose = () => timeoutController.abort();
636
+ this.closeController.signal.addEventListener('abort', onClose);
637
+ }
604
638
  try {
605
639
  const response = await fetch(url, {
606
640
  method,
607
641
  headers,
608
642
  body: body ? JSON.stringify(body) : undefined,
609
- signal: controller.signal,
643
+ signal: combinedSignal,
610
644
  });
611
645
  const json = await response.json();
612
646
  if (!response.ok) {
@@ -621,8 +655,12 @@ class IdentityApi {
621
655
  return (json.data ?? json);
622
656
  }
623
657
  catch (error) {
624
- // Convert AbortError to a more descriptive timeout error
658
+ // Convert AbortError to a more descriptive error
625
659
  if (error instanceof Error && error.name === 'AbortError') {
660
+ // Check which signal triggered the abort
661
+ if (this.closeController.signal.aborted) {
662
+ throw new IdentityApiError('Request cancelled', 'cancelled', 0);
663
+ }
626
664
  throw new IdentityApiError('Request timed out', 'timeout', 0);
627
665
  }
628
666
  throw error;
@@ -2544,6 +2582,8 @@ function getStyles(options) {
2544
2582
  justify-content: center;
2545
2583
  min-height: 200px;
2546
2584
  flex: 1;
2585
+ padding-left: 32px;
2586
+ padding-right: 32px;
2547
2587
  }
2548
2588
 
2549
2589
  /* ========================================
@@ -4432,8 +4472,11 @@ class IdentityRenderer {
4432
4472
  }
4433
4473
  /**
4434
4474
  * Close the modal and clean up.
4475
+ * Cancels all pending API requests to prevent orphaned operations.
4435
4476
  */
4436
4477
  close() {
4478
+ // Cancel all pending API requests first
4479
+ this.api.abort();
4437
4480
  if (this.focusTimeoutId !== null) {
4438
4481
  clearTimeout(this.focusTimeoutId);
4439
4482
  this.focusTimeoutId = null;
@@ -5619,9 +5662,30 @@ function isInitiateUploadResponse(data) {
5619
5662
  }
5620
5663
  class UploadApi {
5621
5664
  constructor(config) {
5665
+ /** Abort controller for cancelling all pending requests on close */
5666
+ this.closeController = new AbortController();
5622
5667
  this.config = config;
5623
5668
  this.timeoutMs = config.timeout ?? DEFAULT_TIMEOUT_MS;
5624
5669
  }
5670
+ /**
5671
+ * Abort all pending requests.
5672
+ * Call this when the renderer is closed to prevent orphaned requests.
5673
+ */
5674
+ abort() {
5675
+ this.closeController.abort();
5676
+ }
5677
+ /**
5678
+ * Check if the API has been aborted.
5679
+ */
5680
+ isAborted() {
5681
+ return this.closeController.signal.aborted;
5682
+ }
5683
+ /**
5684
+ * Get the abort signal for external use (e.g., XHR uploads).
5685
+ */
5686
+ getAbortSignal() {
5687
+ return this.closeController.signal;
5688
+ }
5625
5689
  /**
5626
5690
  * Get vault upload info (public endpoint).
5627
5691
  */
@@ -5674,8 +5738,26 @@ class UploadApi {
5674
5738
  * Internal request method with timeout handling and error context.
5675
5739
  */
5676
5740
  async request(url, options) {
5677
- const controller = new AbortController();
5678
- const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
5741
+ // Don't start new requests if already aborted (renderer closed)
5742
+ if (this.closeController.signal.aborted) {
5743
+ throw new UploadApiError('Request cancelled', 'cancelled', 0);
5744
+ }
5745
+ const timeoutController = new AbortController();
5746
+ const timeoutId = setTimeout(() => timeoutController.abort(), this.timeoutMs);
5747
+ // Combine timeout and close signals - abort on either
5748
+ let combinedSignal;
5749
+ if ('any' in AbortSignal) {
5750
+ combinedSignal = AbortSignal.any([
5751
+ timeoutController.signal,
5752
+ this.closeController.signal,
5753
+ ]);
5754
+ }
5755
+ else {
5756
+ // Fallback for older browsers: link close signal to timeout controller
5757
+ combinedSignal = timeoutController.signal;
5758
+ const onClose = () => timeoutController.abort();
5759
+ this.closeController.signal.addEventListener('abort', onClose);
5760
+ }
5679
5761
  try {
5680
5762
  const response = await fetch(url, {
5681
5763
  method: options.method,
@@ -5684,7 +5766,7 @@ class UploadApi {
5684
5766
  'Accept': 'application/json',
5685
5767
  },
5686
5768
  body: options.body,
5687
- signal: controller.signal,
5769
+ signal: combinedSignal,
5688
5770
  });
5689
5771
  const json = await response.json();
5690
5772
  if (!response.ok) {
@@ -5710,8 +5792,12 @@ class UploadApi {
5710
5792
  if (error instanceof SparkVaultError) {
5711
5793
  throw error;
5712
5794
  }
5713
- // Convert AbortError to TimeoutError
5795
+ // Convert AbortError to appropriate error
5714
5796
  if (error instanceof DOMException && error.name === 'AbortError') {
5797
+ // Check which signal triggered the abort
5798
+ if (this.closeController.signal.aborted) {
5799
+ throw new UploadApiError('Request cancelled', 'cancelled', 0);
5800
+ }
5715
5801
  throw new TimeoutError(`Request timed out after ${this.timeoutMs}ms`);
5716
5802
  }
5717
5803
  // Network errors with context
@@ -7573,6 +7659,8 @@ class UploadRenderer {
7573
7659
  this.uploadModal = null;
7574
7660
  this.isInlineMode = false;
7575
7661
  this.hybridModalInitialized = false;
7662
+ // Track active XHR for cancellation on close
7663
+ this.activeXhr = null;
7576
7664
  this.pasteHandler = null;
7577
7665
  this.container = container;
7578
7666
  this.api = api;
@@ -7605,8 +7693,16 @@ class UploadRenderer {
7605
7693
  }
7606
7694
  /**
7607
7695
  * Close the upload flow.
7696
+ * Cancels all pending API requests and active uploads to prevent orphaned operations.
7608
7697
  */
7609
7698
  close() {
7699
+ // Cancel all pending API requests first
7700
+ this.api.abort();
7701
+ // Abort any active XHR upload
7702
+ if (this.activeXhr) {
7703
+ this.activeXhr.abort();
7704
+ this.activeXhr = null;
7705
+ }
7610
7706
  this.cleanupFileInput();
7611
7707
  this.cleanupPasteHandler();
7612
7708
  // Clean up modal if open (hybrid mode)
@@ -8205,11 +8301,19 @@ class UploadRenderer {
8205
8301
  }
8206
8302
  }
8207
8303
  /**
8208
- * Upload a single chunk using XMLHttpRequest for progress tracking
8304
+ * Upload a single chunk using XMLHttpRequest for progress tracking.
8305
+ * Tracks the XHR so it can be cancelled if the renderer is closed.
8209
8306
  */
8210
8307
  uploadChunkWithProgress(uploadUrl, chunk, chunkStart, totalSize, tusVersion, file, ingotId, requestId) {
8211
8308
  return new Promise((resolve, reject) => {
8309
+ // Check if already aborted before starting
8310
+ if (this.api.isAborted()) {
8311
+ reject(new Error('Upload cancelled'));
8312
+ return;
8313
+ }
8212
8314
  const xhr = new XMLHttpRequest();
8315
+ // Track this XHR so it can be cancelled on close
8316
+ this.activeXhr = xhr;
8213
8317
  // Track progress within this chunk
8214
8318
  xhr.upload.onprogress = (e) => {
8215
8319
  if (e.lengthComputable) {
@@ -8233,6 +8337,7 @@ class UploadRenderer {
8233
8337
  }
8234
8338
  };
8235
8339
  xhr.onload = () => {
8340
+ this.activeXhr = null;
8236
8341
  if (xhr.status >= 200 && xhr.status < 300) {
8237
8342
  // Get new offset from server response
8238
8343
  const newOffsetHeader = xhr.getResponseHeader('Upload-Offset');
@@ -8243,8 +8348,18 @@ class UploadRenderer {
8243
8348
  reject(new Error(`Chunk upload failed with status ${xhr.status}`));
8244
8349
  }
8245
8350
  };
8246
- xhr.onerror = () => reject(new Error('Chunk upload failed'));
8247
- xhr.ontimeout = () => reject(new Error('Chunk upload timed out'));
8351
+ xhr.onerror = () => {
8352
+ this.activeXhr = null;
8353
+ reject(new Error('Chunk upload failed'));
8354
+ };
8355
+ xhr.ontimeout = () => {
8356
+ this.activeXhr = null;
8357
+ reject(new Error('Chunk upload timed out'));
8358
+ };
8359
+ xhr.onabort = () => {
8360
+ this.activeXhr = null;
8361
+ reject(new Error('Upload cancelled'));
8362
+ };
8248
8363
  xhr.open('PATCH', uploadUrl);
8249
8364
  xhr.setRequestHeader('Tus-Resumable', tusVersion);
8250
8365
  xhr.setRequestHeader('Upload-Offset', String(chunkStart));