com.jimuwd.xian.registry-proxy 1.0.32 → 1.0.34

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.
Files changed (3) hide show
  1. package/dist/index.js +45 -19
  2. package/package.json +1 -1
  3. package/src/index.ts +49 -18
package/dist/index.js CHANGED
@@ -7,6 +7,8 @@ import fetch from 'node-fetch';
7
7
  import { homedir } from 'os';
8
8
  import { join, resolve } from 'path';
9
9
  import { URL } from 'url';
10
+ import { Readable } from "node:stream";
11
+ import { pipeline } from "node:stream/promises";
10
12
  const { readFile, writeFile } = fsPromises;
11
13
  class ConcurrencyLimiter {
12
14
  maxConcurrency;
@@ -162,12 +164,15 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
162
164
  console.log(`Proxying: ${relativePathPrefixedWithSlash}`);
163
165
  // 修改为按顺序尝试注册表,找到第一个成功响应即返回
164
166
  for (const { normalizedRegistryUrl, token } of registryInfos) {
167
+ if (req.destroyed)
168
+ break;
165
169
  await limiter.acquire();
170
+ let response = null;
166
171
  try {
167
172
  const targetUrl = `${normalizedRegistryUrl}${relativePathPrefixedWithSlash}${fullUrl.search || ''}`;
168
173
  console.log(`Fetching from: ${targetUrl}`);
169
174
  const headers = token ? { Authorization: `Bearer ${token}` } : undefined;
170
- const response = await fetch(targetUrl, { headers });
175
+ response = await fetch(targetUrl, { headers });
171
176
  console.log(`Response from ${targetUrl}: ${response.status} ${response.statusText}`);
172
177
  if (response.ok) {
173
178
  const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
@@ -202,37 +207,58 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
202
207
  }
203
208
  }
204
209
  else {
205
- // 非application/json则是tarball
210
+ // 二进制流处理
206
211
  if (!response.body) {
207
- console.error(`Empty response body from ${response.url}, status: ${response.status}`);
208
- // 继续尝试下一个注册表
212
+ console.error(`Empty response body from ${response.url}`);
209
213
  continue;
210
214
  }
211
- const contentLength = response.headers.get('Content-Length');
212
- const safeHeaders = {};
213
- safeHeaders["content-type"] = contentType;
214
- if (contentLength && !isNaN(Number(contentLength)))
215
- safeHeaders["content-length"] = contentLength;
216
- res.writeHead(response.status, safeHeaders);
217
- response.body.pipe(res).on('error', (err) => {
218
- console.error(`Stream error for ${relativePathPrefixedWithSlash}:`, err);
219
- res.writeHead(502).end('Stream Error');
215
+ // 头信息处理
216
+ const safeHeaders = {
217
+ 'content-type': contentType,
218
+ 'connection': 'keep-alive',
219
+ };
220
+ // 复制关键头信息
221
+ ['cache-control', 'etag', 'last-modified', 'content-encoding'].forEach(header => {
222
+ const value = response?.headers.get(header);
223
+ if (value)
224
+ safeHeaders[header] = value;
220
225
  });
221
- return;
226
+ res.writeHead(response.status, safeHeaders);
227
+ // 流转换与传输
228
+ const nodeStream = Readable.fromWeb(response.body);
229
+ let isComplete = false;
230
+ const cleanUp = () => {
231
+ if (!isComplete)
232
+ nodeStream.destroy();
233
+ };
234
+ try {
235
+ req.on('close', cleanUp);
236
+ res.on('close', cleanUp);
237
+ await pipeline(nodeStream, res);
238
+ isComplete = true;
239
+ return;
240
+ }
241
+ finally {
242
+ req.off('close', cleanUp);
243
+ res.off('close', cleanUp);
244
+ }
222
245
  }
223
246
  }
224
247
  }
225
248
  catch (e) {
226
- console.error(`Failed to fetch from ${normalizedRegistryUrl}:`, e);
227
- // 继续尝试下一个注册表
249
+ // 增强错误日志
250
+ if (e instanceof Error) {
251
+ console.error(e.code === 'ECONNREFUSED'
252
+ ? `Registry ${normalizedRegistryUrl} unreachable [ECONNREFUSED]`
253
+ : `Error from ${normalizedRegistryUrl}: ${e.message}`);
254
+ }
228
255
  }
229
256
  finally {
230
257
  limiter.release();
231
258
  }
232
259
  }
233
- // 所有注册表都尝试失败
234
- console.error(`All registries failed for ${relativePathPrefixedWithSlash}`);
235
- res.writeHead(404).end('Not Found - All upstream registries failed');
260
+ // 所有注册表尝试失败
261
+ res.writeHead(404).end('All upstream registries failed');
236
262
  };
237
263
  let server;
238
264
  if (proxyInfo.https) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "com.jimuwd.xian.registry-proxy",
3
- "version": "1.0.32",
3
+ "version": "1.0.34",
4
4
  "type": "module",
5
5
  "description": "A lightweight npm registry proxy with fallback support",
6
6
  "main": "dist/index.js",
package/src/index.ts CHANGED
@@ -8,6 +8,8 @@ import fetch, {Response} from 'node-fetch';
8
8
  import {homedir} from 'os';
9
9
  import {join, resolve} from 'path';
10
10
  import {URL} from 'url';
11
+ import {Readable} from "node:stream";
12
+ import {pipeline} from "node:stream/promises";
11
13
 
12
14
  const {readFile, writeFile} = fsPromises;
13
15
 
@@ -221,12 +223,14 @@ export async function startProxyServer(
221
223
 
222
224
  // 修改为按顺序尝试注册表,找到第一个成功响应即返回
223
225
  for (const {normalizedRegistryUrl, token} of registryInfos) {
226
+ if (req.destroyed) break;
224
227
  await limiter.acquire();
228
+ let response: Response | null = null;
225
229
  try {
226
230
  const targetUrl = `${normalizedRegistryUrl}${relativePathPrefixedWithSlash}${fullUrl.search || ''}`;
227
231
  console.log(`Fetching from: ${targetUrl}`);
228
232
  const headers = token ? {Authorization: `Bearer ${token}`} : undefined;
229
- const response = await fetch(targetUrl, {headers});
233
+ response = await fetch(targetUrl, {headers});
230
234
  console.log(`Response from ${targetUrl}: ${response.status} ${response.statusText}`);
231
235
 
232
236
  if (response.ok) {
@@ -260,35 +264,62 @@ export async function startProxyServer(
260
264
  // 继续尝试下一个注册表
261
265
  }
262
266
  } else {
263
- // 非application/json则是tarball
267
+ // 二进制流处理
264
268
  if (!response.body) {
265
- console.error(`Empty response body from ${response.url}, status: ${response.status}`);
266
- // 继续尝试下一个注册表
269
+ console.error(`Empty response body from ${response.url}`);
267
270
  continue;
268
271
  }
269
- const contentLength = response.headers.get('Content-Length');
270
- const safeHeaders: OutgoingHttpHeaders = {};
271
- safeHeaders["content-type"] = contentType;
272
- if (contentLength && !isNaN(Number(contentLength))) safeHeaders["content-length"] = contentLength;
273
- res.writeHead(response.status, safeHeaders);
274
- response.body.pipe(res).on('error', (err: any) => {
275
- console.error(`Stream error for ${relativePathPrefixedWithSlash}:`, err);
276
- res.writeHead(502).end('Stream Error');
272
+
273
+ // 头信息处理
274
+ const safeHeaders: OutgoingHttpHeaders = {
275
+ 'content-type': contentType,
276
+ 'connection': 'keep-alive',
277
+ };
278
+
279
+ // 复制关键头信息
280
+ ['cache-control', 'etag', 'last-modified', 'content-encoding'].forEach(header => {
281
+ const value = response?.headers.get(header);
282
+ if (value) safeHeaders[header] = value;
277
283
  });
278
- return;
284
+
285
+ res.writeHead(response.status, safeHeaders);
286
+
287
+ // 流转换与传输
288
+ const nodeStream = Readable.fromWeb(response.body as any);
289
+ let isComplete = false;
290
+ const cleanUp = () => {
291
+ if (!isComplete) nodeStream.destroy();
292
+ };
293
+
294
+
295
+ try {
296
+ req.on('close', cleanUp);
297
+ res.on('close', cleanUp);
298
+ await pipeline(nodeStream, res);
299
+ isComplete = true;
300
+ return;
301
+ } finally {
302
+ req.off('close', cleanUp);
303
+ res.off('close', cleanUp);
304
+ }
279
305
  }
280
306
  }
281
307
  } catch (e) {
282
- console.error(`Failed to fetch from ${normalizedRegistryUrl}:`, e);
283
- // 继续尝试下一个注册表
308
+ // 增强错误日志
309
+ if (e instanceof Error) {
310
+ console.error(
311
+ (e as any).code === 'ECONNREFUSED'
312
+ ? `Registry ${normalizedRegistryUrl} unreachable [ECONNREFUSED]`
313
+ : `Error from ${normalizedRegistryUrl}: ${e.message}`
314
+ );
315
+ }
284
316
  } finally {
285
317
  limiter.release();
286
318
  }
287
319
  }
288
320
 
289
- // 所有注册表都尝试失败
290
- console.error(`All registries failed for ${relativePathPrefixedWithSlash}`);
291
- res.writeHead(404).end('Not Found - All upstream registries failed');
321
+ // 所有注册表尝试失败
322
+ res.writeHead(404).end('All upstream registries failed');
292
323
  };
293
324
 
294
325
  let server: HttpServer | HttpsServer;