@eik/core 2.1.74 → 2.1.75

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/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## [2.1.75](https://github.com/eik-lib/core/compare/v2.1.74...v2.1.75) (2026-08-14)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * cap concurrent sink write streams in _handleFile to 16 ([#634](https://github.com/eik-lib/core/issues/634)) ([c16c463](https://github.com/eik-lib/core/commit/c16c463f567b43fc617886e7f5d52acc1fd3d075))
7
+
1
8
  ## [2.1.74](https://github.com/eik-lib/core/compare/v2.1.73...v2.1.74) (2026-08-14)
2
9
 
3
10
 
@@ -158,6 +158,37 @@ const MultipartParser = class MultipartParser {
158
158
  /** @type {Record<string,any>|null} */
159
159
  let capturedEikJson = null;
160
160
 
161
+ // Limit how many sink write streams are open simultaneously.
162
+ // Without a cap, a tar with thousands of files would open an equal
163
+ // number of concurrent GCS resumable uploads or filesystem write
164
+ // streams, exhausting the connection pool or file descriptor limit.
165
+ const MAX_CONCURRENT_WRITES = 16;
166
+ let runningWrites = 0;
167
+ /** @type {import("tar").ReadEntry[]} */
168
+ const pendingEntries = [];
169
+
170
+ const drainPending = () => {
171
+ while (
172
+ runningWrites < MAX_CONCURRENT_WRITES &&
173
+ pendingEntries.length > 0
174
+ ) {
175
+ const entry = /** @type {import("tar").ReadEntry} */ (
176
+ pendingEntries.shift()
177
+ );
178
+ runningWrites++;
179
+ const persist = this._persistFile({
180
+ incoming,
181
+ entry,
182
+ signal: ac.signal,
183
+ }).finally(() => {
184
+ runningWrites--;
185
+ drainPending();
186
+ });
187
+ persist.catch(() => {});
188
+ queue.push(persist);
189
+ }
190
+ };
191
+
161
192
  file.once("limit", () => {
162
193
  this._log.warn(
163
194
  `multipart - Uploaded package exceeds the max allowed file size - Field: ${fieldname} - Filename: ${filename} - Name: ${incoming.name} - Version: ${incoming.version} - Type: ${incoming.type} - Limit: ${this._pkgMaxFileSize} bytes`,
@@ -199,15 +230,13 @@ const MultipartParser = class MultipartParser {
199
230
  return;
200
231
  }
201
232
 
202
- const persist = this._persistFile({
203
- incoming,
204
- entry,
205
- signal: ac.signal,
206
- });
207
- // Suppress unhandled rejections from writes aborted due to
208
- // an upstream error — they are intentionally abandoned.
209
- persist.catch(() => {});
210
- queue.push(persist);
233
+ // Queue the entry. drainPending() starts it immediately if
234
+ // a slot is free, otherwise holds it until one opens up.
235
+ // Holding an entry without piping it blocks the tar parser
236
+ // at that position in the archive, creating natural
237
+ // backpressure on the upstream upload stream.
238
+ pendingEntries.push(entry);
239
+ drainPending();
211
240
  },
212
241
  });
213
242
 
@@ -220,6 +249,13 @@ const MultipartParser = class MultipartParser {
220
249
 
221
250
  ac.abort();
222
251
 
252
+ // Drain any entries still waiting in the queue so the tar
253
+ // parser can wind down and the pipeline callback can fire.
254
+ for (const pending of pendingEntries) {
255
+ pending.resume();
256
+ }
257
+ pendingEntries.length = 0;
258
+
223
259
  switch (error.code) {
224
260
  case "TAR_BAD_ARCHIVE":
225
261
  case "TAR_ABORT":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eik/core",
3
- "version": "2.1.74",
3
+ "version": "2.1.75",
4
4
  "description": "Core server package",
5
5
  "main": "lib/main.js",
6
6
  "types": "./types/main.d.ts",