diff --git a/package.json b/package.json index 80960bb..d2b54c4 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "devDependencies": { "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.32.0", - "@types/node": "^24.2.0", "@typescript-eslint/eslint-plugin": "^8.38.0", "@typescript-eslint/parser": "^8.38.0", "auto": "^11.3.0", @@ -34,8 +33,9 @@ "eslint": "^9.0.0", "eslint-config-prettier": "^10.1.8", "prettier": "^3.6.2", - "ts-node": "^10.9.2", - "typescript": "5.8.3" + "typescript": "5.9.2", + "@types/node": "^24.2.0", + "ts-node": "^10.9.2" }, "prettier": { "semi": false diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a986518..eca5b4a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,13 +22,13 @@ importers: version: 24.2.0 "@typescript-eslint/eslint-plugin": specifier: ^8.38.0 - version: 8.38.0(@typescript-eslint/parser@8.38.0(eslint@9.32.0)(typescript@5.8.3))(eslint@9.32.0)(typescript@5.8.3) + version: 8.38.0(@typescript-eslint/parser@8.38.0(eslint@9.32.0)(typescript@5.9.2))(eslint@9.32.0)(typescript@5.9.2) "@typescript-eslint/parser": specifier: ^8.38.0 - version: 8.38.0(eslint@9.32.0)(typescript@5.8.3) + version: 8.38.0(eslint@9.32.0)(typescript@5.9.2) auto: specifier: ^11.3.0 - version: 11.3.0(@types/node@24.2.0)(typescript@5.8.3) + version: 11.3.0(@types/node@24.2.0)(typescript@5.9.2) ava: specifier: ^6.4.1 version: 6.4.1 @@ -43,10 +43,10 @@ importers: version: 3.6.2 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@24.2.0)(typescript@5.8.3) + version: 10.9.2(@types/node@24.2.0)(typescript@5.9.2) typescript: - specifier: 5.8.3 - version: 5.8.3 + specifier: 5.9.2 + version: 5.9.2 packages: "@auto-it/bot-list@11.3.0": @@ -2795,10 +2795,10 @@ packages: integrity: sha512-GQ90TcKpIH4XxYTI2F98yEQYZgjNMOGPpOgdjIBhaLaWji5HPWlRnZ4AeA1hfBxtY7bCGDJsqDDHk/KaHOl5bA==, } - typescript@5.8.3: + typescript@5.9.2: resolution: { - integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==, + integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==, } engines: { node: ">=14.17" } hasBin: true @@ -2981,10 +2981,10 @@ packages: snapshots: "@auto-it/bot-list@11.3.0": {} - "@auto-it/core@11.3.0(@types/node@24.2.0)(typescript@5.8.3)": + "@auto-it/core@11.3.0(@types/node@24.2.0)(typescript@5.9.2)": dependencies: "@auto-it/bot-list": 11.3.0 - "@endemolshinegroup/cosmiconfig-typescript-loader": 3.0.2(cosmiconfig@7.0.0)(typescript@5.8.3) + "@endemolshinegroup/cosmiconfig-typescript-loader": 3.0.2(cosmiconfig@7.0.0)(typescript@5.9.2) "@octokit/core": 3.6.0 "@octokit/plugin-enterprise-compatibility": 1.3.0 "@octokit/plugin-retry": 3.0.9 @@ -3018,10 +3018,10 @@ snapshots: tapable: 2.2.2 terminal-link: 2.1.1 tinycolor2: 1.6.0 - ts-node: 10.9.2(@types/node@24.2.0)(typescript@5.8.3) + ts-node: 10.9.2(@types/node@24.2.0)(typescript@5.9.2) tslib: 2.1.0 type-fest: 0.21.3 - typescript: 5.8.3 + typescript: 5.9.2 typescript-memoize: 1.1.1 url-join: 4.0.1 optionalDependencies: @@ -3032,9 +3032,9 @@ snapshots: - encoding - supports-color - "@auto-it/npm@11.3.0(@types/node@24.2.0)(typescript@5.8.3)": + "@auto-it/npm@11.3.0(@types/node@24.2.0)(typescript@5.9.2)": dependencies: - "@auto-it/core": 11.3.0(@types/node@24.2.0)(typescript@5.8.3) + "@auto-it/core": 11.3.0(@types/node@24.2.0)(typescript@5.9.2) "@auto-it/package-json-utils": 11.3.0 await-to-js: 3.0.0 endent: 2.1.0 @@ -3061,10 +3061,10 @@ snapshots: parse-author: 2.0.0 parse-github-url: 1.0.2 - "@auto-it/released@11.3.0(@types/node@24.2.0)(typescript@5.8.3)": + "@auto-it/released@11.3.0(@types/node@24.2.0)(typescript@5.9.2)": dependencies: "@auto-it/bot-list": 11.3.0 - "@auto-it/core": 11.3.0(@types/node@24.2.0)(typescript@5.8.3) + "@auto-it/core": 11.3.0(@types/node@24.2.0)(typescript@5.9.2) deepmerge: 4.3.1 fp-ts: 2.16.10 io-ts: 2.2.22(fp-ts@2.16.10) @@ -3077,9 +3077,9 @@ snapshots: - supports-color - typescript - "@auto-it/version-file@11.3.0(@types/node@24.2.0)(typescript@5.8.3)": + "@auto-it/version-file@11.3.0(@types/node@24.2.0)(typescript@5.9.2)": dependencies: - "@auto-it/core": 11.3.0(@types/node@24.2.0)(typescript@5.8.3) + "@auto-it/core": 11.3.0(@types/node@24.2.0)(typescript@5.9.2) fp-ts: 2.16.10 io-ts: 2.2.22(fp-ts@2.16.10) semver: 7.7.2 @@ -3104,12 +3104,12 @@ snapshots: dependencies: "@jridgewell/trace-mapping": 0.3.9 - "@endemolshinegroup/cosmiconfig-typescript-loader@3.0.2(cosmiconfig@7.0.0)(typescript@5.8.3)": + "@endemolshinegroup/cosmiconfig-typescript-loader@3.0.2(cosmiconfig@7.0.0)(typescript@5.9.2)": dependencies: cosmiconfig: 7.0.0 lodash.get: 4.4.2 make-error: 1.3.6 - ts-node: 9.1.1(typescript@5.8.3) + ts-node: 9.1.1(typescript@5.9.2) tslib: 2.1.0 transitivePeerDependencies: - typescript @@ -3346,41 +3346,41 @@ snapshots: "@types/parse-json@4.0.2": {} - "@typescript-eslint/eslint-plugin@8.38.0(@typescript-eslint/parser@8.38.0(eslint@9.32.0)(typescript@5.8.3))(eslint@9.32.0)(typescript@5.8.3)": + "@typescript-eslint/eslint-plugin@8.38.0(@typescript-eslint/parser@8.38.0(eslint@9.32.0)(typescript@5.9.2))(eslint@9.32.0)(typescript@5.9.2)": dependencies: "@eslint-community/regexpp": 4.12.1 - "@typescript-eslint/parser": 8.38.0(eslint@9.32.0)(typescript@5.8.3) + "@typescript-eslint/parser": 8.38.0(eslint@9.32.0)(typescript@5.9.2) "@typescript-eslint/scope-manager": 8.38.0 - "@typescript-eslint/type-utils": 8.38.0(eslint@9.32.0)(typescript@5.8.3) - "@typescript-eslint/utils": 8.38.0(eslint@9.32.0)(typescript@5.8.3) + "@typescript-eslint/type-utils": 8.38.0(eslint@9.32.0)(typescript@5.9.2) + "@typescript-eslint/utils": 8.38.0(eslint@9.32.0)(typescript@5.9.2) "@typescript-eslint/visitor-keys": 8.38.0 eslint: 9.32.0 graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - supports-color - "@typescript-eslint/parser@8.38.0(eslint@9.32.0)(typescript@5.8.3)": + "@typescript-eslint/parser@8.38.0(eslint@9.32.0)(typescript@5.9.2)": dependencies: "@typescript-eslint/scope-manager": 8.38.0 "@typescript-eslint/types": 8.38.0 - "@typescript-eslint/typescript-estree": 8.38.0(typescript@5.8.3) + "@typescript-eslint/typescript-estree": 8.38.0(typescript@5.9.2) "@typescript-eslint/visitor-keys": 8.38.0 debug: 4.4.1 eslint: 9.32.0 - typescript: 5.8.3 + typescript: 5.9.2 transitivePeerDependencies: - supports-color - "@typescript-eslint/project-service@8.38.0(typescript@5.8.3)": + "@typescript-eslint/project-service@8.38.0(typescript@5.9.2)": dependencies: - "@typescript-eslint/tsconfig-utils": 8.38.0(typescript@5.8.3) + "@typescript-eslint/tsconfig-utils": 8.38.0(typescript@5.9.2) "@typescript-eslint/types": 8.38.0 debug: 4.4.1 - typescript: 5.8.3 + typescript: 5.9.2 transitivePeerDependencies: - supports-color @@ -3389,28 +3389,28 @@ snapshots: "@typescript-eslint/types": 8.38.0 "@typescript-eslint/visitor-keys": 8.38.0 - "@typescript-eslint/tsconfig-utils@8.38.0(typescript@5.8.3)": + "@typescript-eslint/tsconfig-utils@8.38.0(typescript@5.9.2)": dependencies: - typescript: 5.8.3 + typescript: 5.9.2 - "@typescript-eslint/type-utils@8.38.0(eslint@9.32.0)(typescript@5.8.3)": + "@typescript-eslint/type-utils@8.38.0(eslint@9.32.0)(typescript@5.9.2)": dependencies: "@typescript-eslint/types": 8.38.0 - "@typescript-eslint/typescript-estree": 8.38.0(typescript@5.8.3) - "@typescript-eslint/utils": 8.38.0(eslint@9.32.0)(typescript@5.8.3) + "@typescript-eslint/typescript-estree": 8.38.0(typescript@5.9.2) + "@typescript-eslint/utils": 8.38.0(eslint@9.32.0)(typescript@5.9.2) debug: 4.4.1 eslint: 9.32.0 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - supports-color "@typescript-eslint/types@8.38.0": {} - "@typescript-eslint/typescript-estree@8.38.0(typescript@5.8.3)": + "@typescript-eslint/typescript-estree@8.38.0(typescript@5.9.2)": dependencies: - "@typescript-eslint/project-service": 8.38.0(typescript@5.8.3) - "@typescript-eslint/tsconfig-utils": 8.38.0(typescript@5.8.3) + "@typescript-eslint/project-service": 8.38.0(typescript@5.9.2) + "@typescript-eslint/tsconfig-utils": 8.38.0(typescript@5.9.2) "@typescript-eslint/types": 8.38.0 "@typescript-eslint/visitor-keys": 8.38.0 debug: 4.4.1 @@ -3418,19 +3418,19 @@ snapshots: is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.7.2 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - supports-color - "@typescript-eslint/utils@8.38.0(eslint@9.32.0)(typescript@5.8.3)": + "@typescript-eslint/utils@8.38.0(eslint@9.32.0)(typescript@5.9.2)": dependencies: "@eslint-community/eslint-utils": 4.7.0(eslint@9.32.0) "@typescript-eslint/scope-manager": 8.38.0 "@typescript-eslint/types": 8.38.0 - "@typescript-eslint/typescript-estree": 8.38.0(typescript@5.8.3) + "@typescript-eslint/typescript-estree": 8.38.0(typescript@5.9.2) eslint: 9.32.0 - typescript: 5.8.3 + typescript: 5.9.2 transitivePeerDependencies: - supports-color @@ -3537,12 +3537,12 @@ snapshots: author-regex@1.0.0: {} - auto@11.3.0(@types/node@24.2.0)(typescript@5.8.3): + auto@11.3.0(@types/node@24.2.0)(typescript@5.9.2): dependencies: - "@auto-it/core": 11.3.0(@types/node@24.2.0)(typescript@5.8.3) - "@auto-it/npm": 11.3.0(@types/node@24.2.0)(typescript@5.8.3) - "@auto-it/released": 11.3.0(@types/node@24.2.0)(typescript@5.8.3) - "@auto-it/version-file": 11.3.0(@types/node@24.2.0)(typescript@5.8.3) + "@auto-it/core": 11.3.0(@types/node@24.2.0)(typescript@5.9.2) + "@auto-it/npm": 11.3.0(@types/node@24.2.0)(typescript@5.9.2) + "@auto-it/released": 11.3.0(@types/node@24.2.0)(typescript@5.9.2) + "@auto-it/version-file": 11.3.0(@types/node@24.2.0)(typescript@5.9.2) await-to-js: 3.0.0 chalk: 4.1.2 command-line-application: 0.10.1 @@ -4594,11 +4594,11 @@ snapshots: tr46@0.0.3: {} - ts-api-utils@2.1.0(typescript@5.8.3): + ts-api-utils@2.1.0(typescript@5.9.2): dependencies: - typescript: 5.8.3 + typescript: 5.9.2 - ts-node@10.9.2(@types/node@24.2.0)(typescript@5.8.3): + ts-node@10.9.2(@types/node@24.2.0)(typescript@5.9.2): dependencies: "@cspotcode/source-map-support": 0.8.1 "@tsconfig/node10": 1.0.11 @@ -4612,18 +4612,18 @@ snapshots: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.8.3 + typescript: 5.9.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - ts-node@9.1.1(typescript@5.8.3): + ts-node@9.1.1(typescript@5.9.2): dependencies: arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 source-map-support: 0.5.21 - typescript: 5.8.3 + typescript: 5.9.2 yn: 3.1.1 tslib@1.10.0: {} @@ -4642,7 +4642,7 @@ snapshots: typescript-memoize@1.1.1: {} - typescript@5.8.3: {} + typescript@5.9.2: {} typical@4.0.0: {} diff --git a/src/collections.test.ts b/src/collections.test.ts index 58afefe..c5eee52 100644 --- a/src/collections.test.ts +++ b/src/collections.test.ts @@ -1,5 +1,5 @@ import test from "ava" -import { chain } from "./collections" +import { chain, objChain } from "./collections" test("should associateBy correctly", (t) => { const result = chain([1, 2, 3]) @@ -162,3 +162,61 @@ test("should allow flatMap over sets", (t) => { t.deepEqual(result, [1, 2, 3, 1]) }) + +test("chain::mapAsync should be downwards compatible", async (t) => { + const c = chain([1, 2, 3, 4, 5]) + + const b = await c.mapAsync(async (it) => it * 3) + + const result = b.value() + + t.deepEqual(result, [3, 6, 9, 12, 15]) +}) + +test("chain::mapAsync should be chainable into an async chain", async (t) => { + const c = chain([1, 2, 3, 4, 5]) + + const b = await c + .mapAsync(async (it) => it * 3) + .filter(async (it) => it % 2 === 1) + .sortBy(async (it) => -it) + + const result = b.value() + + t.deepEqual(result, [15, 9, 3]) +}) + +test("The Touched Type should work correctly with object chain", (t) => { + // we do the iife here, so TS doesn't narrow the type further + const result = testTouchedType([{ id: true }] as unknown as Touched) + + t.deepEqual(result, { "0-mapped": { id: true } }) +}) + +type Touched = + | undefined + | true + | (Entity extends ReadonlyArray + ? { + [X in number]?: Touched + } + : Entity extends Array + ? { + [X in number]?: Touched + } + : Entity extends Record + ? { + [K in keyof Entity]?: Touched + } + : never) + +function testTouchedType< + Entity extends Array | ReadonlyArray, +>(touched: Touched) { + if (touched !== true) { + return objChain(touched) + ?.mapKeys((it) => it.toString() + "-mapped") + .value() + } + return false +} diff --git a/src/collections.test.ts.snap b/src/collections.test.ts.snap index d8a29a9..815ea5e 100644 Binary files a/src/collections.test.ts.snap and b/src/collections.test.ts.snap differ diff --git a/src/collections.ts b/src/collections.ts index 8aef44b..26d6882 100644 --- a/src/collections.ts +++ b/src/collections.ts @@ -29,12 +29,15 @@ import { zip, } from "@opencreek/deno-std-collections" import { error } from "." +import { AsyncChain, asyncChain } from "./collections/AsyncChain" -type PairSplit = T extends [infer F, infer L] ? [Chain, Chain] : never +export type PairSplit = T extends [infer F, infer L] + ? [Chain, Chain] + : never -type IfString = T extends string ? U : never +export type IfString = T extends string ? U : never -type ArrayOrChain = Chain | ReadonlyArray +export type ArrayOrChain = Chain | ReadonlyArray // used to mappe type union below // otherwise mixed types in chains, would not be carried over correctly type Distribute = U extends any ? { type: U } : never @@ -42,11 +45,12 @@ type FlattenChain = Distribute extends { type: ArrayOrChain } ? Chain : Chain export function objChain( - value: Record | ObjectChain | Chain, + value: + | Record + | Partial> + | ObjectChain + | Chain, ): ObjectChain> -export function objChain( - value: Partial>, -): ObjectChain>> export function objChain<_K extends string | number | symbol, _T>( value: undefined | null, ): undefined @@ -207,6 +211,10 @@ export class Chain implements Iterable { return this.val } + async() { + return asyncChain(this) + } + associateBy( selector: (el: T) => S, ): ObjectChain { @@ -312,15 +320,14 @@ export class Chain implements Iterable { return new Chain(filtered) } - async filterAsync( + filterAsync( predicate: ( el: T, index: number, array: ReadonlyArray, ) => Promise, - ): Promise> { - const includes = await this.mapAsync(predicate) - return this.filter((_, index) => includes.val[index]) + ): AsyncChain { + return this.async().filter(predicate) } filterNotNullish(): Chain> { @@ -459,11 +466,10 @@ export class Chain implements Iterable { return new Chain(mapped) } - async mapAsync( + mapAsync( transformer: (el: T, index: number, array: ReadonlyArray) => Promise, - ): Promise> { - const ret = await Promise.all(this.val.map(transformer)) - return new Chain(ret) + ): AsyncChain { + return this.async().map(transformer) } mapNotNullish(transformer: (el: T) => O): Chain> { @@ -653,6 +659,7 @@ export class Chain implements Iterable { sortBy(selector: (el: T) => bigint): Chain sortBy(selector: (el: T) => string): Chain sortBy(selector: (el: T) => number): Chain + sortBy(selector: (el: T) => Date | bigint | string | number): Chain sortBy(selector: (el: T) => Date | bigint | string | number): Chain { // this is safe, because sortBy is overloaded as well const ret = sortBy(this.val, selector as (el: T) => number) diff --git a/src/collections/AsyncChain.test.ts b/src/collections/AsyncChain.test.ts new file mode 100644 index 0000000..b543888 --- /dev/null +++ b/src/collections/AsyncChain.test.ts @@ -0,0 +1,244 @@ +import test from "ava" +import { Chain, chain } from "../collections" +import { sleep } from "../sleep" +import { asyncChain } from "./AsyncChain" +import { error } from "../error" + +test("AsyncChain should contain data", async (t) => { + const chain = asyncChain([1, 2, 3]) + + const result = await chain.value() + t.deepEqual(result, [1, 2, 3]) +}) + +test("AsyncChain should map", async (t) => { + const c = chain([1, 2, 3]).async() + + const b = c.map((it) => it * 2) + + const result = await b.value() + + t.deepEqual(result, [2, 4, 6]) +}) +test("AsyncChain should be chainable", async (t) => { + const c = asyncChain([1, 2, 3, 4, 5]) + + const b = await c + .map(withDelay((it) => it * 3)) + .filter(withDelay((it) => it % 2 === 1)) + .sortBy(withDelay((it) => -it)) + + const result = b.value() + + t.deepEqual(result, [15, 9, 3]) +}) + +test("AsyncChain should be chainable including value", async (t) => { + const c = asyncChain([1, 2, 3, 4, 5]) + + const result = await c + .map(withDelay((it) => it * 3)) + .filter(withDelay((it) => it % 2 === 1)) + .sortBy(withDelay((it) => -it)) + .value() + + t.deepEqual(result, [15, 9, 3]) +}) + +test("async chain should not evaluate multiple times", async (t) => { + const c = chain([1, 2, 3, 4, 5]) + + let evaluations = 0 + const b = c.mapAsync(async (it) => { + evaluations++ + return it * 3 + }) + + const result = await b.value() + + t.deepEqual(result, [3, 6, 9, 12, 15]) + + const result2 = await b.filter(async (it) => it % 2 === 1).value() + + t.deepEqual(result2, [3, 9, 15]) + t.is(evaluations, 5, "should not evaluate multiple times") +}) + +test("Async chain should associate to async object chain", async (t) => { + const c = asyncChain([1, 2, 3, 4, 5, 6, 7, 8, 9]) + + const result = await c + .map(withDelay((it) => it * 77)) + .groupBy(withDelay((it) => it % 4)) + .mapKeys(withDelay((it) => it * 10)) + .value() + + t.deepEqual(result, { + "0": [308, 616], + "10": [77, 385, 693], + "20": [154, 462], + "30": [231, 539], + }) +}) + +test("should correctly handle exceptions", async (t) => { + const c = asyncChain([1, 2, 3, 4]) + + await t.throwsAsync( + async () => + await c + .map(withDelay((it) => (it == 3 ? error("3!") : it))) + .map((it) => it) + .value(), + ) +}) + +test("async obj chain should correctly handle exceptions", async (t) => { + const c = asyncChain([1, 2, 3, 4]).associateBy((it) => it) + + await t.throwsAsync(async () => { + await c.mapKeys(withDelay((it) => (it == 3 ? error("3!") : it))) + }) +}) + +test("dropWhile should work correctly", async (t) => { + const c = asyncChain([1, 2, 3, 4, 5, 2, 1]) + const result = await c.dropWhile(withDelay((it) => it < 3)).value() + + t.deepEqual(result, [3, 4, 5, 2, 1]) +}) + +test("dropLastWhile should work correctly", async (t) => { + const c = asyncChain([3, 1, 2, 3, 4, 5]) + const result = await c.dropLastWhile(withDelay((it) => it >= 3)).value() + + t.deepEqual(result, [3, 1, 2]) +}) + +test("reduce should work correctly", async (t) => { + const c = asyncChain([1, 2, 3, 4, 5]) + + const result = await c.reduce(async (acc, cur) => { + await sleep(0) + return acc + cur + }) + + t.is(result, 15) +}) + +test("reduceRight should work correctly", async (t) => { + const c = asyncChain([1, 2, 3, 4, 5]) + + const result = await c.reduceRight(async (acc, cur) => { + await sleep(0) + return acc + cur.toString() + }) + + t.is(result, "54321") +}) + +test("All the functions once, so they don't form infinite loops", async (t) => { + const c = chain([1, 2, 3, 4, 5, 6, 7, 8, 9]).async() + + const result = await c + .map(withDelay((it) => it * 3)) + .mapNotNullish(withDelay((it) => it + 1)) + .concat(c) + .distinct() + .distinctBy(withDelay((it) => it)) + .drop(2) + .dropLast(1) + .dropWhile(withDelay((it) => it < 15)) + .dropLastWhile(withDelay((it) => it > 5)) + .filter(withDelay((it) => it % 3 > 0)) + .filterNotNullish() + .chunk(2) + .flatten() + .flatMap(withDelay((it) => [it, it + 2])) + .slice(0, 5) + .sort() + .sortBy(withDelay((it) => -it)) + .permutations() + .reduce>>(async (array, it) => + chain([it, ...chain(array).value()]), + ) + + t.snapshot(result.value(), "long chain") + + t.snapshot(await c.takeWhile(withDelay((it) => it < 3)), "take while") + t.snapshot( + await c.takeLastWhile(withDelay((it) => it > 3)), + "take last while", + ) + + t.snapshot(await c.zip([2, 1]).unzip(), "zip") + + t.snapshot(await c.maxBy(withDelay((it) => it * 3)), "maxBy") + t.snapshot(await c.maxOf(withDelay((it) => it * 3)), "maxOf") + t.snapshot(await c.minBy(withDelay((it) => it * 3)), "minBy") + t.snapshot(await c.minOf(withDelay((it) => it * 3)), "minOf") + + t.snapshot( + await asyncChain(c.partition(withDelay((it) => it % 2 == 1))), + "partition", + ) + + await c.forEach(async (it) => { + await sleep(0) + t.true(it < 10) + }) + + t.true(await c.every(withDelay((it) => it > 0))) + t.true(await c.some(withDelay((it) => it > 3))) + + t.snapshot(await c.first(), "first") + t.snapshot( + await c.firstNotNullishOf(withDelay((it) => (it < 3 ? null : it))), + "firstNotNullishOf", + ) + t.snapshot(await c.last(), "last") + t.snapshot(await c.findIndex(withDelay((it) => it == 3)), "findIndex") + t.snapshot(await c.findSingle(withDelay((it) => it % 3 == 0)), "findSingle 1") + t.snapshot(await c.findSingle(withDelay((it) => it == 3)), "findSingle 2") + + t.snapshot(await c.indexOf(3), "indexOf") + t.snapshot(await c.concat(c).lastIndexOf(3), "concat") + + t.snapshot(await c.intersect(asyncChain([-1, 1, 2, 3])), "intersect") + + t.snapshot(await c.join(","), "join") + t.snapshot(await c.mapJoin(",", (it) => it + "^"), "mapJoin") +}) + +test("firstNotNullishOf", async (t) => { + const c = asyncChain([1, 2, 3, 4, 5, 6, 7, 8, 9]) + + const result = await c.firstNotNullishOf( + withDelay((it) => (it < 5 ? null : it + 3)), + ) + + t.is(result, 8) +}) + +test("findIndex", async (t) => { + const c = asyncChain([1, 2, 3, 4, 5, 6, 4, 8, 9]) + const result = await c.findIndex(withDelay((it) => it == 4)) + + t.is(result, 3) +}) + +test("findLastIndex", async (t) => { + const c = asyncChain([1, 2, 3, 4, 5, 6, 4, 8, 9]) + const result = await c.findLastIndex(withDelay((it) => it == 4)) + + t.is(result, 6) +}) + +function withDelay( + transform: (el: T) => Promise | U, +): (el: T) => Promise { + return async (el: T) => { + await sleep(Math.random() * 15) + return await transform(el) + } +} diff --git a/src/collections/AsyncChain.test.ts.md b/src/collections/AsyncChain.test.ts.md new file mode 100644 index 0000000..3d55d30 --- /dev/null +++ b/src/collections/AsyncChain.test.ts.md @@ -0,0 +1,1039 @@ +# Snapshot report for `src/collections/AsyncChain.test.ts` + +The actual snapshot is saved in `AsyncChain.test.ts.snap`. + +Generated by [AVA](https://avajs.dev). + +## All the functions once, so they don't form infinite loops + +> long chain + + [ + [ + 16, + 21, + 19, + 18, + 22, + ], + [ + 21, + 16, + 19, + 18, + 22, + ], + [ + 19, + 16, + 21, + 18, + 22, + ], + [ + 16, + 19, + 21, + 18, + 22, + ], + [ + 21, + 19, + 16, + 18, + 22, + ], + [ + 19, + 21, + 16, + 18, + 22, + ], + [ + 19, + 21, + 18, + 16, + 22, + ], + [ + 21, + 19, + 18, + 16, + 22, + ], + [ + 18, + 19, + 21, + 16, + 22, + ], + [ + 19, + 18, + 21, + 16, + 22, + ], + [ + 21, + 18, + 19, + 16, + 22, + ], + [ + 18, + 21, + 19, + 16, + 22, + ], + [ + 18, + 16, + 19, + 21, + 22, + ], + [ + 16, + 18, + 19, + 21, + 22, + ], + [ + 19, + 18, + 16, + 21, + 22, + ], + [ + 18, + 19, + 16, + 21, + 22, + ], + [ + 16, + 19, + 18, + 21, + 22, + ], + [ + 19, + 16, + 18, + 21, + 22, + ], + [ + 21, + 16, + 18, + 19, + 22, + ], + [ + 16, + 21, + 18, + 19, + 22, + ], + [ + 18, + 21, + 16, + 19, + 22, + ], + [ + 21, + 18, + 16, + 19, + 22, + ], + [ + 16, + 18, + 21, + 19, + 22, + ], + [ + 18, + 16, + 21, + 19, + 22, + ], + [ + 22, + 16, + 21, + 19, + 18, + ], + [ + 16, + 22, + 21, + 19, + 18, + ], + [ + 21, + 22, + 16, + 19, + 18, + ], + [ + 22, + 21, + 16, + 19, + 18, + ], + [ + 16, + 21, + 22, + 19, + 18, + ], + [ + 21, + 16, + 22, + 19, + 18, + ], + [ + 21, + 16, + 19, + 22, + 18, + ], + [ + 16, + 21, + 19, + 22, + 18, + ], + [ + 19, + 21, + 16, + 22, + 18, + ], + [ + 21, + 19, + 16, + 22, + 18, + ], + [ + 16, + 19, + 21, + 22, + 18, + ], + [ + 19, + 16, + 21, + 22, + 18, + ], + [ + 19, + 22, + 21, + 16, + 18, + ], + [ + 22, + 19, + 21, + 16, + 18, + ], + [ + 21, + 19, + 22, + 16, + 18, + ], + [ + 19, + 21, + 22, + 16, + 18, + ], + [ + 22, + 21, + 19, + 16, + 18, + ], + [ + 21, + 22, + 19, + 16, + 18, + ], + [ + 16, + 22, + 19, + 21, + 18, + ], + [ + 22, + 16, + 19, + 21, + 18, + ], + [ + 19, + 16, + 22, + 21, + 18, + ], + [ + 16, + 19, + 22, + 21, + 18, + ], + [ + 22, + 19, + 16, + 21, + 18, + ], + [ + 19, + 22, + 16, + 21, + 18, + ], + [ + 18, + 22, + 16, + 21, + 19, + ], + [ + 22, + 18, + 16, + 21, + 19, + ], + [ + 16, + 18, + 22, + 21, + 19, + ], + [ + 18, + 16, + 22, + 21, + 19, + ], + [ + 22, + 16, + 18, + 21, + 19, + ], + [ + 16, + 22, + 18, + 21, + 19, + ], + [ + 16, + 22, + 21, + 18, + 19, + ], + [ + 22, + 16, + 21, + 18, + 19, + ], + [ + 21, + 16, + 22, + 18, + 19, + ], + [ + 16, + 21, + 22, + 18, + 19, + ], + [ + 22, + 21, + 16, + 18, + 19, + ], + [ + 21, + 22, + 16, + 18, + 19, + ], + [ + 21, + 18, + 16, + 22, + 19, + ], + [ + 18, + 21, + 16, + 22, + 19, + ], + [ + 16, + 21, + 18, + 22, + 19, + ], + [ + 21, + 16, + 18, + 22, + 19, + ], + [ + 18, + 16, + 21, + 22, + 19, + ], + [ + 16, + 18, + 21, + 22, + 19, + ], + [ + 22, + 18, + 21, + 16, + 19, + ], + [ + 18, + 22, + 21, + 16, + 19, + ], + [ + 21, + 22, + 18, + 16, + 19, + ], + [ + 22, + 21, + 18, + 16, + 19, + ], + [ + 18, + 21, + 22, + 16, + 19, + ], + [ + 21, + 18, + 22, + 16, + 19, + ], + [ + 19, + 18, + 22, + 16, + 21, + ], + [ + 18, + 19, + 22, + 16, + 21, + ], + [ + 22, + 19, + 18, + 16, + 21, + ], + [ + 19, + 22, + 18, + 16, + 21, + ], + [ + 18, + 22, + 19, + 16, + 21, + ], + [ + 22, + 18, + 19, + 16, + 21, + ], + [ + 22, + 18, + 16, + 19, + 21, + ], + [ + 18, + 22, + 16, + 19, + 21, + ], + [ + 16, + 22, + 18, + 19, + 21, + ], + [ + 22, + 16, + 18, + 19, + 21, + ], + [ + 18, + 16, + 22, + 19, + 21, + ], + [ + 16, + 18, + 22, + 19, + 21, + ], + [ + 16, + 19, + 22, + 18, + 21, + ], + [ + 19, + 16, + 22, + 18, + 21, + ], + [ + 22, + 16, + 19, + 18, + 21, + ], + [ + 16, + 22, + 19, + 18, + 21, + ], + [ + 19, + 22, + 16, + 18, + 21, + ], + [ + 22, + 19, + 16, + 18, + 21, + ], + [ + 18, + 19, + 16, + 22, + 21, + ], + [ + 19, + 18, + 16, + 22, + 21, + ], + [ + 16, + 18, + 19, + 22, + 21, + ], + [ + 18, + 16, + 19, + 22, + 21, + ], + [ + 19, + 16, + 18, + 22, + 21, + ], + [ + 16, + 19, + 18, + 22, + 21, + ], + [ + 21, + 19, + 18, + 22, + 16, + ], + [ + 19, + 21, + 18, + 22, + 16, + ], + [ + 18, + 21, + 19, + 22, + 16, + ], + [ + 21, + 18, + 19, + 22, + 16, + ], + [ + 19, + 18, + 21, + 22, + 16, + ], + [ + 18, + 19, + 21, + 22, + 16, + ], + [ + 18, + 19, + 22, + 21, + 16, + ], + [ + 19, + 18, + 22, + 21, + 16, + ], + [ + 22, + 18, + 19, + 21, + 16, + ], + [ + 18, + 22, + 19, + 21, + 16, + ], + [ + 19, + 22, + 18, + 21, + 16, + ], + [ + 22, + 19, + 18, + 21, + 16, + ], + [ + 22, + 21, + 18, + 19, + 16, + ], + [ + 21, + 22, + 18, + 19, + 16, + ], + [ + 18, + 22, + 21, + 19, + 16, + ], + [ + 22, + 18, + 21, + 19, + 16, + ], + [ + 21, + 18, + 22, + 19, + 16, + ], + [ + 18, + 21, + 22, + 19, + 16, + ], + [ + 19, + 21, + 22, + 18, + 16, + ], + [ + 21, + 19, + 22, + 18, + 16, + ], + [ + 22, + 19, + 21, + 18, + 16, + ], + [ + 19, + 22, + 21, + 18, + 16, + ], + [ + 21, + 22, + 19, + 18, + 16, + ], + 22, + 21, + 19, + 18, + 16, + ] + +> take while + + Chain { + val: [ + 1, + 2, + ], + --- + 1, + 2, + } + +> take last while + + Chain { + val: [ + 4, + 5, + 6, + 7, + 8, + 9, + ], + --- + 4, + 5, + 6, + 7, + 8, + 9, + } + +> zip + + [ + Chain { + val: [ + 1, + 2, + ], + --- + 1, + 2, + }, + Chain { + val: [ + 2, + 1, + ], + --- + 2, + 1, + }, + ] + +> maxBy + + 9 + +> maxOf + + 27 + +> minBy + + 1 + +> minOf + + 3 + +> partition + + Chain { + val: [ + Chain { + val: [ + 1, + 3, + 5, + 7, + 9, + ], + --- + 1, + 3, + 5, + 7, + 9, + }, + Chain { + val: [ + 2, + 4, + 6, + 8, + ], + --- + 2, + 4, + 6, + 8, + }, + ], + --- + Chain { + val: [ + 1, + 3, + 5, + 7, + 9, + ], + --- + 1, + 3, + 5, + 7, + 9, + }, + Chain { + val: [ + 2, + 4, + 6, + 8, + ], + --- + 2, + 4, + 6, + 8, + }, + } + +> first + + 1 + +> firstNotNullishOf + + 3 + +> last + + 9 + +> findIndex + + 2 + +> findSingle 1 + + undefined + +> findSingle 2 + + 3 + +> indexOf + + 2 + +> concat + + 11 + +> intersect + + Chain { + val: [ + 1, + 2, + 3, + ], + --- + 1, + 2, + 3, + } + +> join + + '1,2,3,4,5,6,7,8,9' + +> mapJoin + + '1^,2^,3^,4^,5^,6^,7^,8^,9^' diff --git a/src/collections/AsyncChain.test.ts.snap b/src/collections/AsyncChain.test.ts.snap new file mode 100644 index 0000000..5fe266b Binary files /dev/null and b/src/collections/AsyncChain.test.ts.snap differ diff --git a/src/collections/AsyncChain.ts b/src/collections/AsyncChain.ts new file mode 100644 index 0000000..574673e --- /dev/null +++ b/src/collections/AsyncChain.ts @@ -0,0 +1,1578 @@ +import { + chain, + Chain, + IfString, + objChain, + ObjectChain, + PairSplit, +} from "../collections" +import { + chunk, + intersect, + slidingWindows, +} from "@opencreek/deno-std-collections" +import { error } from "../error" + +export type ArrayOrAsyncChain = Chain | ReadonlyArray | AsyncChain + +export function asyncChain(val: Chain | T>): AsyncChain +export function asyncChain(val: ReadonlyArray | T>): AsyncChain +export function asyncChain( + val: ReadonlyArray | T> | Chain | T>, +): AsyncChain { + if (val instanceof Chain) return new SimpleAsyncChain(val.value()) + return new SimpleAsyncChain(val) +} + +export abstract class AsyncChain implements Promise> { + private _value: Promise> | null + + protected constructor() { + this._value = null + } + + protected startCalculation() { + this._value = this._value ?? this.calculate() + } + + [Symbol.toStringTag] = "AsyncChain" + + async then, TResult2 = never>( + onfulfilled?: + | ((value: Chain) => TResult1 | PromiseLike) + | null + | undefined, + onrejected?: + | ((reason: any) => TResult2 | PromiseLike) + | null + | undefined, + ): Promise { + return await this.await().then(onfulfilled, onrejected) + } + + async catch( + onrejected?: + | ((reason: any) => TResult | PromiseLike) + | null + | undefined, + ): Promise | TResult> { + return this.then((it) => it, onrejected) + } + + finally(onfinally?: (() => void) | null | undefined): Promise> { + return this.then( + (it) => it, + (it) => it, + ).finally(onfinally) + } + + abstract calculate(): Promise> + + async await(): Promise> { + void this.startCalculation() + return (await this._value) ?? error("No promise after starting calculation") + } + + async value(): Promise> { + return (await this.await()).value() + } + + async writableValue(): Promise> { + return (await this.await()).writableValue() + } + + associateBy( + selector: (el: T) => S | Promise, + ): AsyncObjectChain { + return new AssociatingAsyncObjectChain(this, selector) + } + + associateWith( + selector: (key: string) => U, + ): IfString> { + const entries = this.map((el) => { + return [ + el as unknown as string, + selector(el as unknown as string), + ] as const + }) + return objChain(entries) as IfString> + } + + chunk(size: number): AsyncChain { + return new ChunkingAsyncChain(this, size) + } + + concat(other: Iterable | AsyncChain): AsyncChain { + return new ConcatenatingAsyncChain(this, other) + } + + distinct(): AsyncChain { + return this.distinctBy((it) => it) + } + + distinctBy(selector: (el: T) => D): AsyncChain { + return new DistinctAsyncChain(this, selector) + } + + drop(num: number): AsyncChain { + return new SliceAsyncChain(this, num) + } + + dropLast(num: number): AsyncChain { + return new SliceAsyncChain(this, 0, -num) + } + + take(num: number): AsyncChain { + return new SliceAsyncChain(this, 0, num) + } + + takeLast(num: number): AsyncChain { + return new SliceAsyncChain(this, -num) + } + + dropLastWhile( + predicate: (el: T) => Promise | boolean, + ): AsyncChain { + return new DropLastWhileAsyncChain(this, predicate) + } + + dropWhile(predicate: (el: T) => Promise | boolean): AsyncChain { + return new DropWhileAsyncChain(this, predicate) + } + + async every( + predicate: ( + el: T, + index: number, + array: ReadonlyArray, + ) => Promise | boolean, + ): Promise + async every( + predicate: ( + el: T, + index: number, + array: ReadonlyArray, + ) => Promise | boolean, + ): Promise { + const val = await this.value() + for (let i = 0; i < val.length; i++) { + if (!(await predicate(val[i], i, val))) { + return false + } + } + return true + } + + async some( + predicate: ( + value: T, + index: number, + array: ReadonlyArray, + ) => Promise | boolean, + ): Promise { + const val = await this.value() + for (let i = 0; i < val.length; i++) { + if (await predicate(val[i], i, val)) { + return true + } + } + return false + } + + async first(): Promise { + return (await this.firstOrNull()) ?? error("No first element found") + } + + async firstOrNull(): Promise { + return (await this.await()).firstOrNull() + } + + filter( + filter: (el: T, index: number, array: ReadonlyArray) => el is S, + ): AsyncChain + filter( + filter: (el: T, index: number, array: ReadonlyArray) => Promise, + ): AsyncChain + filter( + filter: (el: T, index: number, array: ReadonlyArray) => Promise, + ): AsyncChain + filter( + filter: ( + el: T, + index: number, + array: ReadonlyArray, + ) => Promise | boolean, + ): AsyncChain + filter( + filter: ( + el: T, + index: number, + array: ReadonlyArray, + ) => Promise | boolean, + ): AsyncChain { + return new FilterAsyncChain(this, filter) + } + + filterNotNullish(): AsyncChain> { + return this.filter((it) => it != null) as AsyncChain> + } + + async find( + predicate: (el: T, index: number, array: ReadonlyArray) => el is S, + ): Promise + async find( + predicate: ( + el: T, + index: number, + array: ReadonlyArray, + ) => Promise | boolean, + ): Promise + async find( + predicate: ( + el: T, + index: number, + array: ReadonlyArray, + ) => Promise | boolean, + ): Promise { + const index = await this.findIndex(predicate) + if (index == undefined) return undefined + return (await this.value())[index] + } + + async findIndex( + predicate: ( + el: T, + index: number, + array: ReadonlyArray, + ) => Promise | boolean, + ): Promise { + const val = await this.value() + for (let i = 0; i < val.length; i++) { + if (await predicate(val[i], i, val)) { + return i + } + } + return undefined + } + + async findLast( + predicate: (el: T) => Promise | boolean, + ): Promise { + const index = await this.findLastIndex(predicate) + if (index == undefined) return undefined + return (await this.value())[index] + } + + async findLastIndex( + predicate: (el: T) => Promise | boolean, + ): Promise { + const val = await this.value() + for (let i = val.length - 1; i >= 0; i--) { + if (await predicate(val[i])) { + return i + } + } + return undefined + } + + async findSingle( + predicate: (el: T) => Promise | boolean, + ): Promise { + const first = await this.findIndex(predicate) + if (first == undefined) return undefined + + const last = await this.findLastIndex(predicate) + + if (first !== last) return undefined + + return (await this.value())[last] + } + + async firstNotNullishOf( + selector: (item: T) => Promise | O | undefined | null, + ): Promise | undefined> { + const val = await this.value() + for (let i = 0; i < val.length; i++) { + const res = await selector(val[i]) + if (res != null) return res + } + return undefined + } + + flatten(): FlattenAsyncChain { + return new FlattenAsyncChain(this) + } + + flatMap( + transformer: (el: T, index: number, array: ReadonlyArray) => Chain, + ): FlattenAsyncChain + flatMap( + transformer: ( + el: T, + index: number, + array: ReadonlyArray, + ) => Promise> | ReadonlyArray, + ): FlattenAsyncChain + flatMap( + transformer: (el: T, index: number, array: ReadonlyArray) => Set, + ): FlattenAsyncChain + flatMap( + transformer: ( + el: T, + index: number, + array: ReadonlyArray, + ) => AsyncChain, + ): FlattenAsyncChain + flatMap( + transformer: ( + el: T, + index: number, + array: ReadonlyArray, + ) => Chain | ReadonlyArray | Set | AsyncChain, + ): FlattenAsyncChain + flatMap( + transformer: ( + el: T, + index: number, + array: ReadonlyArray, + ) => + | Chain + | ReadonlyArray + | Set + | AsyncChain + | Promise>, + ): AsyncChain { + const mapped = this.map(transformer) + return mapped.flatten() as unknown as AsyncChain + } + + async forEach( + callback: ( + el: T, + index: number, + array: ReadonlyArray, + ) => void | Promise, + ): Promise { + await this.map(callback).value() + } + + groupBy( + selector: (el: T) => K | Promise, + ): AsyncObjectChain> { + return new GroupingAsyncObjectChain(this, selector) + } + + async indexOf( + searchElement: T, + fromIndex?: number, + ): Promise { + return (await this.await()).indexOf(searchElement, fromIndex) + } + + async lastIndexOf( + searchElement: T, + fromIndex?: number, + ): Promise { + return (await this.await()).lastIndexOf(searchElement, fromIndex) + } + + async includes(el: T, fromIndex?: number): Promise { + return (await this.await()).includes(el, fromIndex) + } + + intersect(...arrays: Array | AsyncChain>): AsyncChain { + return new IntersectionAsyncChain(this, arrays) + } + + async join(separator?: string): Promise { + return (await this.await()).join(separator) + } + + async mapJoin( + separator: string, + transformer: (el: T) => string | Promise, + ): Promise { + return this.reduce(async (acc, it, idx) => { + const mapped = transformer(it) + return acc + (idx > 0 ? separator : "") + mapped + }, "") + } + + async last(): Promise { + return (await this.lastOrNull()) ?? error("No last element found") + } + + async lastOrNull(): Promise { + return (await this.await()).lastOrNull() + } + + map( + transformer: ( + el: T, + index: number, + array: ReadonlyArray, + ) => U | Promise, + ): AsyncChain { + return new MappingAsyncChain(this, transformer) + } + + mapNotNullish( + transformer: (el: T) => Promise | O, + ): AsyncChain> { + return this.map(transformer).filterNotNullish() + } + + async maxBy( + selector: (el: T) => Promise | string, + ): Promise + async maxBy( + selector: (el: T) => Promise | bigint, + ): Promise + async maxBy( + selector: (el: T) => Promise | number, + ): Promise + async maxBy(selector: (el: T) => Promise | Date): Promise + async maxBy( + selector: ( + el: T, + ) => + | Date + | number + | bigint + | string + | Promise, + ): Promise { + let max: number | string | bigint | Date | undefined + let ret: T | undefined + + for (const elem of await this.value()) { + const elemValue = await selector(elem) + if (max == null || elemValue > max) { + max = elemValue + ret = elem + } + } + return ret + } + + async maxOf( + selector: (el: T) => Promise | bigint, + ): Promise + async maxOf( + selector: (el: T) => Promise | number, + ): Promise + async maxOf( + selector: (el: T) => Promise | string, + ): Promise + async maxOf( + selector: (el: T) => Promise | Date, + ): Promise + async maxOf( + selector: ( + el: T, + ) => + | bigint + | number + | string + | Date + | Promise, + ): Promise { + return ( + (await this.map(selector)) + // We need to cast, because of the overloads. We know it's safe because of our overloads though + .maxOf((it) => it as string) + ) + } + + async maxWith(comparator: (a: T, b: T) => number): Promise { + return (await this.await()).maxWith(comparator) + } + + async minBy( + selector: (el: T) => Promise | string, + ): Promise + async minBy( + selector: (el: T) => Promise | bigint, + ): Promise + async minBy( + selector: (el: T) => Promise | number, + ): Promise + async minBy(selector: (el: T) => Promise | Date): Promise + async minBy( + selector: ( + el: T, + ) => + | Date + | number + | bigint + | string + | Promise, + ): Promise { + let min: number | string | bigint | Date | undefined + let ret: T | undefined + + for (const elem of await this.value()) { + const elemValue = await selector(elem) + if (min == null || elemValue < min) { + min = elemValue + ret = elem + } + } + return ret + } + + async minOf( + selector: (el: T) => Promise | bigint, + ): Promise + async minOf( + selector: (el: T) => Promise | number, + ): Promise + async minOf( + selector: (el: T) => Promise | string, + ): Promise + async minOf( + selector: (el: T) => Promise | Date, + ): Promise + async minOf( + selector: ( + el: T, + ) => + | bigint + | number + | string + | Date + | Promise, + ): Promise { + return ( + (await this.map(selector)) + // We need to cast, because of the overloads. We know it's safe because of our overloads though + .minOf((it) => it as string) + ) + } + + async minWith(comparator: (a: T, b: T) => number): Promise { + return (await this.await()).minWith(comparator) + } + + partition( + predicate: (el: T) => Promise | boolean, + ): [AsyncChain, AsyncChain] { + return [ + this.filter(predicate), + this.filter(async (el) => !(await predicate(el))), + ] + } + + permutations(): AsyncChain> { + return new PermutationsAsyncChain(this) + } + + async reduce( + reducer: ( + accumulator: AsyncChain, + current: T, + index: number, + array: ReadonlyArray, + ) => AsyncChain, + ): Promise> + async reduce( + reducer: ( + accumulator: T, + current: T, + index: number, + array: ReadonlyArray, + ) => T, + ): Promise + async reduce( + reducer: ( + accumulator: T, + current: T, + index: number, + array: ReadonlyArray, + ) => Promise, + ): Promise + async reduce( + reducer: ( + accumulator: T, + current: T, + index: number, + array: ReadonlyArray, + ) => Promise | T, + ): Promise + async reduce( + reducer: ( + accumulator: T, + current: T, + index: number, + array: ReadonlyArray, + ) => T, + initial: T, + ): Promise + async reduce( + reducer: ( + accumulator: T, + current: T, + index: number, + array: ReadonlyArray, + ) => Promise, + initial: T, + ): Promise + async reduce( + reducer: ( + accumulator: T, + current: T, + index: number, + array: ReadonlyArray, + ) => Promise | T, + initial: T, + ): Promise + async reduce( + reducer: ( + accumulator: O, + current: T, + index: number, + array: ReadonlyArray, + ) => O, + initial: O, + ): Promise + async reduce( + reducer: ( + accumulator: O, + current: T, + index: number, + array: ReadonlyArray, + ) => Promise, + initial: O, + ): Promise + async reduce( + reducer: ( + accumulator: O, + current: T, + index: number, + array: ReadonlyArray, + ) => O | Promise, + initial: O, + ): Promise + async reduce( + reducer: ( + accumulator: O, + current: T, + index: number, + array: ReadonlyArray, + ) => O | Promise, + initial?: O, + ): Promise + async reduce( + reducer: ( + accumulator: O, + current: T, + index: number, + array: ReadonlyArray, + ) => O | Promise, + initial?: O, + ): Promise { + let ret: O | undefined = + initial != null ? initial : ((await this.firstOrNull()) as O | undefined) + + const values = await this.value() + for (let i = initial != null ? 0 : 1; i < values.length; i++) { + ret = await reducer(ret as O, values[i], i, values) + } + return ret + } + + async reduceRight( + reducer: ( + accumulator: T, + current: T, + index: number, + array: ReadonlyArray, + ) => T, + ): Promise + async reduceRight( + reducer: ( + accumulator: T, + current: T, + index: number, + array: ReadonlyArray, + ) => Promise, + ): Promise + async reduceRight( + reducer: ( + accumulator: T, + current: T, + index: number, + array: ReadonlyArray, + ) => Promise | T, + ): Promise + async reduceRight( + reducer: ( + accumulator: T, + current: T, + index: number, + array: ReadonlyArray, + ) => T, + initial: T, + ): Promise + async reduceRight( + reducer: ( + accumulator: T, + current: T, + index: number, + array: ReadonlyArray, + ) => Promise, + initial: T, + ): Promise + async reduceRight( + reducer: ( + accumulator: T, + current: T, + index: number, + array: ReadonlyArray, + ) => Promise | T, + initial: T, + ): Promise + async reduceRight( + reducer: ( + accumulator: O, + current: T, + index: number, + array: ReadonlyArray, + ) => O, + initial: O, + ): Promise + async reduceRight( + reducer: ( + accumulator: O, + current: T, + index: number, + array: ReadonlyArray, + ) => Promise, + initial: O, + ): Promise + async reduceRight( + reducer: ( + accumulator: O, + current: T, + index: number, + array: ReadonlyArray, + ) => O | Promise, + initial: O, + ): Promise + async reduceRight( + reducer: ( + accumulator: O, + current: T, + index: number, + array: ReadonlyArray, + ) => O | Promise, + initial?: O, + ): Promise + async reduceRight( + reducer: ( + accumulator: O, + current: T, + index: number, + array: ReadonlyArray, + ) => O | Promise, + initial?: O, + ): Promise { + return await this.reverse().reduce(reducer, initial) + } + + reverse(): AsyncChain { + return new ReversingAsyncChain(this) + } + + runningReduce( + reducer: (accumulator: O, current: T) => O, + initialValue: O, + ): AsyncChain { + return new RunningReduceAsyncChain(this, reducer, initialValue) + } + + async sample(): Promise { + return (await this.await()).sample() + } + + slice(start?: number, end?: number): AsyncChain { + return new SliceAsyncChain(this, start, end) + } + + slidingWindows( + size: number, + { step, partial }: { step: number; partial: boolean }, + ): AsyncChain> { + return new SlidingWindowAsyncChain(this, size, { step, partial }) + } + + sort(compareFn?: (a: T, b: T) => number): AsyncChain { + return new SortingAsyncChain(this, compareFn) + } + + sortBy(selector: (el: T) => Date): AsyncChain + sortBy(selector: (el: T) => Promise): AsyncChain + sortBy(selector: (el: T) => Promise | Date): AsyncChain + sortBy(selector: (el: T) => bigint): AsyncChain + sortBy(selector: (el: T) => Promise): AsyncChain + sortBy(selector: (el: T) => Promise | bigint): AsyncChain + sortBy(selector: (el: T) => string): AsyncChain + sortBy(selector: (el: T) => Promise): AsyncChain + sortBy(selector: (el: T) => Promise | string): AsyncChain + sortBy(selector: (el: T) => number): AsyncChain + sortBy(selector: (el: T) => Promise): AsyncChain + sortBy(selector: (el: T) => Promise | number): AsyncChain + sortBy(selector: (el: T) => Date | bigint | string | number): AsyncChain + sortBy( + selector: (el: T) => Promise, + ): AsyncChain + sortBy( + selector: ( + el: T, + ) => + | Promise + | Date + | bigint + | string + | number, + ): AsyncChain + sortBy( + selector: ( + el: T, + ) => + | Promise + | Date + | bigint + | string + | number, + ): AsyncChain { + return new SortByAsyncChain(this, selector) + } + + async sumOf(selector: (el: T) => Promise | number): Promise { + return (await this.map(selector).await()).sumOf((it) => it) + } + + takeLastWhile( + predicate: (el: T) => Promise | boolean, + ): AsyncChain { + return new TakeLastWhileAsyncChain(this, predicate) + } + + takeWhile(predicate: (el: T) => Promise | boolean): AsyncChain { + return new TakeWhileAsyncChain(this, predicate) + } + + union(...arrays: (readonly T[] | AsyncChain)[]): AsyncChain { + return new UnionAsyncChain(this, arrays) + } + + async unzip(): Promise> { + return (await this.await()).unzip() + } + + withoutAll(values: readonly T[] | Chain | AsyncChain): AsyncChain { + return new WithoutAllAsyncChain(this, values) + } + + zip(withArray: readonly U[] | AsyncChain): AsyncChain<[T, U]> { + return new ZippingAsyncChain(this, withArray) + } +} + +export class SimpleAsyncChain extends AsyncChain { + constructor(private val: ReadonlyArray | T>) { + super() + this.startCalculation() + } + + async calculate(): Promise> { + return new Chain(await Promise.all(this.val)) + } +} + +export class ChunkingAsyncChain extends AsyncChain { + constructor( + private val: AsyncChain, + private size: number, + ) { + super() + this.startCalculation() + } + + async calculate(): Promise> { + const chunks = chunk(await this.val.value(), this.size) + return new Chain(chunks) + } +} + +export class ConcatenatingAsyncChain extends AsyncChain { + constructor( + private val: AsyncChain, + private other: AsyncChain | Iterable, + ) { + super() + this.startCalculation() + } + + async calculate(): Promise> { + const adding = + this.other instanceof AsyncChain ? await this.other.await() : this.other + return (await this.val.await()).concat(adding) + } +} + +export class DistinctAsyncChain extends AsyncChain { + constructor( + private val: AsyncChain, + private selector: (el: T) => D, + ) { + super() + this.startCalculation() + } + + async calculate(): Promise> { + return (await this.val.await()).distinctBy(this.selector) + } +} + +export class DropWhileAsyncChain extends AsyncChain { + constructor( + private val: AsyncChain, + private predicate: ( + el: T, + index: number, + array: ReadonlyArray, + ) => Promise | boolean, + ) { + super() + this.startCalculation() + } + + async calculate(): Promise> { + const values = await this.val.await() + let count = 0 + for (let i = 0; i < values.value().length; i++) { + if (!(await this.predicate(values.value()[i], i, values.value()))) { + break + } + count++ + } + + return values.drop(count) + } +} + +export class DropLastWhileAsyncChain extends AsyncChain { + constructor( + private val: AsyncChain, + private predicate: ( + el: T, + index: number, + array: ReadonlyArray, + ) => Promise | boolean, + ) { + super() + this.startCalculation() + } + + async calculate(): Promise> { + const values = await this.val.await() + let count = 0 + for (let i = values.value().length - 1; i >= 0; i--) { + if (!(await this.predicate(values.value()[i], i, values.value()))) { + break + } + count++ + } + + return values.dropLast(count) + } +} + +export class EntriesAsyncChain< + K extends string | number | symbol, + T, + Rec extends Partial> = Record, +> extends AsyncChain<[K, T]> { + constructor(private val: AsyncObjectChain) { + super() + } + + async calculate(): Promise> { + return (await this.val.await()).entries() + } +} + +export class FilterAsyncChain extends AsyncChain { + constructor( + private val: AsyncChain, + private predicate: ( + el: T, + index: number, + array: ReadonlyArray, + ) => Promise | boolean, + ) { + super() + this.startCalculation() + } + + async calculate(): Promise> { + const current = await this.val.await() + const mask = await asyncChain(current).map(this.predicate).value() + + return current.filter((_, index) => mask[index]) + } +} + +type Distribute = U extends any ? { type: U } : never + +type FlattenAsyncType = + Distribute extends { + type: ArrayOrAsyncChain + } + ? U + : T + +export class FlattenAsyncChain extends AsyncChain> { + constructor( + private val: + | AsyncChain + | AsyncChain> + | AsyncChain> + | AsyncChain>, + ) { + super() + this.startCalculation() + } + + async calculate(): Promise>> { + const flattened = await this.val.map(async (el) => + el instanceof AsyncChain + ? await el.value() + : el instanceof Chain + ? el.value() + : el, + ) + + return flattened.flatten() as Chain> + } +} + +export class IntersectionAsyncChain extends AsyncChain { + constructor( + private val: AsyncChain, + private withArrays: ReadonlyArray>, + ) { + super() + this.startCalculation() + } + + async calculate(): Promise> { + const others = await Promise.all( + this.withArrays.map(async (it) => + it instanceof AsyncChain ? await it.value() : it, + ), + ) + const ret = intersect(await this.val.value(), ...others) + return chain(ret) + } +} + +export class KeysAsyncChain< + K extends string | number | symbol, +> extends AsyncChain { + constructor(private val: AsyncObjectChain) { + super() + } + + async calculate(): Promise> { + return (await this.val.await()).keys() + } +} + +export class MappingAsyncChain extends AsyncChain { + constructor( + private val: AsyncChain, + private transformer: ( + el: T, + index: number, + array: ReadonlyArray, + ) => U | Promise, + ) { + super() + this.startCalculation() + } + + async calculate(): Promise> { + const entries = (await this.val.await()).map( + async (el, index, array) => + await this.transformer(await el, index, array), + ) + + return await asyncChain(entries).await() + } +} + +export class PermutationsAsyncChain extends AsyncChain> { + constructor(private val: AsyncChain) { + super() + this.startCalculation() + } + + async calculate(): Promise>> { + return (await this.val.await()).permutations() + } +} + +export class ReversingAsyncChain extends AsyncChain { + constructor(private val: AsyncChain) { + super() + this.startCalculation() + } + + async calculate(): Promise> { + return (await this.val.await()).reverse() + } +} + +export class RunningReduceAsyncChain extends AsyncChain { + constructor( + private val: AsyncChain, + private reducer: (accumulator: O, current: T) => Promise | O, + private initialValue: O, + ) { + super() + this.startCalculation() + } + + async calculate(): Promise> { + const ret = await this.val.reduce( + async (acc, it) => { + const elem = acc[acc.length - 1] + const next = await this.reducer(elem, it) + return [...acc, next] + }, + [this.initialValue], + ) + + return chain(ret) + } +} + +export class SliceAsyncChain extends AsyncChain { + constructor( + private val: AsyncChain, + private start?: number, + private end?: number, + ) { + super() + this.startCalculation() + } + + async calculate(): Promise> { + return (await this.val.await()).slice(this.start, this.end) + } +} + +export class SlidingWindowAsyncChain extends AsyncChain> { + constructor( + private val: AsyncChain, + private size: number, + private options: { + step?: number + partial?: boolean + }, + ) { + super() + this.startCalculation() + } + + async calculate(): Promise>> { + const ret = slidingWindows(await this.val.value(), this.size, this.options) + return new Chain(ret) + } +} + +export class SortByAsyncChain extends AsyncChain { + constructor( + private val: AsyncChain, + private selector: ( + a: T, + ) => + | Promise + | Date + | bigint + | string + | number, + ) { + super() + this.startCalculation() + } + + async calculate(): Promise> { + const values = await this.val + .map(async (el) => [el, await this.selector(el)] as const) + .await() + return values.sortBy((it) => it[1]).map((it) => it[0]) + } +} + +export class SortingAsyncChain extends AsyncChain { + constructor( + private val: AsyncChain, + private compareFn?: (a: T, b: T) => number, + ) { + super() + this.startCalculation() + } + + async calculate(): Promise> { + return (await this.val.await()).sort(this.compareFn) + } +} + +export class TakeWhileAsyncChain extends AsyncChain { + constructor( + private val: AsyncChain, + private predicate: ( + el: T, + index: number, + array: ReadonlyArray, + ) => Promise | boolean, + ) { + super() + this.startCalculation() + } + + async calculate(): Promise> { + const values = await this.val.value() + const ret: Array = [] + for (let i = 0; i < values.length; i++) { + if (await this.predicate(values[i], i, values)) { + ret.push(values[i]) + } else { + break + } + } + + return new Chain(ret) + } +} + +export class TakeLastWhileAsyncChain extends AsyncChain { + constructor( + private val: AsyncChain, + private predicate: ( + el: T, + index: number, + array: ReadonlyArray, + ) => Promise | boolean, + ) { + super() + this.startCalculation() + } + + async calculate(): Promise> { + const values = await this.val.value() + const ret: Array = [] + for (let i = values.length - 1; i >= 0; i--) { + if (await this.predicate(values[i], i, values)) { + ret.push(values[i]) + } else { + break + } + } + + return new Chain(ret.reverse()) + } +} + +export class UnionAsyncChain extends AsyncChain { + constructor( + private val: AsyncChain, + private withArrays: ReadonlyArray>, + ) { + super() + this.startCalculation() + } + + async calculate(): Promise> { + const other = await Promise.all( + this.withArrays.map(async (it) => + it instanceof AsyncChain ? await it.value() : it, + ), + ) + + return (await this.val.await()).union(...other) + } +} + +export class ValuesAsyncChain< + K extends string | number | symbol, + T, + Rec extends Partial> = Record, +> extends AsyncChain { + constructor(private val: AsyncObjectChain) { + super() + } + + async calculate(): Promise> { + return (await this.val.await()).values() + } +} + +export class ZippingAsyncChain extends AsyncChain<[T, U]> { + constructor( + private val: AsyncChain, + private withArray: readonly U[] | AsyncChain, + ) { + super() + this.startCalculation() + } + + async calculate(): Promise> { + const other = + this.withArray instanceof AsyncChain + ? await this.withArray.value() + : this.withArray + return (await this.val.await()).zip(other) + } +} + +export class WithoutAllAsyncChain extends AsyncChain { + constructor( + private val: AsyncChain, + private without: readonly T[] | AsyncChain | Chain, + ) { + super() + this.startCalculation() + } + + async calculate(): Promise> { + const other = + this.without instanceof AsyncChain + ? await this.without.value() + : this.without instanceof Chain + ? this.without.value() + : this.without + return (await this.val.await()).withoutAll(other) + } +} + +// ********************* +// Async Object +// ********************* +export abstract class AsyncObjectChain< + K extends string | number | symbol, + T, + Rec extends Partial> = Record, +> implements Promise> +{ + private _value: Promise> | null + + protected startCalculation() { + this._value = this.calculate() + } + + [Symbol.toStringTag] = "AsyncObjectChain" + + constructor() { + this._value = null + } + + abstract calculate(): Promise> + + async await(): Promise> { + void this.startCalculation() + return (await this._value) ?? error("No promise after starting calculation") + } + + async value(): Promise { + return (await this.await()).value() + } + + async then, TResult2 = never>( + onfulfilled?: + | ((value: ObjectChain) => TResult1 | PromiseLike) + | null + | undefined, + onrejected?: + | ((reason: any) => TResult2 | PromiseLike) + | null + | undefined, + ): Promise { + return await this.calculate().then(onfulfilled, onrejected) + } + + async catch( + onrejected?: + | ((reason: any) => TResult | PromiseLike) + | null + | undefined, + ): Promise | TResult> { + return this.then((it) => it, onrejected) + } + + finally( + onfinally?: (() => void) | null | undefined, + ): Promise> { + return this.then( + (it) => it, + (it) => it, + ).finally(onfinally) + } + + keys(): AsyncChain { + return new KeysAsyncChain(this) + } + + values(): AsyncChain { + return new ValuesAsyncChain(this) + } + + entries(): AsyncChain<[K, T]> { + return new EntriesAsyncChain(this) + } + + mapKeys( + transformer: (key: K) => Promise | U, + ): AsyncObjectChain { + return new MappingEntriesAsyncObjectChain( + this, + async (k, v) => [await transformer(k), v], + ) + } + + mapValues( + transformer: (value: T) => Promise | V, + ): AsyncObjectChain { + return new MappingEntriesAsyncObjectChain( + this, + async (k, v) => [k, await transformer(v)], + ) + } + + mapEntries( + transformer: (key: K, value: T) => Promise<[U, V]> | [U, V], + ): AsyncObjectChain { + return new MappingEntriesAsyncObjectChain( + this, + transformer, + ) + } + + filterKeys( + filter: (key: K) => Promise | boolean, + ): AsyncObjectChain { + return new FilteringAsyncObjectChain(this, (k, _) => filter(k)) + } + + filterValues( + filter: (value: T) => Promise | boolean, + ): AsyncObjectChain { + return new FilteringAsyncObjectChain(this, (_, v) => filter(v)) + } + + filterEntries( + filter: (key: K, value: T) => Promise | boolean, + ): AsyncObjectChain { + return new FilteringAsyncObjectChain(this, filter) + } +} + +export class MappingEntriesAsyncObjectChain< + K extends string | number | symbol, + T, + U extends string | number | symbol, + V, + Rec extends Partial> = Record, +> extends AsyncObjectChain { + constructor( + private val: AsyncObjectChain, + private transformer: (key: K, value: T) => [U, V] | Promise<[U, V]>, + ) { + super() + } + + async calculate(): Promise> { + const values = await this.val + .entries() + .map(async ([k, v]) => await this.transformer(k, v)) + + return objChain(values) + } +} + +export class AssociatingAsyncObjectChain< + K extends string | number | symbol, + T, +> extends AsyncObjectChain { + constructor( + private val: AsyncChain, + private selector: (elem: T) => K | Promise, + ) { + super() + this.startCalculation() + } + + async calculate(): Promise> { + const entries = await this.val.map( + async (it) => [await this.selector(it), it] as const, + ) + + return objChain(entries) + } +} + +export class GroupingAsyncObjectChain< + K extends string | number | symbol, + T, +> extends AsyncObjectChain> { + constructor( + private val: AsyncChain, + private selector: (elem: T) => K | Promise, + ) { + super() + this.startCalculation() + } + + async calculate(): Promise>> { + const entries = await this.val + .map(async (it) => [await this.selector(it), it] as const) + .value() + + const record = {} as Record> + for (const [key, el] of entries) { + if (record[key] != null) { + record[key]?.push(el) + } else { + record[key] = [el] + } + } + + return objChain(record) + } +} + +export class FilteringAsyncObjectChain< + K extends string | number | symbol, + T, + Rec extends Partial> = Record, +> extends AsyncObjectChain { + constructor( + private val: AsyncObjectChain, + private condition: (key: K, value: T) => boolean | Promise, + ) { + super() + } + + async calculate(): Promise> { + const entries = await this.val + .entries() + .filter(async ([k, v]) => await this.condition(k, v)) + + return objChain(entries) + } +} diff --git a/src/extendPrototype.ts b/src/extendPrototype.ts index b243c7d..d537d33 100644 --- a/src/extendPrototype.ts +++ b/src/extendPrototype.ts @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/no-unsafe-function-type */ +/* eslint-disable @typescript-eslint/no-unsafe-function-type */ export function extendProtoype( target: Function, func: Function, diff --git a/src/index.ts b/src/index.ts index 2a24659..c1cb662 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,3 +11,4 @@ export * from "./table" export * from "./StopWatch" export * from "./strings" export * from "./BigDecimal" +export * from "./collections/AsyncChain" diff --git a/src/objects.ts b/src/objects.ts index e89ab94..01cee81 100644 --- a/src/objects.ts +++ b/src/objects.ts @@ -42,10 +42,5 @@ declare global { this: NonNullable, predicate: (thiz: NonNullable) => boolean, ): T | undefined - - mapValues( - this: Readonly>, - transformer: (value: T) => O, - ): Record } }