dsh-dl-server 0.1.0 → 0.2.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.
package/lib/tools.js CHANGED
@@ -111,14 +111,15 @@ export function registerTools({ ctx, queue, config }) {
111
111
  additionalProperties: false,
112
112
  },
113
113
  async execute(args) {
114
+ const health = queue.health()
114
115
  if (args?.id) {
115
116
  const job = queue.get(args.id)
116
117
  return job
117
- ? { ok: true, count: 1, jobs: [job] }
118
+ ? { ok: true, count: 1, jobs: [job], ...health }
118
119
  : { ok: false, code: 'not-found', message: `${args.id} does not exist` }
119
120
  }
120
121
  const jobs = queue.list()
121
- return { ok: true, count: jobs.length, jobs }
122
+ return { ok: true, count: jobs.length, jobs, ...health }
122
123
  },
123
124
  })
124
125
 
package/lib/transport.js CHANGED
@@ -16,10 +16,13 @@ import { randomBytes } from 'node:crypto'
16
16
  * @typedef {object} TransferOptions
17
17
  * @property {string[]} urls candidate sources, tried in order
18
18
  * @property {string} destPath absolute final file path
19
- * @property {number} [connections] aria2 split, default 16
19
+ * @property {number} [connections] aria2 split for a large file, default 16
20
+ * @property {number} [connectionsCap] upper bound when the size picks the count
21
+ * @property {boolean} [preferDirect] skip aria2 for this job
20
22
  * @property {number} [speedLimitKbps] 0 or absent means unlimited
21
23
  * @property {string} [proxy] e.g. http://127.0.0.1:65532
22
24
  * @property {number} [bytesDone] resume hint for the direct channel
25
+ * @property {(url: string, ok: boolean) => void} [onUrlResult]
23
26
  * @property {(done: number, total: number) => void} [onProgress]
24
27
  * @property {AbortSignal} [signal]
25
28
  *
@@ -28,6 +31,28 @@ import { randomBytes } from 'node:crypto'
28
31
  * @property {number} total -1 when the server never reported one
29
32
  */
30
33
 
34
+ /** Below this, aria2's setup cost exceeds any segmentation gain. */
35
+ export const SMALL_FILE_BYTES = 1 * 1024 * 1024
36
+ /** From this size up, extra connections pay for themselves. */
37
+ export const LARGE_FILE_BYTES = 8 * 1024 * 1024
38
+
39
+ /**
40
+ * Decide the channel and the number of connections from the file size.
41
+ *
42
+ * Segmentation only helps once `min-split-size=1M` can produce several pieces,
43
+ * and too many connections on a per-IP-throttled host hurts rather than helps.
44
+ * @param {number} sizeBytes
45
+ * @param {number} [cap]
46
+ * @returns {{ connections: number, direct: boolean }}
47
+ */
48
+ export function planTransfer(sizeBytes, cap = 16) {
49
+ if (!Number.isFinite(sizeBytes) || sizeBytes <= 0) return { connections: Math.min(cap, 8), direct: false }
50
+ if (sizeBytes < SMALL_FILE_BYTES) return { connections: 1, direct: true }
51
+ if (sizeBytes < LARGE_FILE_BYTES) return { connections: Math.min(4, cap), direct: false }
52
+ return { connections: cap, direct: false }
53
+ }
54
+
55
+
31
56
  const IS_WIN = process.platform === 'win32'
32
57
  const ARIA2_BIN = IS_WIN ? 'aria2c.exe' : 'aria2c'
33
58
 
@@ -222,9 +247,17 @@ export class Transport {
222
247
 
223
248
  /** @param {TransferOptions} options @returns {Promise<TransferResult>} */
224
249
  async transfer(options) {
225
- if (this.mode === 'aria2' && this.rpc) {
250
+ const urls = options.urls
251
+ const declared = await this.#probeSize(urls, options.signal)
252
+ const plan = planTransfer(declared, options.connectionsCap ?? options.connections ?? this.config.connectionsPerJob ?? 16)
253
+ const connections = options.connections ?? plan.connections
254
+ const useAria2 = this.mode === 'aria2' && this.rpc && !options.preferDirect && !plan.direct
255
+
256
+ if (useAria2) {
226
257
  try {
227
- return await this.#transferAria2(options)
258
+ const result = await this.#transferAria2({ ...options, connections })
259
+ options.onUrlResult?.(urls[0], true)
260
+ return result
228
261
  } catch (error) {
229
262
  if (options.signal?.aborted) throw error
230
263
  this.log('warn', `aria2 transfer failed, retrying single-stream: ${error.message}`)
@@ -233,6 +266,35 @@ export class Transport {
233
266
  return this.#transferDirect(options)
234
267
  }
235
268
 
269
+ /**
270
+ * Best-effort Content-Length lookup. A failure is not an error: unknown size
271
+ * only means the planner falls back to its middle-of-the-road setting.
272
+ * @param {string[]} urls
273
+ * @param {AbortSignal} [signal]
274
+ * @returns {Promise<number>} the size, or -1 when it could not be determined
275
+ */
276
+ async #probeSize(urls, signal) {
277
+ const url = urls[0]
278
+ if (!url) return -1
279
+ const controller = new AbortController()
280
+ const timer = setTimeout(() => controller.abort(), 5000)
281
+ timer.unref?.()
282
+ const onAbort = () => controller.abort()
283
+ signal?.addEventListener('abort', onAbort, { once: true })
284
+
285
+ try {
286
+ const response = await fetch(url, { method: 'HEAD', signal: controller.signal })
287
+ if (!response.ok) return -1
288
+ const length = Number(response.headers.get('content-length') ?? 0)
289
+ return Number.isFinite(length) && length > 0 ? length : -1
290
+ } catch {
291
+ return -1
292
+ } finally {
293
+ clearTimeout(timer)
294
+ signal?.removeEventListener('abort', onAbort)
295
+ }
296
+ }
297
+
236
298
  /** @param {TransferOptions} options */
237
299
  async #transferAria2({ urls, destPath, connections, speedLimitKbps, onProgress, signal }) {
238
300
  const rpc = this.rpc
@@ -284,7 +346,7 @@ export class Transport {
284
346
  }
285
347
 
286
348
  /** @param {TransferOptions} options */
287
- async #transferDirect({ urls, destPath, bytesDone = 0, onProgress, signal }) {
349
+ async #transferDirect({ urls, destPath, bytesDone = 0, onProgress, onUrlResult, signal }) {
288
350
  await fs.mkdir(path.dirname(destPath), { recursive: true })
289
351
  const partPath = `${destPath}.part`
290
352
  let lastError
@@ -326,10 +388,12 @@ export class Transport {
326
388
  await pipeline(source, sink)
327
389
  onProgress?.(written, total)
328
390
  await fs.rename(partPath, destPath)
391
+ onUrlResult?.(url, true)
329
392
  return { bytes: written, total }
330
393
  } catch (error) {
331
394
  if (error?.name === 'AbortError' || signal?.aborted) throw error
332
395
  lastError = error
396
+ onUrlResult?.(url, false)
333
397
  }
334
398
  }
335
399
  throw lastError ?? new Error('no source available')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-dl-server",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "DLS — DSH Download Server: a global, session-aware, persistent download queue for DeepSeek Harness.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -14,7 +14,7 @@
14
14
  "cordis.patch.yml",
15
15
  "LICENSE",
16
16
  "README.md",
17
- "README.en.md"
17
+ "README.zh.md"
18
18
  ],
19
19
  "keywords": [
20
20
  "deepseek-harness",
@@ -37,18 +37,27 @@
37
37
  "url": "https://github.com/Jestions-dev/dsh-download-server/issues"
38
38
  },
39
39
  "engines": {
40
- "node": ">=20"
40
+ "node": ">=20",
41
+ "dsh": ">=0.2.1-rc.1 <0.3.0-0"
41
42
  },
42
43
  "scripts": {
43
44
  "test": "node --test",
44
45
  "check": "node --check lib/index.js && node --check lib/domain.js && node --check lib/types.js"
45
46
  },
46
47
  "dependencies": {
47
- "@deepseek-ai/cordis": "~4.1.0",
48
- "@deepseek-ai/dsh-storage": "^0.2.2-rc.1",
49
- "@deepseek-ai/dsh-storage-domain": "^0.2.2-rc.1",
50
48
  "zod": "^4.4.3"
51
49
  },
50
+ "peerDependencies": {
51
+ "@deepseek-ai/dsh-storage-domain": ">=0.2.0-rc.1 <0.3.0-0"
52
+ },
53
+ "peerDependenciesMeta": {
54
+ "@deepseek-ai/dsh-storage-domain": {
55
+ "optional": true
56
+ }
57
+ },
58
+ "devDependencies": {
59
+ "@deepseek-ai/dsh-storage-domain": "^0.2.2-rc.1"
60
+ },
52
61
  "dsh": {
53
62
  "bundle": {
54
63
  "patch": "./cordis.patch.yml"