@eik/core 2.1.74 → 2.1.76

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.76](https://github.com/eik-lib/core/compare/v2.1.75...v2.1.76) (2026-08-14)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * propagate sink errors in _readVersions rather than silently resetting version history ([#635](https://github.com/eik-lib/core/issues/635)) ([b248bb7](https://github.com/eik-lib/core/commit/b248bb7b644ccd8fb7116157c44917b1ccf7990e))
7
+
8
+ ## [2.1.75](https://github.com/eik-lib/core/compare/v2.1.74...v2.1.75) (2026-08-14)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * 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))
14
+
1
15
  ## [2.1.74](https://github.com/eik-lib/core/compare/v2.1.73...v2.1.74) (2026-08-14)
2
16
 
3
17
 
@@ -172,25 +172,32 @@ const MapPut = class MapPut {
172
172
 
173
173
  /**
174
174
  * @param {any} incoming
175
+ * @returns {Promise<InstanceType<typeof Versions>>}
175
176
  */
176
177
  async _readVersions(incoming) {
177
178
  const path = createFilePathToVersion(incoming);
178
- let versions;
179
+
180
+ if (!this._sink) throw new Error("No sink configured");
181
+
179
182
  try {
180
- const obj = await readJSON(this._sink, path);
181
- versions = new Versions(obj);
182
- this._log.info(
183
- `map:put - Successfully read version meta file from sink - Pathname: ${path}`,
184
- );
185
- // eslint-disable-next-line no-unused-vars
186
- } catch (error) {
187
- // File does not exist, its probably a new package
188
- versions = new Versions(incoming);
183
+ await this._sink.exist(path);
184
+ } catch {
185
+ // File does not exist — this is a new import map.
189
186
  this._log.info(
190
187
  `map:put - Version meta file did not exist in sink - Create new - Pathname: ${path}`,
191
188
  );
189
+ return new Versions(incoming);
192
190
  }
193
- return versions;
191
+
192
+ // File exists. Any failure here must propagate rather than be
193
+ // swallowed — silently falling back to an empty Versions object
194
+ // would cause _writeVersions to overwrite the index and erase the
195
+ // entire version history.
196
+ const obj = await readJSON(this._sink, path);
197
+ this._log.info(
198
+ `map:put - Successfully read version meta file from sink - Pathname: ${path}`,
199
+ );
200
+ return new Versions(obj);
194
201
  }
195
202
 
196
203
  /**
@@ -251,7 +258,11 @@ const MapPut = class MapPut {
251
258
  org,
252
259
  });
253
260
 
254
- const versions = await this._readVersions(incoming);
261
+ const versions = await this._readVersions(incoming).catch(() => {
262
+ const e = new HttpError.BadGateway();
263
+ end({ labels: { success: false, status: e.status, type: "map" } });
264
+ throw e;
265
+ });
255
266
 
256
267
  if (!versions.check(pVersion)) {
257
268
  this._log.info(
@@ -138,25 +138,32 @@ const PkgPut = class PkgPut {
138
138
 
139
139
  /**
140
140
  * @param {any} incoming
141
+ * @returns {Promise<InstanceType<typeof Versions>>}
141
142
  */
142
143
  async _readVersions(incoming) {
143
144
  const path = createFilePathToVersion(incoming);
144
- let versions;
145
+
146
+ if (!this._sink) throw new Error("No sink configured");
147
+
145
148
  try {
146
- const obj = await readJSON(this._sink, path);
147
- versions = new Versions(obj);
148
- this._log.info(
149
- `pkg:put - Successfully read version meta file from sink - Pathname: ${path}`,
150
- );
151
- // eslint-disable-next-line no-unused-vars
152
- } catch (error) {
153
- // File does not exist, its probably a new package
154
- versions = new Versions(incoming);
149
+ await this._sink.exist(path);
150
+ } catch {
151
+ // File does not exist — this is a new package.
155
152
  this._log.info(
156
153
  `pkg:put - Version meta file did not exist in sink - Create new - Pathname: ${path}`,
157
154
  );
155
+ return new Versions(incoming);
158
156
  }
159
- return versions;
157
+
158
+ // File exists. Any failure here (GCS error, corrupt JSON, network
159
+ // timeout) must propagate rather than be swallowed — silently falling
160
+ // back to an empty Versions object would cause _writeVersions to
161
+ // overwrite the index and erase the entire version history.
162
+ const obj = await readJSON(this._sink, path);
163
+ this._log.info(
164
+ `pkg:put - Successfully read version meta file from sink - Pathname: ${path}`,
165
+ );
166
+ return new Versions(obj);
160
167
  }
161
168
 
162
169
  /**
@@ -251,7 +258,12 @@ const PkgPut = class PkgPut {
251
258
  throw e;
252
259
  }
253
260
 
254
- const versions = await this._readVersions(incoming);
261
+ const versions = await this._readVersions(incoming).catch(() => {
262
+ const e = new HttpError.BadGateway();
263
+ end({ labels: { success: false, status: e.status, type } });
264
+ throw e;
265
+ });
266
+
255
267
  const { pkg, eikJson } = await this._parser(incoming);
256
268
  versions.setVersion(pVersion, pkg.integrity);
257
269
 
@@ -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.76",
4
4
  "description": "Core server package",
5
5
  "main": "lib/main.js",
6
6
  "types": "./types/main.d.ts",
@@ -42,27 +42,9 @@ declare const MapPut: {
42
42
  }): Promise<string>;
43
43
  /**
44
44
  * @param {any} incoming
45
+ * @returns {Promise<InstanceType<typeof Versions>>}
45
46
  */
46
- _readVersions(incoming: any): Promise<{
47
- _versions: Map<any, any>;
48
- _type: string;
49
- _name: string;
50
- _org: string;
51
- get versions(): [any, any][];
52
- get type(): string;
53
- get name(): string;
54
- get org(): string;
55
- setVersion(version: string, integrity: string): void;
56
- getVersion(major: number): any;
57
- check(version: string): boolean;
58
- toJSON(): {
59
- versions: [any, any][];
60
- type: string;
61
- name: string;
62
- org: string;
63
- };
64
- get [Symbol.toStringTag](): string;
65
- }>;
47
+ _readVersions(incoming: any): Promise<InstanceType<typeof Versions>>;
66
48
  /**
67
49
  * @param {any} incoming
68
50
  * @param {any} versions
@@ -102,3 +84,4 @@ declare const MapPut: {
102
84
  };
103
85
  import abslog from "abslog";
104
86
  import Metrics from "@metrics/client";
87
+ import Versions from "../classes/versions.js";
@@ -78,27 +78,9 @@ declare const PkgPut: {
78
78
  _writeEikJson(incoming: any, capturedEikJson: Record<string, any> | null): Promise<void>;
79
79
  /**
80
80
  * @param {any} incoming
81
+ * @returns {Promise<InstanceType<typeof Versions>>}
81
82
  */
82
- _readVersions(incoming: any): Promise<{
83
- _versions: Map<any, any>;
84
- _type: string;
85
- _name: string;
86
- _org: string;
87
- get versions(): [any, any][];
88
- get type(): string;
89
- get name(): string;
90
- get org(): string;
91
- setVersion(version: string, integrity: string): void;
92
- getVersion(major: number): any;
93
- check(version: string): boolean;
94
- toJSON(): {
95
- versions: [any, any][];
96
- type: string;
97
- name: string;
98
- org: string;
99
- };
100
- get [Symbol.toStringTag](): string;
101
- }>;
83
+ _readVersions(incoming: any): Promise<InstanceType<typeof Versions>>;
102
84
  /**
103
85
  * @param {any} incoming
104
86
  */
@@ -143,3 +125,4 @@ declare const PkgPut: {
143
125
  };
144
126
  import abslog from "abslog";
145
127
  import Metrics from "@metrics/client";
128
+ import Versions from "../classes/versions.js";