-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[kbn/bazel-packages] convert to js (#139235)
- Loading branch information
Spencer
authored
Aug 23, 2022
1 parent
fe646b2
commit 6b0bd5b
Showing
18 changed files
with
509 additions
and
211 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
/** | ||
* | ||
* @template T | ||
* @template T2 | ||
* @param {(v: T) => Promise<T2>} fn | ||
* @param {T} item | ||
* @returns {Promise<PromiseSettledResult<T2>>} | ||
*/ | ||
const settle = async (fn, item) => { | ||
const [result] = await Promise.allSettled([(async () => fn(item))()]); | ||
return result; | ||
}; | ||
|
||
/** | ||
* @template T | ||
* @template T2 | ||
* @param {Array<T>} source | ||
* @param {number} limit | ||
* @param {(v: T) => Promise<T2>} mapFn | ||
* @returns {Promise<T2[]>} | ||
*/ | ||
function asyncMapWithLimit(source, limit, mapFn) { | ||
return new Promise((resolve, reject) => { | ||
if (limit < 1) { | ||
reject(new Error('invalid limit, must be greater than 0')); | ||
return; | ||
} | ||
|
||
let failed = false; | ||
let inProgress = 0; | ||
const queue = [...source.entries()]; | ||
|
||
/** @type {T2[]} */ | ||
const results = new Array(source.length); | ||
|
||
/** | ||
* this is run for each item, manages the inProgress state, | ||
* calls the mapFn with that item, writes the map result to | ||
* the result array, and calls runMore() after each item | ||
* completes to either start another item or resolve the | ||
* returned promise. | ||
* | ||
* @param {number} index | ||
* @param {T} item | ||
*/ | ||
function run(index, item) { | ||
inProgress += 1; | ||
settle(mapFn, item).then((result) => { | ||
inProgress -= 1; | ||
|
||
if (failed) { | ||
return; | ||
} | ||
|
||
if (result.status === 'fulfilled') { | ||
results[index] = result.value; | ||
runMore(); | ||
return; | ||
} | ||
|
||
// when an error occurs we update the state to prevent | ||
// holding onto old results and ignore future results | ||
// from in-progress promises | ||
failed = true; | ||
results.length = 0; | ||
reject(result.reason); | ||
}); | ||
} | ||
|
||
/** | ||
* If there is work in the queue, schedule it, if there isn't | ||
* any work to be scheduled and there isn't anything in progress | ||
* then we're done. This function is called every time a mapFn | ||
* promise resolves and once after initialization | ||
*/ | ||
function runMore() { | ||
if (!queue.length) { | ||
if (inProgress === 0) { | ||
resolve(results); | ||
} | ||
|
||
return; | ||
} | ||
|
||
while (inProgress < limit) { | ||
const entry = queue.shift(); | ||
if (!entry) { | ||
break; | ||
} | ||
|
||
run(...entry); | ||
} | ||
} | ||
|
||
runMore(); | ||
}); | ||
} | ||
|
||
module.exports = { asyncMapWithLimit }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import { setTimeout } from 'timers/promises'; | ||
import { asyncMapWithLimit } from './async'; | ||
|
||
const NUMS = [1, 2, 3, 4]; | ||
const ident = jest.fn(async function ident<T>(x: T) { | ||
return await x; | ||
}); | ||
const double = jest.fn(async function double(x: number) { | ||
return x * 2; | ||
}); | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('resolves with an empty array', async () => { | ||
const result = await asyncMapWithLimit([], 10, ident); | ||
expect(ident).not.toHaveBeenCalled(); | ||
expect(result).toEqual([]); | ||
}); | ||
|
||
it('resolves with a mapped array', async () => { | ||
const result = await asyncMapWithLimit(NUMS, 10, double); | ||
expect(double).toHaveBeenCalledTimes(NUMS.length); | ||
expect(result.join(',')).toMatchInlineSnapshot(`"2,4,6,8"`); | ||
}); | ||
|
||
it('rejects when limit it not >= 1', async () => { | ||
await expect(() => asyncMapWithLimit([], -1, ident)).rejects.toMatchInlineSnapshot( | ||
`[Error: invalid limit, must be greater than 0]` | ||
); | ||
await expect(() => asyncMapWithLimit([], 0, ident)).rejects.toMatchInlineSnapshot( | ||
`[Error: invalid limit, must be greater than 0]` | ||
); | ||
await expect(() => asyncMapWithLimit([], -Infinity, ident)).rejects.toMatchInlineSnapshot( | ||
`[Error: invalid limit, must be greater than 0]` | ||
); | ||
}); | ||
|
||
it('rejects with the first error produced and stops calling mapFn', async () => { | ||
const map = jest.fn(async (num) => { | ||
if (num % 2 === 0) { | ||
throw new Error('even numbers are not supported'); | ||
} | ||
return num * 2; | ||
}); | ||
|
||
await expect(() => asyncMapWithLimit(NUMS, 1, map)).rejects.toMatchInlineSnapshot( | ||
`[Error: even numbers are not supported]` | ||
); | ||
await setTimeout(10); | ||
expect(map).toHaveBeenCalledTimes(2); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
const Path = require('path'); | ||
|
||
const { BazelPackage } = require('./bazel_package'); | ||
const { getAllBazelPackageDirs } = require('./bazel_package_dirs'); | ||
const { findPackages } = require('./find_files'); | ||
const { asyncMapWithLimit } = require('./async'); | ||
|
||
/** | ||
* @param {string} repoRoot | ||
*/ | ||
function discoverBazelPackageLocations(repoRoot) { | ||
const packagesWithBuildBazel = getAllBazelPackageDirs(repoRoot) | ||
.flatMap((packageDir) => findPackages(packageDir, 'BUILD.bazel')) | ||
.map((path) => Path.dirname(path)); | ||
|
||
// NOTE: only return as discovered packages with a package.json + BUILD.bazel file. | ||
// In the future we should change this to only discover the ones with kibana.jsonc. | ||
return getAllBazelPackageDirs(repoRoot) | ||
.flatMap((packageDir) => findPackages(packageDir, 'package.json')) | ||
.map((path) => Path.dirname(path)) | ||
.filter((pkg) => packagesWithBuildBazel.includes(pkg)) | ||
.sort((a, b) => a.localeCompare(b)); | ||
} | ||
|
||
/** | ||
* @param {string} repoRoot | ||
*/ | ||
async function discoverBazelPackages(repoRoot) { | ||
return await asyncMapWithLimit( | ||
discoverBazelPackageLocations(repoRoot), | ||
100, | ||
async (dir) => await BazelPackage.fromDir(repoRoot, dir) | ||
); | ||
} | ||
|
||
module.exports = { discoverBazelPackageLocations, discoverBazelPackages }; |
Oops, something went wrong.