@eik/core 2.1.73 → 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,17 @@
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
+
8
+ ## [2.1.74](https://github.com/eik-lib/core/compare/v2.1.73...v2.1.74) (2026-08-14)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * prevent process crash on second busboy error and fix UTF-8 chunk corruption ([#633](https://github.com/eik-lib/core/issues/633)) ([9f5e3ec](https://github.com/eik-lib/core/commit/9f5e3ecab5ea8ceed16a0826c0c9c1824e68beac)), closes [#9](https://github.com/eik-lib/core/issues/9) [#11](https://github.com/eik-lib/core/issues/11)
14
+
1
15
  ## [2.1.73](https://github.com/eik-lib/core/compare/v2.1.72...v2.1.73) (2026-08-14)
2
16
 
3
17
 
@@ -106,7 +106,13 @@ const MultipartParser = class MultipartParser {
106
106
  },
107
107
  );
108
108
 
109
- busboy.once("error", (/** @type {any} */ error) => {
109
+ // Use .on() instead of .once() so that a second error emitted
110
+ // by a concurrent _handleFile failure (after the first error has
111
+ // already removed a .once() listener) does not hit zero listeners
112
+ // and crash the process with an unhandled EventEmitter throw.
113
+ // Calling reject() a second time is a safe no-op on an already-
114
+ // settled Promise.
115
+ busboy.on("error", (/** @type {any} */ error) => {
110
116
  reject(error);
111
117
  });
112
118
 
@@ -152,6 +158,37 @@ const MultipartParser = class MultipartParser {
152
158
  /** @type {Record<string,any>|null} */
153
159
  let capturedEikJson = null;
154
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
+
155
192
  file.once("limit", () => {
156
193
  this._log.warn(
157
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`,
@@ -193,15 +230,13 @@ const MultipartParser = class MultipartParser {
193
230
  return;
194
231
  }
195
232
 
196
- const persist = this._persistFile({
197
- incoming,
198
- entry,
199
- signal: ac.signal,
200
- });
201
- // Suppress unhandled rejections from writes aborted due to
202
- // an upstream error — they are intentionally abandoned.
203
- persist.catch(() => {});
204
- 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();
205
240
  },
206
241
  });
207
242
 
@@ -214,6 +249,13 @@ const MultipartParser = class MultipartParser {
214
249
 
215
250
  ac.abort();
216
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
+
217
259
  switch (error.code) {
218
260
  case "TAR_BAD_ARCHIVE":
219
261
  case "TAR_ABORT":
@@ -22,7 +22,7 @@ const readJSON = (sink, path) =>
22
22
 
23
23
  pipeline(from.stream, to, (error) => {
24
24
  if (error) return reject(error);
25
- const str = buffer.join("").toString();
25
+ const str = Buffer.concat(buffer).toString("utf8");
26
26
  try {
27
27
  const obj = JSON.parse(str);
28
28
  return resolve(obj);
@@ -86,7 +86,7 @@ const streamCollector = (from) =>
86
86
 
87
87
  pipeline(from, to, (error) => {
88
88
  if (error) return reject(error);
89
- return resolve(buffer.join("").toString());
89
+ return resolve(Buffer.concat(buffer).toString("utf8"));
90
90
  });
91
91
  });
92
92
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eik/core",
3
- "version": "2.1.73",
3
+ "version": "2.1.75",
4
4
  "description": "Core server package",
5
5
  "main": "lib/main.js",
6
6
  "types": "./types/main.d.ts",