feat: pipeline improvements

This commit is contained in:
2023-11-07 18:00:17 +01:00
parent 8a421cb2fb
commit 51602ffa0f
38 changed files with 183 additions and 130 deletions

View File

@@ -3,19 +3,19 @@
"version": "3.0.0", "version": "3.0.0",
"type": "module", "type": "module",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"main": "lib/index.js", "main": "src/index.js",
"types": "src/types.d.ts",
"files": [ "files": [
"lib", "lib",
"README.md", "README.md",
"CHANGELOG.md" "CHANGELOG.md"
], ],
"scripts": { "scripts": {
"build": "tsup-node --dts",
"docs": "typedoc --json ./docs/docs.json --options ../../typedoc.base.json src/index.ts", "docs": "typedoc --json ./docs/docs.json --options ../../typedoc.base.json src/index.ts",
"format": "prettier . -c --ignore-path ../../.gitignore", "format": "prettier . -c --ignore-path ../../.gitignore",
"format:fix": "prettier --write . --ignore-path ../../.gitignore", "format:fix": "prettier --write . --ignore-path ../../.gitignore",
"lint": "eslint --ext .ts src/", "lint": "tsc --noEmit && eslint --ext .js src/",
"lint:fix": "eslint --fix --ext .ts src/", "lint:fix": "eslint --fix --ext .js src/",
"test": "c8 mocha" "test": "c8 mocha"
}, },
"devDependencies": { "devDependencies": {
@@ -30,19 +30,9 @@
"mocha": "10.2.0", "mocha": "10.2.0",
"mocha-junit-reporter": "2.2.0", "mocha-junit-reporter": "2.2.0",
"ts-node": "10.9.1", "ts-node": "10.9.1",
"tsup": "7.2.0",
"typedoc": "0.24.8", "typedoc": "0.24.8",
"typescript": "5.1.6" "typescript": "5.1.6"
}, },
"tsup": {
"entry": [
"src/index.ts"
],
"sourcemap": true,
"clean": true,
"format": "esm",
"outDir": "lib"
},
"prettier": "@openstapps/prettier-config", "prettier": "@openstapps/prettier-config",
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [

View File

@@ -15,12 +15,15 @@
/** /**
* Chunk array into smaller arrays of a specified size. * Chunk array into smaller arrays of a specified size.
* @param array The array to chunk. * @template T
* @param chunkSize The size of each chunk. * @param array {T[]} The array to chunk.
* @param [chunkSize] {number} The size of each chunk.
* @returns {T[][]}
*/ */
export function chunk<T>(array: T[], chunkSize = 1): T[][] { export function chunk(array, chunkSize = 1) {
const arrayCopy = [...array]; const arrayCopy = [...array];
const out: T[][] = []; /** @type {T[][]} */
const out = [];
if (chunkSize <= 0) return out; if (chunkSize <= 0) return out;
while (arrayCopy.length > 0) out.push(arrayCopy.splice(0, chunkSize)); while (arrayCopy.length > 0) out.push(arrayCopy.splice(0, chunkSize));
return out; return out;

View File

@@ -15,8 +15,13 @@
/** /**
* Returns the difference between two arrays. * Returns the difference between two arrays.
* @template T
* @param a {T[]}
* @param b {T[]}
* @param transform {(item: T) => unknown}
* @returns {T[]}
*/ */
export function differenceBy<T>(a: T[], b: T[], transform: (item: T) => unknown) { export function differenceBy(a, b, transform) {
const disallowed = new Set(b.map(transform)); const disallowed = new Set(b.map(transform));
return a.filter(item => !disallowed.has(transform(item))); return a.filter(item => !disallowed.has(transform(item)));

View File

@@ -17,14 +17,20 @@
* Gets a value from a nested object. * Gets a value from a nested object.
* The path must be key names separated by dots. * The path must be key names separated by dots.
* If the path doesn't exist, undefined is returned. * If the path doesn't exist, undefined is returned.
*
* @template {unknown} U
* @param object {object}
* @param path {string}
* @returns {U}
*/ */
export function get<U = unknown>(object: object, path: string): U { export function get(object, path) {
return path.split('.').reduce( return /** @type {U} */ (
(accumulator, current) => path
accumulator?.hasOwnProperty(current) .split('.')
? // eslint-disable-next-line @typescript-eslint/no-explicit-any .reduce(
(accumulator as any)[current] (accumulator, current) =>
: undefined, accumulator?.hasOwnProperty(current) ? /** @type {any} */ (accumulator)[current] : undefined,
object, object,
) as unknown as U; )
);
} }

View File

@@ -15,31 +15,42 @@
/** /**
* Group an array by a function * Group an array by a function
* @template T
* @param collection {T[]}
* @param group {(item: T) => string | undefined}
* @returns {Record<string, T[]>}
*/ */
export function groupBy<T>(collection: T[], group: (item: T) => string | undefined): Record<string, T[]> { export function groupBy(collection, group) {
return collection.reduce((accumulator: Record<string, T[]>, item) => { return collection.reduce((accumulator, item) => {
const key = group(item) ?? ''; const key = group(item) ?? '';
accumulator[key] = accumulator[key] ?? []; accumulator[key] = accumulator[key] ?? [];
accumulator[key].push(item); accumulator[key].push(item);
return accumulator; return accumulator;
}, {}); }, /** @type {Record<string, T[]>} */ ({}));
} }
/** /**
* Group an array by a function (returns a Map, whose keys keep order info of items entry) * Group an array by a function (returns a Map, whose keys keep order info of items entry)
* @template T
* @param collection {T[]}
* @param group {(item: T) => string | undefined}
* @returns {Map<string, T[]>}
*/ */
export function groupByStable<T>(collection: T[], group: (item: T) => string | undefined): Map<string, T[]> { export function groupByStable(collection, group) {
return collection.reduce((accumulator: Map<string, T[]>, item) => { return collection.reduce((accumulator, item) => {
const key = group(item) ?? ''; const key = group(item) ?? '';
accumulator.set(key, accumulator.get(key) ?? []); accumulator.set(key, accumulator.get(key) ?? []);
accumulator.get(key)?.push(item); accumulator.get(key)?.push(item);
return accumulator; return accumulator;
}, new Map<string, T[]>()); }, /** @type {Map<string, T[]>} */ new Map());
} }
/** /**
* * @template {object} T
* @param collection {T[]}
* @param property {keyof T}
* @returns {Record<string, T[]>}
*/ */
export function groupByProperty<T extends object>(collection: T[], property: keyof T): Record<string, T[]> { export function groupByProperty(collection, property) {
return groupBy(collection, item => item[property] as unknown as string); return groupBy(collection, item => item[property]);
} }

View File

@@ -14,14 +14,17 @@
*/ */
/** /**
* Create an object composed of keys generated from the results of running * Create an object composed of keys generated from the results of running each element of collection through iteratee.
* each element of collection thru iteratee. The corresponding value of * The corresponding value of each key is the last element responsible for generating the key.
* each key is the last element responsible for generating the key. The * The iteratee is invoked with one argument: (value).
* iteratee is invoked with one argument: (value). * @template T
* @param collection {T[]}
* @param key {(item: T) => string | number}
* @returns {Record<string, T>}
*/ */
export function keyBy<T>(collection: T[], key: (item: T) => string | number): Record<string, T> { export function keyBy(collection, key) {
return collection.reduce((accumulator, item) => { return collection.reduce((accumulator, item) => {
accumulator[key(item)] = item; accumulator[key(item)] = item;
return accumulator; return accumulator;
}, {} as Record<string | number, T>); }, /** @type {Record<string, T>} */ ({}));
} }

View File

@@ -19,19 +19,19 @@
* Note: JavaScript is not multithreaded. * Note: JavaScript is not multithreaded.
* This will not let you run tasks in parallel, use it only to limit how many network requests should be * This will not let you run tasks in parallel, use it only to limit how many network requests should be
* running at once. * running at once.
* @param items the items to iterate through * @template T
* @param task the task to be run * @template U
* @param limit the maximum number of tasks that should be run asynchronously * @param items {T[]} the items to iterate through
* @param task {(item: T, index: number) => Promise<U>} the task to be run
* @param [limit] {number} the maximum number of tasks that should be run asynchronously
* @returns {Promise<U[]>}
*/ */
export async function mapAsyncLimit<T, U = void>( export async function mapAsyncLimit(items, task, limit = 5) {
items: T[],
task: (item: T, index: number) => Promise<U>,
limit = 5,
): Promise<U[]> {
return Promise.all( return Promise.all(
Array.from({length: limit}).map(async () => { Array.from({length: limit}).map(async () => {
let i = 0; let i = 0;
const results: U[] = []; /** @type {U[]} */
const results = [];
for (let item = items.shift(); item !== undefined; item = items.shift()) { for (let item = items.shift(); item !== undefined; item = items.shift()) {
results.push(await task(item, i)); results.push(await task(item, i));
i++; i++;

View File

@@ -15,12 +15,14 @@
/** /**
* Maps the values of an object to a new object * Maps the values of an object to a new object
* @template {object} T
* @template U
* @param object {T}
* @param transform {(value: T[keyof T], key: keyof T) => U}
* @returns {{[key in keyof T]: U}}
*/ */
export function mapValues<T extends object, U>( export function mapValues(object, transform) {
object: T, const result = /** @type {{[key in keyof T]: U}} */ ({});
transform: (value: T[keyof T], key: keyof T) => U,
): {[key in keyof T]: U} {
const result = {} as {[key in keyof T]: U};
for (const key in object) { for (const key in object) {
if (object.hasOwnProperty(key)) { if (object.hasOwnProperty(key)) {

View File

@@ -15,9 +15,13 @@
/** /**
* Returns the minimum value of a collection. * Returns the minimum value of a collection.
* @template T
* @param array {T[]}
* @param transform {(item: T) => number | undefined}
* @returns {T}
*/ */
export function minBy<T>(array: T[], transform: (item: T) => number | undefined): T { export function minBy(array, transform) {
const transforms = array.map(transform); const transforms = array.map(transform);
const min = Math.min(...(transforms.filter(it => !!it) as number[])); const min = Math.min(.../** @type {number[]} */ (transforms.filter(it => !!it)));
return array.find((_, i) => transforms[i] === min) as T; return /** @type {T} */ (array.find((_, i) => transforms[i] === min));
} }

View File

@@ -15,9 +15,14 @@
/** /**
* Returns a new object without the specified keys. * Returns a new object without the specified keys.
* @template {object} T
* @template {keyof T} U
* @param object {T}
* @param keys {U[]}
* @returns {Omit<T, U>}
*/ */
export function omit<T extends object, U extends keyof T>(object: T, ...keys: U[]): Omit<T, U> { export function omit(object, ...keys) {
const out = {...object}; const out = {...object};
for (const key of keys) delete out[key]; for (const key of keys) delete out[key];
return out as Exclude<T, U>; return out;
} }

View File

@@ -16,13 +16,14 @@
/** /**
* Partitions a list into two lists. One with the elements that satisfy a predicate, * Partitions a list into two lists. One with the elements that satisfy a predicate,
* and one with the elements that don't satisfy the predicate. * and one with the elements that don't satisfy the predicate.
* @template T
* @param array {T[]}
* @param transform {(item: T) => boolean}
* @returns {[T[], T[]]}
*/ */
export function partition<T>(array: T[], transform: (item: T) => boolean): [T[], T[]] { export function partition(array, transform) {
return array.reduce<[T[], T[]]>( return array.reduce((accumulator, item) => {
(accumulator, item) => { accumulator[transform(item) ? 0 : 1].push(item);
accumulator[transform(item) ? 0 : 1].push(item); return accumulator;
return accumulator; }, /** @type {[T[], T[]]} */ ([[], []]));
},
[[], []],
);
} }

View File

@@ -15,27 +15,34 @@
/** /**
* Pick a set of properties from an object * Pick a set of properties from an object
* @template {object} T
* @template {keyof T} U
* @param object {T}
* @param keys {U[]}
* @returns {Pick<T, U>}
*/ */
export function pick<T extends object, U extends keyof T>(object: T, keys: U[]): Pick<T, U> { export function pick(object, keys) {
return keys.reduce((accumulator, key) => { return keys.reduce((accumulator, key) => {
if (object.hasOwnProperty(key)) { if (object.hasOwnProperty(key)) {
accumulator[key] = object[key]; accumulator[key] = object[key];
} }
return accumulator; return accumulator;
}, {} as Pick<T, U>); }, /** @type {Pick<T, U>} */ ({}));
} }
/** /**
* Pick a set of properties from an object using a predicate function * Pick a set of properties from an object using a predicate function
* @template {object} T
* @template {keyof T} U
* @param object {T}
* @param predicate {(value: T[U], key: U) => boolean}
* @returns {Pick<T, U>}
*/ */
export function pickBy<T extends object, U extends keyof T>( export function pickBy(object, predicate) {
object: T, return /** @type {U[]} */ (Object.keys(object)).reduce((accumulator, key) => {
predicate: (value: T[U], key: U) => boolean,
): Pick<T, U> {
return (Object.keys(object) as U[]).reduce((accumulator, key) => {
if (predicate(object[key], key)) { if (predicate(object[key], key)) {
accumulator[key] = object[key]; accumulator[key] = object[key];
} }
return accumulator; return accumulator;
}, {} as Pick<T, U>); }, /** @type {Pick<T, U>} */ ({}));
} }

View File

@@ -15,8 +15,11 @@
/** /**
* Shuffles an array * Shuffles an array
* @template T
* @param array {T[]}
* @returns {T[]}
*/ */
export function shuffle<T>(array: T[]): T[] { export function shuffle(array) {
const copy = [...array]; const copy = [...array];
const out = []; const out = [];

View File

@@ -15,8 +15,11 @@
/** /**
* sort function for two strings * sort function for two strings
* @param [a] {string}
* @param [b] {string}
* @returns {number}
*/ */
export function stringSort(a = '', b = ''): number { export function stringSort(a = '', b = '') {
if (a < b) return -1; if (a < b) return -1;
if (a > b) return 1; if (a > b) return 1;
return 0; return 0;
@@ -24,9 +27,12 @@ export function stringSort(a = '', b = ''): number {
/** /**
* sort function for two strings that allows for a custom transform * sort function for two strings that allows for a custom transform
* @template T
* @param map {(item: T) => string | undefined}
* @returns {(a: T, b: T) => number}
*/ */
export function stringSortBy<T>(map: (item: T) => string | undefined): (a: T, b: T) => number { export function stringSortBy(map) {
return (a: T, b: T): number => { return (a, b) => {
const aValue = map(a) || ''; const aValue = map(a) || '';
const bValue = map(b) || ''; const bValue = map(b) || '';
if (aValue < bValue) return -1; if (aValue < bValue) return -1;

View File

@@ -15,17 +15,20 @@
/** /**
* Sum an an array * Sum an an array
* @template {object} T
* @param collection {T[]}
* @param transform {(value: T) => number | undefined}
* @returns {number}
*/ */
export function sumBy<T extends object>( export function sumBy(collection, transform) {
collection: T[],
transform: (value: T) => number | undefined,
): number {
return collection.reduce((accumulator, item) => accumulator + (transform(item) || 0), 0); return collection.reduce((accumulator, item) => accumulator + (transform(item) || 0), 0);
} }
/** /**
* Sum an array of numbers * Sum an array of numbers
* @param collection {Array<number | undefined>}
* @returns {number}
*/ */
export function sum(collection: Array<number | undefined>): number { export function sum(collection) {
return collection.reduce<number>((accumulator, item) => accumulator + (item || 0), 0); return /** @type {number[]} */ (collection).reduce((accumulator, item) => accumulator + (item ?? 0), 0);
} }

View File

@@ -13,17 +13,15 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
export type Tree<T> = {
[key: string]: Tree<T>;
} & {
_?: T[] | undefined;
};
/** /**
* * @template T
* @param items {T[]}
* @param transform {(item: T) => string[]}
* @returns {import('./types.js').Tree<T>}
*/ */
export function treeGroupBy<T>(items: T[], transform: (item: T) => string[]): Tree<T> { export function treeGroupBy(items, transform) {
const tree: Tree<T> = {}; /** @type {import('./types.js').Tree<T>} */
const tree = {};
for (const item of items) { for (const item of items) {
let currentTree = tree; let currentTree = tree;
@@ -34,7 +32,7 @@ export function treeGroupBy<T>(items: T[], transform: (item: T) => string[]): Tr
} }
// eslint-disable-next-line unicorn/no-array-for-each // eslint-disable-next-line unicorn/no-array-for-each
keys.forEach((key, i) => { keys.forEach((key, i) => {
currentTree = currentTree[key] = (currentTree[key] ?? {}) as Tree<T>; currentTree = currentTree[key] = currentTree[key] ?? {};
if (i === keys.length - 1) { if (i === keys.length - 1) {
currentTree._ = currentTree._ ?? []; currentTree._ = currentTree._ ?? [];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion

View File

@@ -0,0 +1,7 @@
export * from './index.js';
export type Tree<T> = {
[key: string]: Tree<T>;
} & {
_?: T[] | undefined;
};

View File

@@ -15,12 +15,16 @@
/** /**
* Filter out duplicates from an array. * Filter out duplicates from an array.
* @template T
* @param array {T[]}
* @param transform {(item: T) => string | number}
* @returns {T[]}
*/ */
export function uniqBy<T>(array: T[], transform: (item: T) => string | number): T[] { export function uniqBy(array, transform) {
return Object.values( return Object.values(
array.reduce((accumulator, current) => { array.reduce((accumulator, current) => {
accumulator[transform(current)] = current; accumulator[transform(current)] = current;
return accumulator; return accumulator;
}, {} as Record<string | number, T>), }, /** @type {Record<string, T>} */ ({})),
); );
} }

View File

@@ -15,7 +15,12 @@
/** /**
* Zip two arrays together. * Zip two arrays together.
* @template T
* @template U
* @param a {T[]}
* @param b {U[]}
* @returns {[T, U][]}
*/ */
export function zip<T, U>(a: T[], b: U[]): [T, U][] { export function zip(a, b) {
return a.map((_, i) => [a[i], b[i]]); return a.map((_, i) => [a[i], b[i]]);
} }

View File

@@ -12,17 +12,17 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {treeGroupBy} from '../src/index.js';
import {expect} from 'chai'; import {expect} from 'chai';
import {Tree, treeGroupBy} from '../src/index.js';
interface TestItem {
id: number;
path?: string[];
}
/**
* @typedef {{id: number; path?: string[]}} TestItem
* @typedef {import('../src/types.js').Tree<TestItem>} TestItemTree
*/
describe('tree-group', function () { describe('tree-group', function () {
it('should create a tree', function () { it('should create a tree', function () {
const items: Array<TestItem> = [ /** @type {TestItem[]} */
const items = [
{ {
id: 1, id: 1,
path: ['a', 'b', 'c'], path: ['a', 'b', 'c'],
@@ -35,20 +35,22 @@ describe('tree-group', function () {
const tree = treeGroupBy(items, item => item.path ?? []); const tree = treeGroupBy(items, item => item.path ?? []);
const expectedTree: Tree<TestItem> = { /** @type {TestItemTree} */
a: { const expectedTree = {
a: /** @type {TestItemTree} */ ({
b: { b: {
c: {_: [items[0]]}, c: {_: [items[0]]},
d: {_: [items[1]]}, d: {_: [items[1]]},
} as Tree<TestItem>, },
} as Tree<TestItem>, }),
} as Tree<TestItem>; };
expect(tree).to.deep.equal(expectedTree); expect(tree).to.deep.equal(expectedTree);
}); });
it('should also sort empty paths', () => { it('should also sort empty paths', () => {
const items: Array<TestItem> = [ /** @type {TestItem[]} */
const items = [
{ {
id: 1, id: 1,
path: ['a', 'b', 'c'], path: ['a', 'b', 'c'],
@@ -60,14 +62,14 @@ describe('tree-group', function () {
const tree = treeGroupBy(items, item => item.path ?? []); const tree = treeGroupBy(items, item => item.path ?? []);
const expectedTree: Tree<TestItem> = { const expectedTree = /** @type {TestItemTree} */ ({
a: { a: {
b: { b: {
c: {_: [items[0]]}, c: {_: [items[0]]},
} as Tree<TestItem>, },
} as Tree<TestItem>, },
_: [items[1]], _: [items[1]],
} as Tree<TestItem>; });
expect(tree).to.deep.equal(expectedTree); expect(tree).to.deep.equal(expectedTree);
}); });

View File

@@ -74,15 +74,6 @@
"typedoc": "0.24.8", "typedoc": "0.24.8",
"typescript": "5.1.6" "typescript": "5.1.6"
}, },
"tsup": {
"entry": [
"src/index.ts"
],
"sourcemap": true,
"clean": true,
"format": "esm",
"outDir": "lib"
},
"prettier": "@openstapps/prettier-config", "prettier": "@openstapps/prettier-config",
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [

3
pnpm-lock.yaml generated
View File

@@ -1625,9 +1625,6 @@ importers:
ts-node: ts-node:
specifier: 10.9.1 specifier: 10.9.1
version: 10.9.1(@types/node@18.15.3)(typescript@5.1.6) version: 10.9.1(@types/node@18.15.3)(typescript@5.1.6)
tsup:
specifier: 7.2.0
version: 7.2.0(ts-node@10.9.1)(typescript@5.1.6)
typedoc: typedoc:
specifier: 0.24.8 specifier: 0.24.8
version: 0.24.8(typescript@5.1.6) version: 0.24.8(typescript@5.1.6)