@jonathanleelx/meddle 0.4.0-beta.9 → 0.4.0

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.
@@ -41,6 +41,27 @@ function isNpmInstall() {
41
41
  return installMethod === 'npm'
42
42
  }
43
43
 
44
+ function formatBytes(bytes) {
45
+ if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
46
+ if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`
47
+ return `${bytes} B`
48
+ }
49
+
50
+ const PROGRESS_WIDTH = 40
51
+
52
+ function drawProgressBar({ received, total, attempt, maxAttempts }) {
53
+ const label = attempt > 1 ? ` (第 ${attempt}/${maxAttempts} 次尝试)` : ''
54
+ if (total > 0) {
55
+ const ratio = Math.min(1, received / total)
56
+ const filled = Math.round(PROGRESS_WIDTH * ratio)
57
+ const bar = '#'.repeat(filled) + '-'.repeat(PROGRESS_WIDTH - filled)
58
+ const line = ` [${bar}] ${(ratio * 100).toFixed(1)}% ${formatBytes(received)}/${formatBytes(total)}${label}`
59
+ process.stdout.write(`\r${line}`)
60
+ } else {
61
+ process.stdout.write(`\r 下载中 ${formatBytes(received)}${label}...`)
62
+ }
63
+ }
64
+
44
65
  function showHelp() {
45
66
  output.plain(`
46
67
  meddle update - 检查并升级 meddle
@@ -85,17 +106,32 @@ async function installVersion(version) {
85
106
  const binDir = process.env.MEDDLE_BIN_DIR || path.join(home, 'bin')
86
107
  const destFile = path.join(binDir, os.platform() === 'win32' ? 'meddle.exe' : 'meddle')
87
108
  output.info(`下载 meddle ${version} (${os.platform()}/${os.arch()})...`)
109
+ let progressShown = false
110
+ const showProgress = output.isJsonMode() ? () => {} : drawProgressBar
88
111
  try {
89
112
  const result = await downloadBinaryAsset({
90
113
  version,
91
114
  destFile,
92
115
  platform: os.platform(),
93
116
  arch: os.arch(),
117
+ onAttempt: (attempt, maxAttempts) => {
118
+ if (attempt > 1) {
119
+ process.stdout.write('\n')
120
+ progressShown = false
121
+ output.info(`连接中断,正在重试 (${attempt}/${maxAttempts})...`)
122
+ }
123
+ },
124
+ onProgress: (p) => {
125
+ showProgress(p)
126
+ progressShown = true
127
+ },
94
128
  })
129
+ if (progressShown) process.stdout.write('\n')
95
130
  output.success(`已安装 meddle ${version} 到 ${result.installed}`)
96
131
  if (result.backup) output.info(`旧版本已备份到 ${result.backup}`)
97
132
  output.info('重启 meddle 后新版本生效')
98
133
  } catch (err) {
134
+ if (progressShown) process.stdout.write('\n')
99
135
  output.error(`升级失败: ${err && err.message ? err.message : err}`)
100
136
  process.exit(1)
101
137
  }
@@ -25,7 +25,7 @@ const DEFAULT_GITHUB_LATEST_URL = `https://github.com/${REPO}/releases/latest`
25
25
  const DEFAULT_DOWNLOAD_BASE_URL = `https://github.com/${REPO}/releases/download`
26
26
  const DEFAULT_CACHE_TTL_MS = 24 * 60 * 60 * 1000
27
27
  const DEFAULT_TIMEOUT_MS = 5000
28
- const DEFAULT_DOWNLOAD_TIMEOUT_MS = 120 * 1000
28
+ const DEFAULT_DOWNLOAD_TIMEOUT_MS = 5 * 60 * 1000
29
29
 
30
30
  const VERSION_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/
31
31
 
@@ -280,10 +280,13 @@ function sha256Hex(buffer) {
280
280
  * thrown (Windows commonly fails with EPERM on a running binary).
281
281
  *
282
282
  * The payload download is retried (default 3 attempts) because binaries are
283
- * large (100MB+) and flaky connections drop mid-body.
283
+ * large (100MB+) and flaky connections drop mid-body. Progress is reported
284
+ * via `onProgress` while streaming (received/total bytes), and `onAttempt`
285
+ * fires before each attempt (skipping retries is visible to callers).
284
286
  * @param {{ version: string, destFile: string, platform: string, arch: string,
285
287
  * baseUrl?: string, fetchImpl?: Function, timeoutMs?: number,
286
288
  * retries?: number, retryDelayMs?: number,
289
+ * onProgress?: Function, onAttempt?: Function,
287
290
  * fsImpl?: object }} opts
288
291
  * @returns {Promise<{ installed: string, backup: string, version: string }>}
289
292
  */
@@ -295,6 +298,8 @@ async function downloadBinaryAsset(opts) {
295
298
  const payloadTimeout = opts.timeoutMs || DEFAULT_DOWNLOAD_TIMEOUT_MS
296
299
  const maxRetries = opts.retries === undefined ? 3 : opts.retries
297
300
  const retryDelayMs = opts.retryDelayMs === undefined ? 1000 : opts.retryDelayMs
301
+ const onProgress = typeof opts.onProgress === 'function' ? opts.onProgress : () => {}
302
+ const onAttempt = typeof opts.onAttempt === 'function' ? opts.onAttempt : () => {}
298
303
 
299
304
  const asset = getAssetName({ platform, arch })
300
305
  const assetUrl = `${baseUrl}/v${version}/${asset}`
@@ -304,16 +309,36 @@ async function downloadBinaryAsset(opts) {
304
309
  // timeout for the payload and the regular timeout for the metadata fetch.
305
310
  const fetchPayload = async () => {
306
311
  let lastError = null
307
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
308
- if (attempt > 0 && retryDelayMs > 0) {
309
- await new Promise((r) => setTimeout(r, retryDelayMs * attempt))
312
+ for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
313
+ if (attempt > 1 && retryDelayMs > 0) {
314
+ await new Promise((r) => setTimeout(r, retryDelayMs * (attempt - 1)))
310
315
  }
316
+ onAttempt(attempt, maxRetries + 1)
311
317
  try {
312
318
  const response = await doFetch(assetUrl, { signal: AbortSignal.timeout(payloadTimeout) })
313
319
  if (!response.ok) throw new Error(`download failed (${response.status})`)
314
- return Buffer.from(await response.arrayBuffer())
320
+ const total = Number(response.headers.get('content-length')) || 0
321
+ const chunks = []
322
+ let received = 0
323
+ if (response.body && typeof response.body.getReader === 'function') {
324
+ const reader = response.body.getReader()
325
+ for (;;) {
326
+ const { done, value } = await reader.read()
327
+ if (done) break
328
+ chunks.push(Buffer.from(value))
329
+ received += value.byteLength
330
+ onProgress({ received, total })
331
+ }
332
+ } else {
333
+ const buf = Buffer.from(await response.arrayBuffer())
334
+ chunks.push(buf)
335
+ received = buf.length
336
+ onProgress({ received, total })
337
+ }
338
+ return Buffer.concat(chunks)
315
339
  } catch (err) {
316
340
  lastError = err
341
+ if (attempt > maxRetries) break
317
342
  }
318
343
  }
319
344
  throw lastError || new Error(`download failed for ${assetUrl}`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jonathanleelx/meddle",
3
- "version": "0.4.0-beta.9",
3
+ "version": "0.4.0",
4
4
  "description": "A simple development proxy server with custom routing rules, mock responses, HTTPS interception, and a plugin system",
5
5
  "type": "commonjs",
6
6
  "main": "index.js",