@vaadin/upload 25.0.2 → 25.1.0-alpha1

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/README.md CHANGED
@@ -28,6 +28,29 @@ Once installed, import the component in your application:
28
28
  import '@vaadin/upload';
29
29
  ```
30
30
 
31
+ ## Performance Considerations
32
+
33
+ When uploading large numbers of files, the component automatically throttles concurrent uploads to prevent browser performance degradation. By default, a maximum of 3 files are uploaded simultaneously, with additional files queued automatically.
34
+
35
+ You can customize this limit using the `max-concurrent-uploads` attribute:
36
+
37
+ ```html
38
+ <!-- Limit to 5 concurrent uploads -->
39
+ <vaadin-upload max-concurrent-uploads="5"></vaadin-upload>
40
+ ```
41
+
42
+ ```js
43
+ // Or set it programmatically
44
+ upload.maxConcurrentUploads = 5;
45
+ ```
46
+
47
+ This helps prevent:
48
+ - Browser XHR limitations (failures when uploading 2000+ files simultaneously)
49
+ - Performance degradation with hundreds of concurrent uploads
50
+ - Network congestion on slower connections
51
+
52
+ The default value of 3 balances upload performance with network resource conservation.
53
+
31
54
  ## Contributing
32
55
 
33
56
  Read the [contributing guide](https://vaadin.com/docs/latest/contributing) to learn about our development process, how to propose bugfixes and improvements, and how to test your changes to Vaadin components.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/upload",
3
- "version": "25.0.2",
3
+ "version": "25.1.0-alpha1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -34,23 +34,23 @@
34
34
  ],
35
35
  "dependencies": {
36
36
  "@open-wc/dedupe-mixin": "^1.3.0",
37
- "@vaadin/a11y-base": "~25.0.2",
38
- "@vaadin/button": "~25.0.2",
39
- "@vaadin/component-base": "~25.0.2",
40
- "@vaadin/progress-bar": "~25.0.2",
41
- "@vaadin/vaadin-themable-mixin": "~25.0.2",
37
+ "@vaadin/a11y-base": "25.1.0-alpha1",
38
+ "@vaadin/button": "25.1.0-alpha1",
39
+ "@vaadin/component-base": "25.1.0-alpha1",
40
+ "@vaadin/progress-bar": "25.1.0-alpha1",
41
+ "@vaadin/vaadin-themable-mixin": "25.1.0-alpha1",
42
42
  "lit": "^3.0.0"
43
43
  },
44
44
  "devDependencies": {
45
- "@vaadin/chai-plugins": "~25.0.2",
46
- "@vaadin/test-runner-commands": "~25.0.2",
45
+ "@vaadin/chai-plugins": "25.1.0-alpha1",
46
+ "@vaadin/test-runner-commands": "25.1.0-alpha1",
47
47
  "@vaadin/testing-helpers": "^2.0.0",
48
- "@vaadin/vaadin-lumo-styles": "~25.0.2",
48
+ "@vaadin/vaadin-lumo-styles": "25.1.0-alpha1",
49
49
  "sinon": "^21.0.0"
50
50
  },
51
51
  "web-types": [
52
52
  "web-types.json",
53
53
  "web-types.lit.json"
54
54
  ],
55
- "gitHead": "d17c1f8b7c6f3f991cafd9dbdbe5759caa57afcd"
55
+ "gitHead": "c789cdd350bcd74b280268a83f5475ad7f2f65e1"
56
56
  }
@@ -95,6 +95,9 @@ class UploadFile extends UploadFileMixin(ThemableMixin(PolylitMixin(LumoInjectio
95
95
 
96
96
  /** @protected */
97
97
  render() {
98
+ const isFileStartVisible = this.held && !this.uploading && !this.complete;
99
+ const isFileRetryVisible = this.errorMessage;
100
+
98
101
  return html`
99
102
  <div part="done-icon" ?hidden="${!this.complete}" aria-hidden="true"></div>
100
103
  <div part="warning-icon" ?hidden="${!this.errorMessage}" aria-hidden="true"></div>
@@ -111,7 +114,7 @@ class UploadFile extends UploadFileMixin(ThemableMixin(PolylitMixin(LumoInjectio
111
114
  part="start-button"
112
115
  file-event="file-start"
113
116
  @click="${this._fireFileEvent}"
114
- ?hidden="${!this.held}"
117
+ ?hidden="${!isFileStartVisible}"
115
118
  ?disabled="${this.disabled}"
116
119
  aria-label="${this.i18n ? this.i18n.file.start : nothing}"
117
120
  aria-describedby="name"
@@ -121,7 +124,7 @@ class UploadFile extends UploadFileMixin(ThemableMixin(PolylitMixin(LumoInjectio
121
124
  part="retry-button"
122
125
  file-event="file-retry"
123
126
  @click="${this._fireFileEvent}"
124
- ?hidden="${!this.errorMessage}"
127
+ ?hidden="${!isFileRetryVisible}"
125
128
  ?disabled="${this.disabled}"
126
129
  aria-label="${this.i18n ? this.i18n.file.retry : nothing}"
127
130
  aria-describedby="name"
@@ -205,6 +205,16 @@ export declare class UploadMixinClass {
205
205
  */
206
206
  uploadFormat: UploadFormat;
207
207
 
208
+ /**
209
+ * Specifies the maximum number of files that can be uploaded simultaneously.
210
+ * This helps prevent browser performance degradation and XHR limitations when
211
+ * uploading large numbers of files. Files exceeding this limit will be queued
212
+ * and uploaded as active uploads complete.
213
+ * @attr {number} max-concurrent-uploads
214
+ * @default 3
215
+ */
216
+ maxConcurrentUploads: number;
217
+
208
218
  /**
209
219
  * The object used to localize this component. To change the default
210
220
  * localization, replace this with an object that provides all properties, or
@@ -322,6 +322,20 @@ export const UploadMixin = (superClass) =>
322
322
  value: 'raw',
323
323
  },
324
324
 
325
+ /**
326
+ * Specifies the maximum number of files that can be uploaded simultaneously.
327
+ * This helps prevent browser performance degradation and XHR limitations when
328
+ * uploading large numbers of files. Files exceeding this limit will be queued
329
+ * and uploaded as active uploads complete.
330
+ * @attr {number} max-concurrent-uploads
331
+ * @type {number}
332
+ */
333
+ maxConcurrentUploads: {
334
+ type: Number,
335
+ value: 3,
336
+ sync: true,
337
+ },
338
+
325
339
  /**
326
340
  * Pass-through to input's capture attribute. Allows user to trigger device inputs
327
341
  * such as camera or microphone immediately.
@@ -347,6 +361,18 @@ export const UploadMixin = (superClass) =>
347
361
  _files: {
348
362
  type: Array,
349
363
  },
364
+
365
+ /** @private */
366
+ _uploadQueue: {
367
+ type: Array,
368
+ value: () => [],
369
+ },
370
+
371
+ /** @private */
372
+ _activeUploads: {
373
+ type: Number,
374
+ value: 0,
375
+ },
350
376
  };
351
377
  }
352
378
 
@@ -694,16 +720,46 @@ export const UploadMixin = (superClass) =>
694
720
  if (files && !Array.isArray(files)) {
695
721
  files = [files];
696
722
  }
697
- files = files.filter((file) => !file.complete);
698
- Array.prototype.forEach.call(files, this._uploadFile.bind(this));
723
+ files.filter((file) => !file.complete).forEach((file) => this._queueFileUpload(file));
699
724
  }
700
725
 
701
726
  /** @private */
702
- _uploadFile(file) {
727
+ _queueFileUpload(file) {
703
728
  if (file.uploading) {
704
729
  return;
705
730
  }
706
731
 
732
+ file.held = true;
733
+ file.uploading = file.indeterminate = true;
734
+ file.complete = file.abort = file.error = false;
735
+ file.status = this.__effectiveI18n.uploading.status.held;
736
+ this._renderFileList();
737
+
738
+ this._uploadQueue.push(file);
739
+ this._processUploadQueue();
740
+ }
741
+
742
+ /**
743
+ * Process the upload queue by starting uploads for queued files
744
+ * if there is available capacity.
745
+ *
746
+ * @private
747
+ */
748
+ _processUploadQueue() {
749
+ // Process as many queued files as we have capacity for
750
+ while (this._uploadQueue.length > 0 && this._activeUploads < this.maxConcurrentUploads) {
751
+ const nextFile = this._uploadQueue.shift();
752
+ if (nextFile) {
753
+ this._uploadFile(nextFile);
754
+ }
755
+ }
756
+ }
757
+
758
+ /** @private */
759
+ _uploadFile(file) {
760
+ // Increment active uploads counter
761
+ this._activeUploads += 1;
762
+
707
763
  const ini = Date.now();
708
764
  const xhr = (file.xhr = this._createXhr());
709
765
 
@@ -740,11 +796,22 @@ export const UploadMixin = (superClass) =>
740
796
  this.dispatchEvent(new CustomEvent('upload-progress', { detail: { file, xhr } }));
741
797
  };
742
798
 
799
+ xhr.onabort = () => {
800
+ // Decrement active uploads counter
801
+ this._activeUploads -= 1;
802
+ this._processUploadQueue();
803
+ };
804
+
743
805
  // More reliable than xhr.onload
744
806
  xhr.onreadystatechange = () => {
745
807
  if (xhr.readyState === 4) {
746
808
  clearTimeout(stalledId);
747
809
  file.indeterminate = file.uploading = false;
810
+
811
+ // Decrement active uploads counter
812
+ this._activeUploads -= 1;
813
+ this._processUploadQueue();
814
+
748
815
  if (file.abort) {
749
816
  return;
750
817
  }
@@ -815,9 +882,8 @@ export const UploadMixin = (superClass) =>
815
882
  xhr.open(this.method, file.uploadTarget, true);
816
883
  this._configureXhr(xhr, file, isRawUpload);
817
884
 
885
+ file.held = false;
818
886
  file.status = this.__effectiveI18n.uploading.status.connecting;
819
- file.uploading = file.indeterminate = true;
820
- file.complete = file.abort = file.error = file.held = false;
821
887
 
822
888
  xhr.upload.onloadstart = () => {
823
889
  this.dispatchEvent(
@@ -862,7 +928,7 @@ export const UploadMixin = (superClass) =>
862
928
  }),
863
929
  );
864
930
  if (evt) {
865
- this._uploadFile(file);
931
+ this._queueFileUpload(file);
866
932
  this._updateFocus(this.files.indexOf(file));
867
933
  }
868
934
  }
@@ -934,7 +1000,7 @@ export const UploadMixin = (superClass) =>
934
1000
  this.files = [file, ...this.files];
935
1001
 
936
1002
  if (!this.noAuto) {
937
- this._uploadFile(file);
1003
+ this._queueFileUpload(file);
938
1004
  }
939
1005
  }
940
1006
 
@@ -957,6 +1023,9 @@ export const UploadMixin = (superClass) =>
957
1023
  * @protected
958
1024
  */
959
1025
  _removeFile(file) {
1026
+ this._uploadQueue = this._uploadQueue.filter((f) => f !== file);
1027
+ this._processUploadQueue();
1028
+
960
1029
  const fileIndex = this.files.indexOf(file);
961
1030
  if (fileIndex >= 0) {
962
1031
  this.files = this.files.filter((i) => i !== file);
@@ -998,7 +1067,7 @@ export const UploadMixin = (superClass) =>
998
1067
 
999
1068
  /** @private */
1000
1069
  _onFileStart(event) {
1001
- this._uploadFile(event.detail.file);
1070
+ this._queueFileUpload(event.detail.file);
1002
1071
  }
1003
1072
 
1004
1073
  /** @private */
package/web-types.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/web-types",
3
3
  "name": "@vaadin/upload",
4
- "version": "25.0.2",
4
+ "version": "25.1.0-alpha1",
5
5
  "description-markup": "markdown",
6
6
  "contributions": {
7
7
  "html": {
@@ -373,6 +373,15 @@
373
373
  ]
374
374
  }
375
375
  },
376
+ {
377
+ "name": "max-concurrent-uploads",
378
+ "description": "Specifies the maximum number of files that can be uploaded simultaneously.\nThis helps prevent browser performance degradation and XHR limitations when\nuploading large numbers of files. Files exceeding this limit will be queued\nand uploaded as active uploads complete.",
379
+ "value": {
380
+ "type": [
381
+ "number"
382
+ ]
383
+ }
384
+ },
376
385
  {
377
386
  "name": "capture",
378
387
  "description": "Pass-through to input's capture attribute. Allows user to trigger device inputs\nsuch as camera or microphone immediately.",
@@ -534,6 +543,15 @@
534
543
  ]
535
544
  }
536
545
  },
546
+ {
547
+ "name": "maxConcurrentUploads",
548
+ "description": "Specifies the maximum number of files that can be uploaded simultaneously.\nThis helps prevent browser performance degradation and XHR limitations when\nuploading large numbers of files. Files exceeding this limit will be queued\nand uploaded as active uploads complete.",
549
+ "value": {
550
+ "type": [
551
+ "number"
552
+ ]
553
+ }
554
+ },
537
555
  {
538
556
  "name": "capture",
539
557
  "description": "Pass-through to input's capture attribute. Allows user to trigger device inputs\nsuch as camera or microphone immediately.",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/web-types",
3
3
  "name": "@vaadin/upload",
4
- "version": "25.0.2",
4
+ "version": "25.1.0-alpha1",
5
5
  "description-markup": "markdown",
6
6
  "framework": "lit",
7
7
  "framework-config": {
@@ -229,6 +229,13 @@
229
229
  "kind": "expression"
230
230
  }
231
231
  },
232
+ {
233
+ "name": ".maxConcurrentUploads",
234
+ "description": "Specifies the maximum number of files that can be uploaded simultaneously.\nThis helps prevent browser performance degradation and XHR limitations when\nuploading large numbers of files. Files exceeding this limit will be queued\nand uploaded as active uploads complete.",
235
+ "value": {
236
+ "kind": "expression"
237
+ }
238
+ },
232
239
  {
233
240
  "name": ".capture",
234
241
  "description": "Pass-through to input's capture attribute. Allows user to trigger device inputs\nsuch as camera or microphone immediately.",