@antora/content-aggregator 3.2.0-alpha.1 → 3.2.0-alpha.3

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.
@@ -5,7 +5,6 @@ const { createHash } = require('crypto')
5
5
  const createGitHttpPlugin = require('./git-plugin-http')
6
6
  const decodeUint8Array = require('./decode-uint8-array')
7
7
  const deepClone = require('./deep-clone')
8
- const deepFlatten = require('./deep-flatten')
9
8
  const EventEmitter = require('events')
10
9
  const expandPath = require('@antora/expand-path-helper')
11
10
  const File = require('./file')
@@ -93,83 +92,71 @@ function aggregateContent (playbook) {
93
92
  return ensureCacheDir(requestedCacheDir, startDir).then((cacheDir) => {
94
93
  const gitConfig = Object.assign({ ensureGitSuffix: true }, playbook.git)
95
94
  const gitPlugins = loadGitPlugins(gitConfig, playbook.network || {}, startDir)
96
- const fetchConcurrency = Math.max(gitConfig.fetchConcurrency || Infinity, 1)
95
+ const concurrency = {
96
+ fetch: Math.max(gitConfig.fetchConcurrency || Infinity, 1),
97
+ read: Math.max(gitConfig.readConcurrency || Infinity, 1),
98
+ }
97
99
  const sourcesByUrl = sources.reduce((accum, source) => {
98
100
  return accum.set(source.url, [...(accum.get(source.url) || []), Object.assign({}, sourceDefaults, source)])
99
101
  }, new Map())
100
102
  const progress = !quiet && createProgress(sourcesByUrl.keys(), process.stdout)
101
103
  const refPatternCache = Object.assign(new Map(), { braces: new Map() })
102
104
  const loadOpts = { cacheDir, fetch, gitPlugins, progress, startDir, refPatternCache }
103
- return collectFiles(sourcesByUrl, loadOpts, fetchConcurrency).then(buildAggregate, (err) => {
105
+ return collectFiles(sourcesByUrl, loadOpts, concurrency).then(buildAggregate, (err) => {
104
106
  progress && progress.terminate()
105
107
  throw err
106
108
  })
107
109
  })
108
110
  }
109
111
 
110
- async function collectFiles (sourcesByUrl, loadOpts, concurrency) {
111
- const tasks = [...sourcesByUrl.entries()].map(([url, sources]) => [
112
- () => loadRepository(url, Object.assign({ fetchTags: tagsSpecified(sources) }, loadOpts)),
113
- ({ repo, authStatus }) =>
114
- Promise.all(
115
- sources.map((source) => {
116
- // NOTE if repository is managed (has a url property), we can assume the remote name is origin
117
- // TODO if the repo has no remotes, then remoteName should be undefined
118
- const remoteName = repo.url ? 'origin' : source.remote || 'origin'
119
- return collectFilesFromSource(source, repo, remoteName, authStatus)
120
- })
121
- ),
122
- ])
123
- let rejection, started
124
- const startedContinuations = []
125
- const recordRejection = (err) => {
126
- rejection = err
127
- }
128
- const runTask = (primary, continuation, idx) =>
129
- primary().then((value) => {
130
- if (!rejection) startedContinuations[idx] = continuation(value).catch(recordRejection)
131
- }, recordRejection)
132
- if (tasks.length > concurrency) {
133
- started = []
134
- const pending = []
135
- for (const [primary, continuation] of tasks) {
136
- const current = runTask(primary, continuation, started.length).finally(() =>
137
- pending.splice(pending.indexOf(current), 1)
138
- )
139
- started.push(current)
140
- if (pending.push(current) < concurrency) continue
141
- await Promise.race(pending)
142
- if (rejection) break
112
+ async function collectFiles (sourcesByUrl, loadOpts, concurrency, fetchedUrls) {
113
+ const loadTasks = [...sourcesByUrl.entries()].map(([url, sources]) => {
114
+ const loadOptsForUrl = Object.assign({}, loadOpts)
115
+ if (loadOpts.fetch && fetchedUrls && fetchedUrls.length && fetchedUrls.includes(url)) loadOptsForUrl.fetch = false
116
+ if (tagsSpecified(sources)) loadOptsForUrl.fetchTags = true
117
+ return loadRepository.bind(null, url, loadOptsForUrl, { url, sources })
118
+ })
119
+ return gracefulPromiseAllWithLimit(loadTasks, concurrency.fetch).then(([results, rejections]) => {
120
+ if (rejections.length) {
121
+ if (concurrency.fetch > 1 && rejections.every(({ recoverable }) => recoverable)) {
122
+ if (loadOpts.progress) loadOpts.progress.terminate() // reset cursor position and allow it be reused
123
+ const msg0 = 'An unexpected error occurred while concurrently fetching content sources.'
124
+ const msg1 = 'Retrying with git.fetch_concurrency value of 1.'
125
+ logger.warn(msg0 + ' ' + msg1)
126
+ const fulfilledUrls = results.map((it) => it && it.repo.url && it.url).filter((it) => it)
127
+ return collectFiles(sourcesByUrl, loadOpts, Object.assign(concurrency, { fetch: 1 }), fulfilledUrls)
128
+ }
129
+ throw rejections[0]
143
130
  }
144
- } else {
145
- started = tasks.map(([primary, continuation], idx) => runTask(primary, continuation, idx))
146
- }
147
- return Promise.all(started).then(() =>
148
- Promise.all(startedContinuations).then((result) => {
149
- if (rejection) throw rejection
150
- return result
151
- })
152
- )
131
+ return Promise.all(
132
+ results.map(({ repo, authStatus, sources }) =>
133
+ selectStartPathsForRepository(repo, authStatus, sources).then((startPaths) =>
134
+ collectFilesFromStartPaths.bind(null, startPaths, repo, authStatus)
135
+ )
136
+ )
137
+ ).then((collectTasks) => promiseAllWithLimit(collectTasks, concurrency.read))
138
+ })
153
139
  }
154
140
 
155
141
  function buildAggregate (componentVersionBuckets) {
156
- return [
157
- ...deepFlatten(componentVersionBuckets)
158
- .reduce((accum, batch) => {
159
- const key = batch.version + '@' + batch.name
160
- const entry = accum.get(key)
161
- if (!entry) return accum.set(key, batch)
142
+ const entries = Object.assign(new Map(), { accum: [] })
143
+ for (const batchesForOrigin of componentVersionBuckets) {
144
+ for (const batch of batchesForOrigin) {
145
+ let key, entry
146
+ if ((entry = entries.get((key = batch.version + '@' + batch.name)))) {
162
147
  const { files, origins } = batch
163
148
  ;(batch.files = entry.files).push(...files)
164
149
  ;(batch.origins = entry.origins).push(origins[0])
165
150
  Object.assign(entry, batch)
166
- return accum
167
- }, new Map())
168
- .values(),
169
- ]
151
+ } else {
152
+ entries.set(key, batch).accum.push(batch)
153
+ }
154
+ }
155
+ }
156
+ return entries.accum
170
157
  }
171
158
 
172
- async function loadRepository (url, opts) {
159
+ async function loadRepository (url, opts, result = {}) {
173
160
  let authStatus, dir, repo
174
161
  const cache = { [REF_PATTERN_CACHE_KEY]: opts.refPatternCache }
175
162
  if (~url.indexOf(':') && GIT_URI_DETECTOR_RX.test(url)) {
@@ -179,6 +166,7 @@ async function loadRepository (url, opts) {
179
166
  dir = ospath.join(cacheDir, generateCloneFolderName(displayUrl))
180
167
  // NOTE the presence of the url property on the repo object implies the repository is remote
181
168
  repo = { cache, dir, fs, gitdir: dir, noCheckout: true, url }
169
+ const { credentialManager } = gitPlugins
182
170
  const validStateFile = ospath.join(dir, VALID_STATE_FILENAME)
183
171
  try {
184
172
  await fsp.access(validStateFile)
@@ -188,7 +176,7 @@ async function loadRepository (url, opts) {
188
176
  await git
189
177
  .fetch(fetchOpts)
190
178
  .then(() => {
191
- authStatus = identifyAuthStatus(gitPlugins.credentialManager, credentials, url)
179
+ authStatus = identifyAuthStatus(credentialManager, credentials, url)
192
180
  return git.setConfig(Object.assign({ path: 'remote.origin.private', value: authStatus }, repo))
193
181
  })
194
182
  .catch((fetchErr) => {
@@ -209,13 +197,13 @@ async function loadRepository (url, opts) {
209
197
  .clone(fetchOpts)
210
198
  .then(() => git.resolveRef(Object.assign({ ref: 'HEAD', depth: 1 }, repo)))
211
199
  .then(() => {
212
- authStatus = identifyAuthStatus(gitPlugins.credentialManager, credentials, url)
200
+ authStatus = identifyAuthStatus(credentialManager, credentials, url)
213
201
  return git.setConfig(Object.assign({ path: 'remote.origin.private', value: authStatus }, repo))
214
202
  })
215
203
  .catch((cloneErr) => {
216
- // FIXME triggering the error handler here causes assertion problems in the test suite
217
- //if (fetchOpts.onProgress) fetchOpts.onProgress.finish(cloneErr)
218
- throw transformGitCloneError(cloneErr, displayUrl)
204
+ if (fetchOpts.onProgress) fetchOpts.onProgress.finish(cloneErr)
205
+ const authRequested = credentialManager.status({ url }) === 'requested'
206
+ throw transformGitCloneError(cloneErr, displayUrl, authRequested)
219
207
  })
220
208
  .then(() => fsp.writeFile(validStateFile, '').catch(invariably.void))
221
209
  .then(() => fetchOpts.onProgress && fetchOpts.onProgress.finish())
@@ -239,7 +227,7 @@ async function loadRepository (url, opts) {
239
227
  } else {
240
228
  throw new Error(`Local content source does not exist: ${dir}${url !== dir ? ' (url: ' + url + ')' : ''}`)
241
229
  }
242
- return { repo, authStatus }
230
+ return Object.assign(result, { repo, authStatus })
243
231
  }
244
232
 
245
233
  function extractCredentials (url) {
@@ -261,27 +249,42 @@ function extractCredentials (url) {
261
249
  }
262
250
  }
263
251
 
264
- async function collectFilesFromSource (source, repo, remoteName, authStatus) {
265
- const originUrl = repo.url || (await resolveRemoteUrl(repo, remoteName))
266
- return selectReferences(source, repo, remoteName).then((refs) => {
267
- if (!refs.length) {
268
- const { url, branches, tags, startPath, startPaths } = source
252
+ async function selectStartPathsForRepository (repo, authStatus, sources) {
253
+ const startPaths = []
254
+ const originUrls = {}
255
+ for (const source of sources) {
256
+ const { version, editUrl } = source
257
+ // NOTE if repository is managed (has a url property), we can assume the remote name is origin
258
+ // TODO if the repo has no remotes, then remoteName should be undefined
259
+ const remoteName = repo.url ? 'origin' : source.remote || 'origin'
260
+ const originUrl = repo.url || (originUrls[remoteName] ||= await resolveRemoteUrl(repo, remoteName))
261
+ const refs = await selectReferences(source, repo, remoteName)
262
+ if (refs.length) {
263
+ for (const ref of refs) {
264
+ for (const startPath of await selectStartPaths(source, repo, remoteName, ref)) {
265
+ startPaths.push({ startPath, ref, originUrl, editUrl, version })
266
+ }
267
+ }
268
+ } else {
269
+ const { url, branches, tags } = source
269
270
  const startPathInfo =
270
- 'startPaths' in source ? { 'start paths': startPaths || undefined } : { 'start path': startPath || undefined }
271
+ 'startPaths' in source
272
+ ? { 'start paths': source.startPaths || undefined }
273
+ : { 'start path': source.startPath || undefined }
271
274
  const sourceInfo = yaml.dump({ url, branches, tags, ...startPathInfo }, { flowLevel: 1 }).trimRight()
272
275
  logger.info(`No matching references found for content source entry (${sourceInfo.replace(NEWLINE_RX, ' | ')})`)
273
- return []
274
276
  }
275
- return Promise.all(refs.map((it) => collectFilesFromReference(source, repo, remoteName, authStatus, it, originUrl)))
276
- })
277
+ }
278
+ return startPaths
277
279
  }
278
280
 
279
281
  // QUESTION should we resolve HEAD to a ref eagerly to avoid having to do a match on it?
280
282
  async function selectReferences (source, repo, remote) {
281
283
  let { branches: branchPatterns, tags: tagPatterns, worktrees: worktreePatterns } = source
282
- const isBare = repo.noCheckout
284
+ const managed = 'url' in repo
285
+ const isBare = managed || repo.noCheckout
283
286
  const patternCache = repo.cache[REF_PATTERN_CACHE_KEY]
284
- const noWorktree = repo.url ? undefined : false
287
+ const noWorktree = managed ? undefined : false
285
288
  const refs = new Map()
286
289
  if (
287
290
  tagPatterns &&
@@ -297,11 +300,14 @@ async function selectReferences (source, repo, remote) {
297
300
  }
298
301
  }
299
302
  }
300
- if (!branchPatterns) return [...refs.values()]
301
- branchPatterns = Array.isArray(branchPatterns)
302
- ? branchPatterns.map((pattern) => String(pattern))
303
- : splitRefPatterns(String(branchPatterns))
304
- if (!branchPatterns.length) return [...refs.values()]
303
+ if (
304
+ !branchPatterns ||
305
+ !(branchPatterns = Array.isArray(branchPatterns)
306
+ ? branchPatterns.map((pattern) => String(pattern))
307
+ : splitRefPatterns(String(branchPatterns))).length
308
+ ) {
309
+ return [...refs.values()]
310
+ }
305
311
  const worktreeName = repo.worktreeName // possibly switch to worktree property ({ name, dir}) in future
306
312
  if (worktreeName) branchPatterns = branchPatterns.map((it) => (it === 'HEAD' ? 'HEAD@' + worktreeName : it))
307
313
  if (worktreePatterns) {
@@ -386,10 +392,11 @@ async function selectReferences (source, repo, remote) {
386
392
  refs.set(shortname, { shortname, fullname: 'heads/' + shortname, type: 'branch', head })
387
393
  }
388
394
  }
389
- } else if (!remoteBranches.length) {
395
+ } else if (!managed || !remoteBranches.length) {
390
396
  const localBranches = await git.listBranches(repo)
391
397
  if (localBranches.length) {
392
398
  for (const shortname of filterRefs(localBranches, branchPatterns, patternCache)) {
399
+ if (refs.has(shortname)) continue // NOTE prefer remote branches in bare repository
393
400
  refs.set(shortname, { shortname, fullname: 'heads/' + shortname, type: 'branch', head: noWorktree })
394
401
  }
395
402
  }
@@ -410,10 +417,9 @@ function getCurrentBranchName (repo, remote) {
410
417
  ).then((ref) => (ref.startsWith('refs/') ? ref.replace(SHORTEN_REF_RX, '') : undefined))
411
418
  }
412
419
 
413
- async function collectFilesFromReference (source, repo, remoteName, authStatus, ref, originUrl) {
420
+ async function selectStartPaths (source, repo, remoteName, ref) {
414
421
  const url = repo.url
415
422
  const displayUrl = url || repo.dir
416
- const { version, editUrl } = source
417
423
  const worktreePath = ref.head
418
424
  if (!worktreePath) {
419
425
  ref.oid = await git.resolveRef(
@@ -433,17 +439,22 @@ async function collectFilesFromReference (source, repo, remoteName, authStatus,
433
439
  const flag = worktreePath ? ' <worktree>' : ref.remote && worktreePath === false ? ` <remotes/${ref.remote}>` : ''
434
440
  throw new Error(`no start paths found in ${where} (${ref.type}: ${ref.shortname}${flag})`)
435
441
  }
436
- return Promise.all(
437
- startPaths.map((startPath) =>
438
- collectFilesFromStartPath(startPath, repo, authStatus, ref, worktreePath, originUrl, editUrl, version)
439
- )
440
- )
442
+ return startPaths
443
+ }
444
+ return [cleanStartPath(coerceToString(source.startPath))]
445
+ }
446
+
447
+ async function collectFilesFromStartPaths (startPaths, repo, authStatus) {
448
+ const buckets = []
449
+ for (const { startPath, ref, originUrl, editUrl, version } of startPaths) {
450
+ buckets.push(await collectFilesFromStartPath(startPath, repo, authStatus, ref, originUrl, editUrl, version))
441
451
  }
442
- const startPath = cleanStartPath(coerceToString(source.startPath))
443
- return collectFilesFromStartPath(startPath, repo, authStatus, ref, worktreePath, originUrl, editUrl, version)
452
+ repo.cache = undefined
453
+ return buckets
444
454
  }
445
455
 
446
- function collectFilesFromStartPath (startPath, repo, authStatus, ref, worktreePath, originUrl, editUrl, version) {
456
+ function collectFilesFromStartPath (startPath, repo, authStatus, ref, originUrl, editUrl, version) {
457
+ const worktreePath = ref.head
447
458
  const origin = computeOrigin(originUrl, authStatus, repo.gitdir, ref, startPath, worktreePath, editUrl)
448
459
  return (worktreePath ? readFilesFromWorktree(origin) : readFilesFromGitTree(repo, ref.oid, startPath))
449
460
  .then((files) =>
@@ -523,26 +534,25 @@ function srcFs (cwd, origin) {
523
534
  }
524
535
 
525
536
  function readFilesFromGitTree (repo, oid, startPath) {
526
- return git
527
- .readTree(Object.assign({ oid }, repo))
528
- .then((root) =>
529
- getGitTreeAtStartPath(repo, oid, startPath).then((start) =>
530
- srcGitTree(repo, Object.assign(root, { dirname: '' }), start)
531
- )
532
- )
537
+ return git.readTree(Object.assign({ oid }, repo)).then((root) => {
538
+ Object.assign(root, { dirname: '' })
539
+ return startPath
540
+ ? getGitTreeAtStartPath(repo, oid, startPath).then((start) => {
541
+ Object.assign(start, { dirname: startPath })
542
+ return srcGitTree(repo, root, start)
543
+ })
544
+ : srcGitTree(repo, root)
545
+ })
533
546
  }
534
547
 
535
548
  function getGitTreeAtStartPath (repo, oid, startPath) {
536
- return git.readTree(Object.assign({ oid, filepath: startPath }, repo)).then(
537
- (result) => Object.assign(result, { dirname: startPath }),
538
- (err) => {
539
- const m = err instanceof ObjectTypeError && err.data.expected === 'tree' ? 'is not a directory' : 'does not exist'
540
- throw new Error(`the start path '${startPath}' ${m}`)
541
- }
542
- )
549
+ return git.readTree(Object.assign({ oid, filepath: startPath }, repo)).catch((err) => {
550
+ const m = err instanceof ObjectTypeError && err.data.expected === 'tree' ? 'is not a directory' : 'does not exist'
551
+ throw new Error(`the start path '${startPath}' ${m}`)
552
+ })
543
553
  }
544
554
 
545
- function srcGitTree (repo, root, start) {
555
+ function srcGitTree (repo, root, start = root) {
546
556
  return new Promise((resolve, reject) => {
547
557
  const files = []
548
558
  createGitTreeWalker(repo, root, filterGitEntry, gitEntryToFile)
@@ -829,7 +839,6 @@ function onGitProgress ({ phase, loaded, total }) {
829
839
 
830
840
  function onGitComplete (err) {
831
841
  if (err) {
832
- // TODO could use progressBar.interrupt() to replace bar with message instead
833
842
  this.chars.incomplete = '?'
834
843
  this.update(0)
835
844
  // NOTE force progress bar to update regardless of throttle setting
@@ -979,8 +988,8 @@ function ensureCacheDir (preferredCacheDir, startDir) {
979
988
  )
980
989
  }
981
990
 
982
- function transformGitCloneError (err, displayUrl) {
983
- let wrappedMsg, trimMessage
991
+ function transformGitCloneError (err, displayUrl, authRequested) {
992
+ let wrappedMsg, recoverable, trimMessage
984
993
  if (HTTP_ERROR_CODE_RX.test(err.code)) {
985
994
  switch (err.data.statusCode) {
986
995
  case 401:
@@ -989,11 +998,13 @@ function transformGitCloneError (err, displayUrl) {
989
998
  : 'Content repository not found or requires credentials'
990
999
  break
991
1000
  case 404:
992
- wrappedMsg = 'Content repository not found'
1001
+ wrappedMsg = authRequested
1002
+ ? 'Content repository not found or credentials were rejected'
1003
+ : 'Content repository not found'
993
1004
  break
994
1005
  default:
995
1006
  wrappedMsg = err.message
996
- trimMessage = true
1007
+ recoverable = trimMessage = true
997
1008
  }
998
1009
  } else if (err instanceof UrlParseError || err instanceof UnknownTransportError) {
999
1010
  wrappedMsg = 'Content source uses an unsupported transport protocol'
@@ -1001,14 +1012,14 @@ function transformGitCloneError (err, displayUrl) {
1001
1012
  wrappedMsg = `Content repository host could not be resolved: ${err.hostname}`
1002
1013
  } else {
1003
1014
  wrappedMsg = `${err.name}: ${err.message}`
1004
- trimMessage = true
1015
+ recoverable = trimMessage = true
1005
1016
  }
1006
1017
  if (trimMessage) {
1007
- wrappedMsg = ~(wrappedMsg = wrappedMsg.trimRight()).indexOf('. ') ? wrappedMsg : wrappedMsg.replace(/\.$/, '')
1018
+ wrappedMsg = ~(wrappedMsg = wrappedMsg.trimEnd()).indexOf('. ') ? wrappedMsg : wrappedMsg.replace(/\.$/, '')
1008
1019
  }
1009
1020
  const errWrapper = new Error(`${wrappedMsg} (url: ${displayUrl})`)
1010
1021
  errWrapper.stack += `\nCaused by: ${err.stack || 'unknown'}`
1011
- return errWrapper
1022
+ return recoverable ? Object.assign(errWrapper, { recoverable }) : errWrapper
1012
1023
  }
1013
1024
 
1014
1025
  function splitRefPatterns (str) {
@@ -1035,22 +1046,20 @@ function coerceToString (value) {
1035
1046
  return value == null ? '' : String(value)
1036
1047
  }
1037
1048
 
1038
- async function resolveRepositoryFromWorktree (repo) {
1049
+ function resolveRepositoryFromWorktree (repo) {
1039
1050
  return fsp
1040
1051
  .readFile(repo.gitdir, 'utf8')
1041
- .then((contents) => contents.trimRight().substr(8))
1042
- .then((worktreeGitdir) => {
1043
- const worktreeName = ospath.basename(worktreeGitdir)
1044
- return fsp.readFile(ospath.join(worktreeGitdir, 'commondir'), 'utf8').then(
1052
+ .then((contents) => contents.substr(8).trimEnd())
1053
+ .then((worktreeGitdir) =>
1054
+ fsp.readFile(ospath.join(worktreeGitdir, 'commondir'), 'utf8').then(
1045
1055
  (contents) => {
1046
- const gitdir = ospath.join(worktreeGitdir, contents.trimRight())
1047
- return ospath.basename(gitdir) === '.git'
1048
- ? Object.assign(repo, { dir: ospath.dirname(gitdir), gitdir, worktreeName })
1049
- : Object.assign(repo, { dir: gitdir, gitdir, worktreeName })
1056
+ const gitdir = ospath.join(worktreeGitdir, contents.trimEnd())
1057
+ const dir = ospath.basename(gitdir) === '.git' ? ospath.dirname(gitdir) : gitdir
1058
+ return Object.assign(repo, { dir, gitdir, worktreeName: ospath.basename(worktreeGitdir) })
1050
1059
  },
1051
1060
  () => repo
1052
1061
  )
1053
- })
1062
+ )
1054
1063
  }
1055
1064
 
1056
1065
  function findWorktrees (repo, patterns) {
@@ -1076,7 +1085,7 @@ function findWorktrees (repo, patterns) {
1076
1085
  .then((branch = worktreeName) =>
1077
1086
  fsp
1078
1087
  .readFile(ospath.join(gitdir, 'gitdir'), 'utf8')
1079
- .then((contents) => [branch, { head: ospath.dirname(contents.trimRight()), name: worktreeName }])
1088
+ .then((contents) => [branch, { head: ospath.dirname(contents.trimEnd()), name: worktreeName }])
1080
1089
  )
1081
1090
  })
1082
1091
  )
@@ -1085,4 +1094,37 @@ function findWorktrees (repo, patterns) {
1085
1094
  ).then((entries = []) => mainWorktree.then((entry) => new Map(entry ? entries.push(entry) && entries : entries)))
1086
1095
  }
1087
1096
 
1097
+ async function gracefulPromiseAllWithLimit (tasks, limit = Infinity) {
1098
+ const rejections = []
1099
+ const recordRejection = (err) => rejections.push(err) && undefined
1100
+ const started = []
1101
+ if (tasks.length <= limit) {
1102
+ for (const task of tasks) started.push(task().catch(recordRejection))
1103
+ } else {
1104
+ const pending = []
1105
+ for (const task of tasks) {
1106
+ const current = task()
1107
+ .catch(recordRejection)
1108
+ .finally(() => pending.splice(pending.indexOf(current), 1))
1109
+ started.push(current)
1110
+ if (pending.push(current) < limit) continue
1111
+ await Promise.race(pending)
1112
+ if (rejections.length) break
1113
+ }
1114
+ }
1115
+ return Promise.all(started).then((results) => [results, rejections])
1116
+ }
1117
+
1118
+ async function promiseAllWithLimit (tasks, limit = Infinity) {
1119
+ if (tasks.length <= limit) return Promise.all(tasks.map((task) => task()))
1120
+ const started = []
1121
+ const pending = []
1122
+ for (const task of tasks) {
1123
+ const current = task().finally(() => pending.splice(pending.indexOf(current), 1))
1124
+ started.push(current)
1125
+ if (pending.push(current) >= limit) await Promise.race(pending)
1126
+ }
1127
+ return Promise.all(started)
1128
+ }
1129
+
1088
1130
  module.exports = aggregateContent
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antora/content-aggregator",
3
- "version": "3.2.0-alpha.1",
3
+ "version": "3.2.0-alpha.3",
4
4
  "description": "Fetches and aggregates content from distributed sources for use in an Antora documentation pipeline.",
5
5
  "license": "MPL-2.0",
6
6
  "author": "OpenDevise Inc. (https://opendevise.com)",
@@ -29,12 +29,12 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@antora/expand-path-helper": "~2.0",
32
- "@antora/logger": "3.2.0-alpha.1",
32
+ "@antora/logger": "3.2.0-alpha.3",
33
33
  "@antora/user-require-helper": "~2.0",
34
34
  "braces": "~3.0",
35
35
  "cache-directory": "~2.0",
36
36
  "glob-stream": "~7.0",
37
- "hpagent": "~1.1",
37
+ "hpagent": "~1.2",
38
38
  "isomorphic-git": "~1.21",
39
39
  "js-yaml": "~4.1",
40
40
  "multi-progress": "~4.0",
@@ -62,7 +62,7 @@
62
62
  ],
63
63
  "scripts": {
64
64
  "test": "_mocha",
65
- "prepublishOnly": "node $npm_config_local_prefix/npm/prepublishOnly.js",
66
- "postpublish": "node $npm_config_local_prefix/npm/postpublish.js"
65
+ "prepublishOnly": "npx -y downdoc@latest --prepublish",
66
+ "postpublish": "npx -y downdoc@latest --postpublish"
67
67
  }
68
68
  }