pixivflow 2.19.3 → 2.19.5

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 (30) hide show
  1. package/dist/commands/scheduler-runtime.js +4 -15
  2. package/dist/config/defaults.d.ts +1 -0
  3. package/dist/config/defaults.js +4 -0
  4. package/dist/config/environment.js +19 -0
  5. package/dist/config/types.d.ts +21 -0
  6. package/dist/config/validation.js +15 -0
  7. package/dist/delivery/DeliveryAck.d.ts +14 -0
  8. package/dist/delivery/DeliveryAck.js +18 -0
  9. package/dist/delivery/DeliveryService.d.ts +9 -0
  10. package/dist/delivery/DeliveryService.js +11 -0
  11. package/dist/delivery/OutboxWorker.js +13 -0
  12. package/dist/delivery/settleDeliveryTerminal.d.ts +25 -0
  13. package/dist/delivery/settleDeliveryTerminal.js +40 -0
  14. package/dist/download/DownloadManager.js +12 -1
  15. package/dist/download/handlers/IllustrationTargetHandler.d.ts +42 -1
  16. package/dist/download/handlers/IllustrationTargetHandler.js +238 -37
  17. package/dist/download/handlers/NovelTargetHandler.d.ts +47 -0
  18. package/dist/download/handlers/NovelTargetHandler.js +211 -27
  19. package/dist/download/pipeline/DownloadPipeline.d.ts +22 -3
  20. package/dist/download/pipeline/DownloadPipeline.js +109 -92
  21. package/dist/download/plan/DownloadPlanner.d.ts +47 -1
  22. package/dist/download/plan/DownloadPlanner.js +105 -15
  23. package/dist/package.json +1 -1
  24. package/dist/scheduler/TargetOutcome.d.ts +195 -2
  25. package/dist/scheduler/TargetOutcome.js +202 -3
  26. package/dist/storage/repositories/DeliveryRepository.d.ts +12 -0
  27. package/dist/storage/repositories/DeliveryRepository.js +18 -1
  28. package/dist/version.js +1 -1
  29. package/dist/webui/package.json +1 -1
  30. package/package.json +1 -1
@@ -5,8 +5,10 @@ const logger_1 = require("../../logger");
5
5
  const errors_1 = require("../../utils/errors");
6
6
  const pixiv_date_utils_1 = require("../../utils/pixiv-date-utils");
7
7
  const pixiv_utils_1 = require("../../utils/pixiv-utils");
8
+ const TargetOutcome_1 = require("../../scheduler/TargetOutcome");
8
9
  const WorkIdentity_1 = require("../../scheduler/WorkIdentity");
9
10
  const target_label_1 = require("../../utils/target-label");
11
+ const DownloadPlanner_1 = require("../plan/DownloadPlanner");
10
12
  class NovelTargetHandler {
11
13
  client;
12
14
  database;
@@ -16,6 +18,34 @@ class NovelTargetHandler {
16
18
  topicPipelineFactory;
17
19
  deliveryService;
18
20
  outcomes = [];
21
+ /**
22
+ * Candidate scan of the last pipeline.run() of this handle() call: how many
23
+ * candidates were attempted, which were skipped and why, and whether any
24
+ * job-level outage appeared. This is what turns an empty result into an
25
+ * explicit verdict instead of an ambiguous "completed".
26
+ */
27
+ scan = null;
28
+ /**
29
+ * Global candidate-scan bound (`download.candidateScanLimit`), supplied by
30
+ * DownloadManager. This is what lets the FETCH stage ask for more than one
31
+ * candidate: a scheduled one-post-per-slot target has `limit: 1`, and asking
32
+ * the ranking API for exactly one work is the reason a single duplicate used
33
+ * to be unfixable downstream.
34
+ */
35
+ defaultCandidateScanLimit;
36
+ /** Publish the global candidate-scan bound for the fetch stage. */
37
+ setDefaultCandidateScanLimit(limit) {
38
+ this.defaultCandidateScanLimit = limit;
39
+ }
40
+ /**
41
+ * How many candidates to FETCH so the bounded scan has something to choose
42
+ * from. Same rule the planner applies to the attempt window, so fetch and
43
+ * scan agree on one bound.
44
+ */
45
+ candidateFetchLimit(target) {
46
+ const targetLimit = target.limit && target.limit > 0 ? target.limit : 10;
47
+ return Math.max(targetLimit, (0, DownloadPlanner_1.resolveCandidateScanLimit)(target, this.defaultCandidateScanLimit));
48
+ }
19
49
  /**
20
50
  * Cell identity for this handle() call. Set only for a single-work cell of a
21
51
  * scheduled occurrence; null for ad-hoc runs and N-works-per-run targets, which
@@ -33,6 +63,7 @@ class NovelTargetHandler {
33
63
  }
34
64
  async handle(target, execution) {
35
65
  this.outcomes = [];
66
+ this.scan = null;
36
67
  this.execution = execution && (0, WorkIdentity_1.isSingleWorkCell)(target) ? execution : null;
37
68
  // A cell that already owns a work is in RECOVERY, not in a new selection.
38
69
  // Crash/shutdown recovery is not an intentional second run: running the
@@ -72,6 +103,7 @@ class NovelTargetHandler {
72
103
  }
73
104
  const novels = await this.fetchNovels(target, mode);
74
105
  const result = await this.pipeline.run(novels, target, 'novel', (novel, tag) => this.downloadAndDeliver(novel, tag, target));
106
+ this.scan = result.scan;
75
107
  await this.handleDownloadResult(result, target, mode, novels.length);
76
108
  return this.summarize();
77
109
  }
@@ -79,26 +111,84 @@ class NovelTargetHandler {
79
111
  return this.classifyError(error, displayTag, mode);
80
112
  }
81
113
  }
114
+ /**
115
+ * Reduce the outcomes collected while processing one target to one verdict,
116
+ * including the bounded-scan bookkeeping.
117
+ *
118
+ * A `duplicate` is deliberately NOT a target verdict for a scan: a duplicate
119
+ * is a CANDIDATE problem (skip it and try the next), which is what the scan
120
+ * already did. Returning it here is the bug that made a scheduled slot report
121
+ * success after submitting nothing. It remains a verdict only for the
122
+ * single-work RECOVERY path, whose cell identity is fixed.
123
+ */
82
124
  summarize() {
83
- return (this.outcomes.find((o) => o.kind === 'submitted') ??
84
- this.outcomes.find((o) => o.kind === 'stored') ??
85
- this.outcomes.find((o) => o.kind === 'delivery_pending') ??
86
- this.outcomes.find((o) => o.kind === 'duplicate') ??
87
- this.outcomes.find((o) => o.kind === 'failed') ?? {
125
+ const scan = this.scan ?? undefined;
126
+ const submitted = this.outcomes.find((o) => o.kind === 'submitted');
127
+ if (submitted)
128
+ return scan ? { ...submitted, scan } : submitted;
129
+ const stored = this.outcomes.find((o) => o.kind === 'stored');
130
+ if (stored)
131
+ return scan ? { ...stored, scan } : stored;
132
+ const pending = this.outcomes.find((o) => o.kind === 'delivery_pending');
133
+ if (pending)
134
+ return scan ? { ...pending, scan } : pending;
135
+ const duplicate = this.outcomes.find((o) => o.kind === 'duplicate');
136
+ if (duplicate)
137
+ return scan ? { ...duplicate, scan } : duplicate;
138
+ const failed = this.outcomes.find((o) => o.kind === 'failed');
139
+ if (failed)
140
+ return scan ? { ...failed, scan } : failed;
141
+ if (scan && scan.outages.length > 0) {
142
+ return {
143
+ kind: 'failed',
144
+ retryable: true,
145
+ error: `job-level outage while scanning candidates: ${scan.outages.join(', ')}`,
146
+ scan,
147
+ };
148
+ }
149
+ if (scan && (0, TargetOutcome_1.hasTransientFailure)(scan)) {
150
+ return {
151
+ kind: 'failed',
152
+ retryable: true,
153
+ error: `candidate scan hit transient infrastructure failures ` +
154
+ `(${scan.skipped.filter((s) => s.retryable).length} of ${scan.attempted} attempted)`,
155
+ scan,
156
+ };
157
+ }
158
+ if (scan && (scan.attempted > 0 || scan.skipped.length > 0)) {
159
+ return { kind: 'no_candidate', reason: (0, TargetOutcome_1.noEligibleCandidateText)(scan), scan };
160
+ }
161
+ return {
88
162
  kind: 'no_candidate',
89
163
  reason: 'no matching novel after filtering/dedupe',
90
- });
164
+ ...(scan ? { scan } : {}),
165
+ };
91
166
  }
92
167
  classifyError(error, displayTag, mode) {
93
168
  const message = error instanceof Error ? error.message : String(error);
169
+ const scan = this.scan ?? undefined;
170
+ // A hard job-level outage is named as such and is never recorded as a
171
+ // no-candidate business outcome, whatever its message happens to look like.
172
+ const outage = (0, TargetOutcome_1.classifyJobLevelOutage)(error);
94
173
  this.database.logExecution(displayTag, 'novel', 'failed', message);
95
- logger_1.logger.error(`Novel ${mode === 'ranking' ? 'ranking' : 'tag'} ${displayTag} failed`, { error: message });
174
+ logger_1.logger.error(`Novel ${mode === 'ranking' ? 'ranking' : 'tag'} ${displayTag} failed`, {
175
+ error: message,
176
+ ...(outage ? { jobLevelOutage: outage } : {}),
177
+ });
178
+ if (outage) {
179
+ return {
180
+ kind: 'failed',
181
+ retryable: true,
182
+ error: `job-level outage (${outage}): ${message}`,
183
+ ...(scan ? { scan } : {}),
184
+ };
185
+ }
96
186
  if (/no matching|all .*filtered|no_candidate|language filter/i.test(message)) {
97
- return { kind: 'no_candidate', reason: message };
187
+ return { kind: 'no_candidate', reason: message, ...(scan ? { scan } : {}) };
98
188
  }
99
189
  const retryable = (0, errors_1.isRetryableNetworkError)(error) ||
100
190
  (error instanceof Error && /timeout|econn|enotfound|etimed|429|5\d\d/i.test(error.message));
101
- return { kind: 'failed', retryable, error: message };
191
+ return { kind: 'failed', retryable, error: message, ...(scan ? { scan } : {}) };
102
192
  }
103
193
  async fetchNovels(target, mode) {
104
194
  if (mode === 'topic') {
@@ -137,6 +227,7 @@ class NovelTargetHandler {
137
227
  skipped: 0,
138
228
  alreadyDownloaded: 0,
139
229
  filteredOut: 0,
230
+ scan: { bound: 0, attempted: 0, skipped: [], outages: [] },
140
231
  };
141
232
  let totalFound = 0;
142
233
  for (let offset = 0; offset <= additionalDays && aggregate.downloaded < requested; offset++) {
@@ -157,11 +248,20 @@ class NovelTargetHandler {
157
248
  const novels = await this.fetchTopicNovels(attemptTarget);
158
249
  totalFound += novels.length;
159
250
  const result = await this.pipeline.run(novels, attemptTarget, 'novel', (novel, tag) => this.downloadAndDeliver(novel, tag, attemptTarget));
251
+ // The lookback loop is ONE bounded scan: accumulate every day's
252
+ // skips/outages so the verdict covers all candidates attempted.
253
+ aggregate.scan = (0, TargetOutcome_1.mergeScanSummaries)(aggregate.scan, result.scan);
160
254
  aggregate.downloaded += result.downloaded;
161
255
  aggregate.skipped += result.skipped;
162
256
  aggregate.alreadyDownloaded += result.alreadyDownloaded;
163
257
  aggregate.filteredOut += result.filteredOut;
258
+ if (aggregate.scan.outages.length > 0) {
259
+ // A dead token / dead database / dead network is not "no matching
260
+ // novel": stop looking back and let the job fail/retry.
261
+ break;
262
+ }
164
263
  }
264
+ this.scan = aggregate.scan;
165
265
  await this.handleDownloadResult(aggregate, target, 'topic', totalFound, checkedDays);
166
266
  }
167
267
  resolveTopicDay(target) {
@@ -191,12 +291,14 @@ class NovelTargetHandler {
191
291
  endDate: rankingDate,
192
292
  limit: Math.max(targetLimit * 20, 100),
193
293
  };
294
+ const fetchLimit = this.candidateFetchLimit(target);
194
295
  let novels = await this.client.searchNovels(searchTarget);
195
296
  logger_1.logger.info(`Found ${novels.length} novel(s) for ${rankingDate}`);
196
- this.sortByPopularityAndLog(novels, targetLimit);
197
- if (novels.length > targetLimit) {
198
- novels = novels.slice(0, targetLimit);
199
- logger_1.logger.info(`Selected top ${novels.length} novel(s) by popularity`);
297
+ this.sortByPopularityAndLog(novels, fetchLimit);
298
+ if (novels.length > fetchLimit) {
299
+ novels = novels.slice(0, fetchLimit);
300
+ logger_1.logger.info(`Selected top ${novels.length} novel(s) by popularity ` +
301
+ `(to fill ${targetLimit}, candidate scan bound ${fetchLimit})`);
200
302
  }
201
303
  return novels;
202
304
  }
@@ -207,7 +309,9 @@ class NovelTargetHandler {
207
309
  rankingDate = (0, pixiv_date_utils_1.getYesterdayDate)();
208
310
  }
209
311
  logger_1.logger.info(`Fetching ranking novels (mode: ${rankingMode}, date: ${rankingDate})`);
210
- const novels = await this.rankingService.getRankingNovelsWithFallback(rankingMode, rankingDate, target.limit);
312
+ // Ask for the whole bounded scan window, not `limit`: the planner then
313
+ // narrows it to the candidates this run may attempt.
314
+ const novels = await this.rankingService.getRankingNovelsWithFallback(rankingMode, rankingDate, this.candidateFetchLimit(target));
211
315
  logger_1.logger.info(`Ranking API returned ${novels.length} novel(s)`);
212
316
  return novels;
213
317
  }
@@ -236,10 +340,19 @@ class NovelTargetHandler {
236
340
  const { downloaded, skipped, alreadyDownloaded, filteredOut } = result;
237
341
  const targetLimit = target.limit || 10;
238
342
  const tagForLog = (0, target_label_1.getTargetLabel)(target);
239
- if (downloaded === 0 && targetLimit > 0) {
343
+ // The bounded scan produced its own explicit verdict when it attempted
344
+ // candidates or pre-filtered some, so the legacy "zero downloads" reporter
345
+ // is only the fallback for a scan that had nothing at all to look at.
346
+ const scanOwnsVerdict = result.scan.attempted > 0 || result.scan.skipped.length > 0;
347
+ if (downloaded === 0 && targetLimit > 0 && !scanOwnsVerdict) {
240
348
  await this.handleZeroDownloads(alreadyDownloaded, skipped, filteredOut, targetLimit, tagForLog, mode, target, totalFound, checkedDays, result.skipDetails);
241
349
  return;
242
350
  }
351
+ if (scanOwnsVerdict && downloaded > 0) {
352
+ logger_1.logger.info(`Candidate scan verdict for ${tagForLog}: submitted after skipping ` +
353
+ `${result.scan.skipped.length} candidate(s) of ${result.scan.attempted} attempted ` +
354
+ `(bound ${result.scan.bound})`);
355
+ }
243
356
  if (downloaded > 0 && downloaded < targetLimit * 0.5 && skipped > 0) {
244
357
  logger_1.logger.warn(`Only downloaded ${downloaded} out of ${targetLimit} requested novel(s). ${skipped} novel(s) were skipped due to 404 errors or other issues.`);
245
358
  }
@@ -375,6 +488,7 @@ class NovelTargetHandler {
375
488
  return;
376
489
  }
377
490
  const result = await this.pipeline.run(novels, target, 'novel', (novel, tag) => this.downloadAndDeliver(novel, tag, target));
491
+ this.scan = result.scan;
378
492
  this.handleDownloadResult(result, target, 'user', novels.length);
379
493
  }
380
494
  catch (error) {
@@ -460,7 +574,15 @@ class NovelTargetHandler {
460
574
  user: detail.user,
461
575
  create_date: detail.create_date,
462
576
  };
463
- await this.downloadAndDeliver(novel, `novel-${novelId}`, target);
577
+ const attempt = await this.downloadAndDeliver(novel, `novel-${novelId}`, target);
578
+ if (attempt.kind === 'skipped') {
579
+ // A cell that already owns a work cannot advance to another candidate —
580
+ // its identity is fixed. An already-delivered locked work is therefore a
581
+ // terminal business duplicate for THIS cell (it published nothing new).
582
+ this.outcomes.push(attempt.skip.code === 'duplicate'
583
+ ? { kind: 'duplicate', workId: lockedWorkId, reason: attempt.skip.reason }
584
+ : { kind: 'failed', retryable: false, error: `LOCKED_WORK_UNAVAILABLE: ${attempt.skip.reason}` });
585
+ }
464
586
  }
465
587
  catch (error) {
466
588
  const message = error instanceof Error ? error.message : String(error);
@@ -478,6 +600,11 @@ class NovelTargetHandler {
478
600
  /**
479
601
  * Process ONE candidate work for this cell.
480
602
  *
603
+ * Returns what happened to THIS candidate, so the pipeline can advance to the
604
+ * next one when it was unusable. Throwing is reserved for JOB-level failures
605
+ * (dead database / dead token / dead delivery provider): a candidate-level
606
+ * failure is reported as a `skipped` attempt and never as a job verdict.
607
+ *
481
608
  * The cell is bound to `novel` BEFORE any side effect: it is the binding, not
482
609
  * the candidate list, that decides what a later recovery resumes. The binding
483
610
  * is only rolled back when the attempt produced no artifact at all, so in-run
@@ -498,7 +625,14 @@ class NovelTargetHandler {
498
625
  targetId: execution.targetId,
499
626
  boundWorkId: binding.workId,
500
627
  });
501
- return;
628
+ return {
629
+ kind: 'skipped',
630
+ skip: {
631
+ code: 'duplicate',
632
+ workId,
633
+ reason: `cell already owns work ${binding.workId} (concurrent selection)`,
634
+ },
635
+ };
502
636
  }
503
637
  }
504
638
  let artifact;
@@ -509,33 +643,83 @@ class NovelTargetHandler {
509
643
  // Nothing was persisted, so the cell may still pick another candidate.
510
644
  if (execution && !recovering)
511
645
  execution.release(workId);
512
- throw error;
646
+ const failure = (0, TargetOutcome_1.classifyCandidateFailure)(error, workId);
647
+ if (failure.scope === 'job' || !(0, TargetOutcome_1.skipCandidateWithoutRetry)(failure.skip)) {
648
+ // Job-level outage, or a transient candidate failure: re-thrown so the
649
+ // queue's existing retry/backoff owns it and the scheduler records a JOB
650
+ // failure — never an exhausted candidate list.
651
+ throw error;
652
+ }
653
+ this.logError(error, `Candidate novel ${workId} skipped (${failure.skip.code})`);
654
+ return { kind: 'skipped', skip: failure.skip };
513
655
  }
514
656
  if (!artifact) {
515
657
  if (execution && !recovering)
516
658
  execution.release(workId);
517
- return;
659
+ return {
660
+ kind: 'skipped',
661
+ skip: { code: 'filtered', workId, reason: 'downloader declined this candidate (no artifact)' },
662
+ };
518
663
  }
519
- // Committed: the artifact is durable and this work now defines the cell. Even
520
- // if the delivery below throws, recovery must resume THIS work never release.
664
+ // The artifact is durable, but this cell has NOT committed to it until the
665
+ // delivery below actually claims it. A candidate that turns out to be a
666
+ // duplicate must give the binding BACK, or `bind()` would refuse the next
667
+ // candidate and the scan could never advance — the whole point of this
668
+ // change. `releaseCellWork` itself refuses once the cell moved on
669
+ // (delivery_pending/submitted), so a committed identity stays stable.
670
+ const attempt = this.recordArtifactOutcome(artifact, target);
671
+ if (attempt.kind === 'skipped' && execution && !recovering) {
672
+ execution.release(workId);
673
+ }
674
+ return attempt;
675
+ }
676
+ /**
677
+ * Turn a downloaded novel artifact into the target's business outcome.
678
+ *
679
+ * An ALREADY-DELIVERED work is returned as a candidate SKIP, never as a
680
+ * target outcome: the run must try the next candidate instead of ending the
681
+ * slot as a `duplicate`. The delivery idempotency ledger is what makes the
682
+ * second concurrent worker lose this race instead of double-submitting.
683
+ */
684
+ recordArtifactOutcome(artifact, target) {
521
685
  const isDelivery = target.storageMode === 'cache' && target.delivery?.target?.trim();
522
686
  if (!isDelivery || !this.deliveryService) {
523
687
  this.outcomes.push({ kind: 'stored', workId: artifact.pixivId, workType: artifact.type });
524
- return;
688
+ return { kind: 'selected', workId: artifact.pixivId, workType: artifact.type };
525
689
  }
526
690
  const ec = target.delivery;
527
691
  const slotId = ec?.executionContext?.slotId;
528
692
  if (this.deliveryService.isAlreadyDelivered(target.delivery.target, artifact.type, artifact.pixivId)) {
529
- this.outcomes.push({ kind: 'duplicate', workId: artifact.pixivId, reason: 'already delivered to target (ledger)' });
530
- return;
693
+ return {
694
+ kind: 'skipped',
695
+ skip: {
696
+ code: 'duplicate',
697
+ workId: artifact.pixivId,
698
+ reason: 'already delivered to target (delivery ledger)',
699
+ },
700
+ };
531
701
  }
532
702
  const res = this.deliveryService.enqueue(artifact, target, {
533
703
  slotId,
534
704
  fields: target.delivery?.fields,
535
705
  });
536
- this.outcomes.push(res.duplicate
537
- ? { kind: 'duplicate', workId: artifact.pixivId, reason: 'already delivered (ledger)' }
538
- : { kind: 'delivery_pending', workId: artifact.pixivId, workType: artifact.type, deliveryId: res.deliveryId });
706
+ if (res.duplicate) {
707
+ return {
708
+ kind: 'skipped',
709
+ skip: {
710
+ code: 'duplicate',
711
+ workId: artifact.pixivId,
712
+ reason: 'already delivered (idempotency ledger)',
713
+ },
714
+ };
715
+ }
716
+ this.outcomes.push({
717
+ kind: 'delivery_pending',
718
+ workId: artifact.pixivId,
719
+ workType: artifact.type,
720
+ deliveryId: res.deliveryId,
721
+ });
722
+ return { kind: 'selected', workId: artifact.pixivId, workType: artifact.type };
539
723
  }
540
724
  }
541
725
  exports.NovelTargetHandler = NovelTargetHandler;
@@ -4,10 +4,18 @@ import { DownloadPlanner } from '../plan/DownloadPlanner';
4
4
  import { DownloadExecutor } from '../exec/DownloadExecutor';
5
5
  import { ProgressReporter } from '../report/ProgressReporter';
6
6
  import { ErrorRecoveryStrategy } from '../recovery/ErrorRecovery';
7
+ import { CandidateAttempt, CandidateScanSummary } from '../../scheduler/TargetOutcome';
7
8
  type DownloadItem = PixivIllust | PixivNovel;
8
9
  type ItemType = 'illustration' | 'novel';
9
10
  export interface DownloadPipelineResult {
11
+ /**
12
+ * Candidates that PRODUCED a business result for this target. A candidate
13
+ * that was skipped (duplicate, deleted, private, ...) does not count: that is
14
+ * precisely the accounting error that let a duplicate end a scheduled slot as
15
+ * a completed run.
16
+ */
10
17
  downloaded: number;
18
+ /** Candidates the executor skipped after an error (legacy error counter). */
11
19
  skipped: number;
12
20
  alreadyDownloaded: number;
13
21
  filteredOut: number;
@@ -16,6 +24,11 @@ export interface DownloadPipelineResult {
16
24
  id: string;
17
25
  error: string;
18
26
  }[];
27
+ /**
28
+ * Bounded candidate-scan bookkeeping. Always present, so the caller can state
29
+ * an explicit terminal outcome instead of "completed".
30
+ */
31
+ scan: CandidateScanSummary;
19
32
  }
20
33
  export interface DownloadPipelineDependencies {
21
34
  config: StandaloneConfig;
@@ -29,6 +42,14 @@ export interface DownloadPipelineDependencies {
29
42
  */
30
43
  isCancelled?: () => boolean;
31
44
  }
45
+ /**
46
+ * What a candidate hook must report back: did this candidate produce the
47
+ * target's business result, or was it unusable?
48
+ *
49
+ * `void` is accepted and treated as `selected`, so a hook with no
50
+ * candidate-level information keeps the historical meaning of "it resolved".
51
+ */
52
+ export type CandidateDownloadFn<T extends DownloadItem> = (item: T, tag: string) => Promise<CandidateAttempt | void>;
32
53
  export declare class DownloadPipeline {
33
54
  private readonly config;
34
55
  private readonly planner;
@@ -37,9 +58,7 @@ export declare class DownloadPipeline {
37
58
  private readonly recovery;
38
59
  private readonly isRunCancelled;
39
60
  constructor(deps: DownloadPipelineDependencies);
40
- run<T extends DownloadItem>(items: T[], target: TargetConfig, itemType: ItemType, downloadFn: (item: T, tag: string) => Promise<void>): Promise<DownloadPipelineResult>;
41
- private executeRandomMode;
42
- private executeSequentialMode;
61
+ run<T extends DownloadItem>(items: T[], target: TargetConfig, itemType: ItemType, downloadFn: CandidateDownloadFn<T>): Promise<DownloadPipelineResult>;
43
62
  private updateProgress;
44
63
  private logRecoveryDecision;
45
64
  }