├── scripts ├── imported │ ├── config.js │ ├── package.json │ └── index.js ├── example.js ├── log-version.js ├── nop │ ├── package.json │ └── index.js ├── correct-import-extensions.js └── post-build.js ├── .npmignore ├── .gitignore ├── src ├── promise.ts ├── split │ ├── index.ts │ ├── type.ts │ └── split.ts ├── tests │ ├── global.ts │ ├── ok.ts │ ├── sleep.ts │ ├── walk.ts │ ├── index.tsx │ ├── input.ts │ ├── main.ts │ ├── sync.ts │ ├── readme.ts │ ├── the-thing.ts │ ├── downstream.ts │ ├── line.ts │ ├── split-fn.ts │ ├── typed.ts │ ├── iterable-lifecycle.ts │ ├── push.ts │ ├── blend.ts │ └── split.ts ├── push │ ├── weak-linked-list.ts │ ├── abstract-linked-list.ts │ └── index.ts ├── index.ts ├── like.ts ├── series.ts ├── defer.ts ├── walk │ └── index.ts ├── is.ts ├── args.ts ├── all.ts ├── the-sync-thing.tsx ├── all-settled.ts ├── the-thing.tsx ├── downstream.ts ├── blend │ └── index.ts ├── line │ └── index.ts └── iterable-lifecycle.ts ├── import-map-deno.json ├── .nycrc ├── CONTRIBUTING.md ├── tsconfig.json ├── .github └── workflows │ ├── test-actions.yml │ ├── release-actions.yml │ └── codeql-analysis.yml ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── LICENSE.md ├── CODE-OF-CONDUCT.md ├── README.md └── package.json /scripts/imported/config.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | node_modules 4 | coverage -------------------------------------------------------------------------------- /scripts/example.js: -------------------------------------------------------------------------------- 1 | import "../esnext/example/index.js"; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | node_modules 4 | esnext 5 | coverage -------------------------------------------------------------------------------- /src/promise.ts: -------------------------------------------------------------------------------- 1 | export * from "./all"; 2 | export * from "./all-settled"; 3 | -------------------------------------------------------------------------------- /src/split/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./split"; 2 | export * from "./type"; 3 | -------------------------------------------------------------------------------- /src/tests/global.ts: -------------------------------------------------------------------------------- 1 | const GlobalPromise = Promise; 2 | 3 | export { GlobalPromise, GlobalPromise as Promise }; 4 | -------------------------------------------------------------------------------- /import-map-deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "@virtualstate/union": "https://cdn.skypack.dev/@virtualstate/union" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /scripts/log-version.js: -------------------------------------------------------------------------------- 1 | import("fs") 2 | .then(({ promises }) => promises.readFile("package.json")) 3 | .then(JSON.parse) 4 | .then(({ version }) => console.log(version)); 5 | -------------------------------------------------------------------------------- /src/tests/ok.ts: -------------------------------------------------------------------------------- 1 | import { ok } from "../like"; 2 | 3 | { 4 | let caught; 5 | try { 6 | ok(false); 7 | } catch (reason) { 8 | caught = reason; 9 | } 10 | ok(caught); 11 | } 12 | -------------------------------------------------------------------------------- /scripts/nop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "main": "./index.js", 4 | "type": "module", 5 | "exports": { 6 | ".": "./index.js", 7 | "./": "./index.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /scripts/imported/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "main": "./index.js", 4 | "type": "module", 5 | "exports": { 6 | ".": "./index.js", 7 | "./": "./index.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | 4 | ], 5 | "reporter": [ 6 | "clover", 7 | "json-summary", 8 | "html", 9 | "text-summary" 10 | ], 11 | "branches": 80, 12 | "lines": 80, 13 | "functions": 80, 14 | "statements": 80 15 | } 16 | -------------------------------------------------------------------------------- /src/push/weak-linked-list.ts: -------------------------------------------------------------------------------- 1 | import { AbstractLinkedList } from "./abstract-linked-list"; 2 | 3 | export class WeakLinkedList extends AbstractLinkedList { 4 | constructor() { 5 | super(); 6 | } 7 | clear() { 8 | this.map = new WeakMap(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/tests/sleep.ts: -------------------------------------------------------------------------------- 1 | export function sleep(timeout: number, arg: T): Promise; 2 | export function sleep(timeout: number): Promise; 3 | export function sleep(timeout: number, arg?: T) { 4 | return new Promise((resolve) => 5 | setTimeout(() => resolve(arg), timeout) 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /scripts/nop/index.js: -------------------------------------------------------------------------------- 1 | export const listenAndServe = undefined; 2 | export const createServer = undefined; 3 | 4 | /** 5 | * @type {any} 6 | */ 7 | const on = () => {}; 8 | /** 9 | * @type {any} 10 | */ 11 | const env = {}; 12 | 13 | /*** 14 | * @type {any} 15 | */ 16 | export default { 17 | env, 18 | on, 19 | exit(arg) {}, 20 | }; 21 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as Promise from "./promise"; 2 | 3 | export { Promise }; 4 | 5 | export * from "./promise"; 6 | export * from "./push"; 7 | export * from "./defer"; 8 | export * from "./split"; 9 | export * from "./walk"; 10 | export * from "./series"; 11 | export * from "./blend"; 12 | export * from "./iterable-lifecycle"; 13 | 14 | // Core dependency 15 | export { union, UnionInput, UnionOptions } from "@virtualstate/union"; 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make by way of an issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | We are open to all ideas big or small, and are greatly appreciative of any and all contributions. 7 | 8 | Please note we have a code of conduct, please follow it in all your interactions with the project. 9 | -------------------------------------------------------------------------------- /src/tests/walk.ts: -------------------------------------------------------------------------------- 1 | import { Walk } from "../walk"; 2 | 3 | const walk = new Walk(); 4 | 5 | walk.push({ 6 | async *[Symbol.asyncIterator]() { 7 | yield 1; 8 | yield 2; 9 | }, 10 | }); 11 | walk.push({ 12 | async *[Symbol.asyncIterator]() { 13 | yield 3; 14 | yield 4; 15 | }, 16 | }); 17 | walk.close(); 18 | 19 | let values = []; 20 | for await (const value of walk) { 21 | values.push(value); 22 | } 23 | 24 | console.log({ values }); 25 | -------------------------------------------------------------------------------- /src/like.ts: -------------------------------------------------------------------------------- 1 | export function isLike(value: unknown): value is T { 2 | return !!value; 3 | } 4 | 5 | export function ok(value: unknown, message?: string): asserts value; 6 | export function ok(value: unknown, message?: string): asserts value is T; 7 | export function ok(value: unknown, message?: string): asserts value { 8 | if (!value) { 9 | throw new Error(message ?? "Expected value"); 10 | } 11 | } 12 | 13 | export function no(): never { 14 | throw new Error("Not implemented"); 15 | } 16 | -------------------------------------------------------------------------------- /scripts/imported/index.js: -------------------------------------------------------------------------------- 1 | /* c8 ignore start */ 2 | 3 | const initialImportPath = 4 | getConfig()["@virtualstate/app-history/test/imported/path"] ?? 5 | "@virtualstate/app-history"; 6 | 7 | if (typeof initialImportPath !== "string") 8 | throw new Error("Expected string import path"); 9 | 10 | export const { AppHistory } = await import(initialImportPath); 11 | 12 | export function getConfig() { 13 | return { 14 | ...getNodeConfig(), 15 | }; 16 | } 17 | 18 | function getNodeConfig() { 19 | if (typeof process === "undefined") return {}; 20 | return JSON.parse(process.env.TEST_CONFIG ?? "{}"); 21 | } 22 | /* c8 ignore end */ 23 | -------------------------------------------------------------------------------- /src/series.ts: -------------------------------------------------------------------------------- 1 | export interface AsyncFn { 2 | (): Promise; 3 | } 4 | 5 | export interface Series { 6 | (fn: AsyncFn): Promise; 7 | } 8 | 9 | export function series(): Series { 10 | let promise: Promise | undefined = undefined; 11 | return async (fn) => { 12 | const current = (promise = run(promise)); 13 | return current; 14 | async function run(previous?: Promise) { 15 | if (previous) { 16 | await previous; 17 | } 18 | try { 19 | return await fn(); 20 | } finally { 21 | if (promise === current) { 22 | } 23 | } 24 | } 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/tests/index.tsx: -------------------------------------------------------------------------------- 1 | export default 1; 2 | 3 | try { 4 | await import("./main"); 5 | await import("./ok"); 6 | await import("./the-thing"); 7 | await import("./readme"); 8 | await import("./typed"); 9 | await import("./downstream"); 10 | await import("./sync"); 11 | await import("./push"); 12 | await import("./split"); 13 | await import("./split-fn"); 14 | await import("./walk"); 15 | await import("./iterable-lifecycle"); 16 | await import("./blend"); 17 | await import("./line"); 18 | } catch (error) { 19 | if (error instanceof AggregateError) { 20 | console.error(error.errors); 21 | } 22 | throw error; 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "lib": ["es2018", "esnext"], 5 | "types": ["jest", "node"], 6 | "esModuleInterop": true, 7 | "target": "esnext", 8 | "noImplicitAny": true, 9 | "downlevelIteration": true, 10 | "moduleResolution": "node", 11 | "declaration": true, 12 | "sourceMap": true, 13 | "outDir": "esnext", 14 | "baseUrl": "./src", 15 | "jsx": "react", 16 | "jsxFactory": "h", 17 | "jsxFragmentFactory": "createFragment", 18 | "allowJs": true, 19 | "paths": {} 20 | }, 21 | "include": ["src/**/*"], 22 | "typeRoots": ["./node_modules/@types", "src/types"], 23 | "exclude": ["node_modules/@opennetwork/vdom"] 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/test-actions.yml: -------------------------------------------------------------------------------- 1 | name: test-actions 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | env: 8 | NO_COVERAGE_BADGE_UPDATE: 1 9 | FLAGS: FETCH_SERVICE_DISABLE,POST_CONFIGURE_TEST,PLAYWRIGHT,CONTINUE_ON_ERROR 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 0 14 | - uses: actions/setup-node@v2 15 | with: 16 | node-version: "16.x" 17 | registry-url: "https://registry.npmjs.org" 18 | - uses: denoland/setup-deno@v1 19 | with: 20 | deno-version: "v1.x" 21 | - run: | 22 | yarn install 23 | npx playwright install-deps 24 | - run: yarn build 25 | # yarn coverage === c8 + yarn test 26 | - run: yarn coverage 27 | - run: yarn test:deno 28 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.192.0/containers/typescript-node/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] Node.js version: 16, 14, 12 4 | ARG VARIANT="16-buster" 5 | FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT} 6 | 7 | # [Optional] Uncomment this section to install additional OS packages. 8 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 9 | # && apt-get -y install --no-install-recommends 10 | 11 | # [Optional] Uncomment if you want to install an additional version of node using nvm 12 | # ARG EXTRA_NODE_VERSION=10 13 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 14 | 15 | # [Optional] Uncomment if you want to install more global node packages 16 | # RUN su node -c "npm install -g " 17 | -------------------------------------------------------------------------------- /src/tests/input.ts: -------------------------------------------------------------------------------- 1 | /* c8 ignore start */ 2 | 3 | import { PromiseArgs } from "../args"; 4 | import { sleep } from "./sleep"; 5 | 6 | export const inputs: PromiseArgs[] = [ 7 | [], 8 | [Promise.resolve(1)], 9 | [Promise.resolve(1), () => Promise.reject(1), Promise.resolve(1)], 10 | [ 11 | [Promise.resolve(1)], 12 | () => Promise.reject(1), 13 | [Promise.resolve(1)], 14 | () => Promise.reject(1), 15 | ], 16 | [ 17 | () => sleep(10, 1), 18 | () => sleep(5, 1), 19 | () => sleep(15).then(() => Promise.reject("15")), 20 | ], 21 | [ 22 | async function* () { 23 | yield 1; 24 | yield 2; 25 | }, 26 | async function* () { 27 | yield 3; 28 | yield 4; 29 | }, 30 | async function* () { 31 | yield 5; 32 | yield 6; 33 | await sleep(5); 34 | await Promise.reject(7); 35 | }, 36 | async function () { 37 | await sleep(10); 38 | return 8 as const; 39 | }, 40 | ], 41 | ]; 42 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.192.0/containers/typescript-node 3 | { 4 | "name": "Node.js & TypeScript", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | // Update 'VARIANT' to pick a Node version: 12, 14, 16 8 | "args": { 9 | "VARIANT": "16" 10 | } 11 | }, 12 | 13 | // Set *default* container specific settings.json values on container create. 14 | "settings": {}, 15 | 16 | // Add the IDs of extensions you want installed when the container is created. 17 | "extensions": ["dbaeumer.vscode-eslint"], 18 | 19 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 20 | // "forwardPorts": [], 21 | 22 | // Use 'postCreateCommand' to run commands after the container is created. 23 | // "postCreateCommand": "yarn install", 24 | 25 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 26 | "remoteUser": "node" 27 | } 28 | -------------------------------------------------------------------------------- /src/defer.ts: -------------------------------------------------------------------------------- 1 | import { ok } from "./like"; 2 | 3 | export type DeferredStatus = "pending" | "fulfilled" | "rejected"; 4 | 5 | export interface Deferred { 6 | resolve(value: T): void; 7 | reject(reason: unknown): void; 8 | promise: Promise; 9 | readonly settled: boolean; 10 | readonly status: DeferredStatus; 11 | } 12 | 13 | export function defer(): Deferred { 14 | let resolve: Deferred["resolve"] | undefined = undefined, 15 | reject: Deferred["reject"] | undefined = undefined, 16 | settled = false, 17 | status: DeferredStatus = "pending"; 18 | const promise = new Promise((resolveFn, rejectFn) => { 19 | resolve = (value) => { 20 | status = "fulfilled"; 21 | settled = true; 22 | resolveFn(value); 23 | }; 24 | reject = (reason) => { 25 | status = "rejected"; 26 | settled = true; 27 | rejectFn(reason); 28 | }; 29 | }); 30 | ok(resolve); 31 | ok(reject); 32 | return { 33 | get settled() { 34 | return settled; 35 | }, 36 | get status() { 37 | return status; 38 | }, 39 | resolve, 40 | reject, 41 | promise, 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Axiom Applied Technologies and Development Limited 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/tests/main.ts: -------------------------------------------------------------------------------- 1 | import { Promise, PromiseAllRejectEarly } from "../index"; 2 | import { GlobalPromise } from "./global"; 3 | 4 | const { inputs } = await import("./input"); 5 | 6 | for (const input of inputs) { 7 | const promiseOutput = await Promise.allSettled(...input); 8 | console.log({ input }); 9 | console.log({ promiseOutput }); 10 | for await (const state of Promise.allSettled(...input)) { 11 | console.log({ state }); 12 | } 13 | 14 | try { 15 | const allPromiseOutput = await Promise.all(...input); 16 | console.log({ allPromiseOutput }); 17 | } catch (allPromiseReason) { 18 | console.log({ allPromiseReason }); 19 | } 20 | 21 | try { 22 | for await (const state of Promise.all(...input)) { 23 | console.log({ state }); 24 | } 25 | } catch (allPromiseReason) { 26 | console.log({ allPromiseReason }); 27 | } 28 | } 29 | 30 | await Promise.all 31 | .call( 32 | { 33 | [PromiseAllRejectEarly]: true, 34 | }, 35 | (async () => { 36 | throw new Error(); 37 | })() 38 | ) 39 | .catch((reason: unknown) => reason); 40 | 41 | await Promise.all( 42 | GlobalPromise.reject("message"), 43 | GlobalPromise.reject("message") 44 | ).catch((reason: unknown) => reason); 45 | 46 | await Promise.allSettled( 47 | // Not allowed 48 | 1 as unknown as Promise 49 | ).catch((reason: unknown) => reason); 50 | -------------------------------------------------------------------------------- /src/walk/index.ts: -------------------------------------------------------------------------------- 1 | import { Push, PushOptions } from "../push"; 2 | import { series } from "../series"; 3 | 4 | export interface Walk extends AsyncIterable { 5 | push(source: AsyncIterable): unknown; 6 | throw(reason?: unknown): unknown; 7 | break(): void; 8 | close(): unknown; 9 | } 10 | 11 | export interface WalkOptions extends PushOptions {} 12 | 13 | export class Walk implements Walk { 14 | private readonly target: Push; 15 | 16 | private series = series(); 17 | 18 | constructor(options?: WalkOptions) { 19 | this.target = new Push(options); 20 | } 21 | 22 | push(source: AsyncIterable): unknown { 23 | return this.series(async () => { 24 | if (!this.target.open) return; 25 | await this.target.wait(); 26 | try { 27 | for await (const snapshot of source) { 28 | if (!this.target.open) break; 29 | await this.target.push(snapshot); 30 | if (!this.target.open) break; 31 | } 32 | } catch (error) { 33 | await this.target.throw(error); 34 | throw error; 35 | } 36 | }); 37 | } 38 | 39 | throw(reason?: unknown) { 40 | return this.target.throw(reason); 41 | } 42 | 43 | break() { 44 | return this.target.break(); 45 | } 46 | 47 | close(): unknown { 48 | return this.series(async () => { 49 | await this.target.close(); 50 | }); 51 | } 52 | 53 | async *[Symbol.asyncIterator]() { 54 | yield* this.target; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/push/abstract-linked-list.ts: -------------------------------------------------------------------------------- 1 | import { ok } from "../like"; 2 | 3 | interface AbstractMap { 4 | get(index: object): T | undefined; 5 | has(index: object): boolean; 6 | set(index: object, value: T): void; 7 | } 8 | 9 | export interface Node { 10 | value: T; 11 | next?: object; 12 | } 13 | 14 | export interface LinkedList { 15 | get(pointer: object): Node | undefined; 16 | has(pointer: object): boolean; 17 | insert(after: object, pointer: object, value: T): void; 18 | clear(): void; 19 | } 20 | 21 | export abstract class AbstractLinkedList implements LinkedList { 22 | protected constructor(private _map?: AbstractMap>) {} 23 | 24 | protected set map(map: AbstractMap>) { 25 | this._map = map; 26 | } 27 | 28 | get(pointer: object): Node | undefined { 29 | return this._map.get(pointer); 30 | } 31 | 32 | has(pointer: object): boolean { 33 | return this._map.has(pointer); 34 | } 35 | 36 | insert(after: object, pointer: object, value: T): void { 37 | if (!after) { 38 | this.clear(); 39 | } 40 | const reference = after && this.get(after); 41 | ok(!after || reference, "Pointer does not belong in this list"); 42 | const { _map: map } = this; 43 | map.set(pointer, { 44 | value, 45 | next: reference ? reference.next : undefined, 46 | }); 47 | if (after) { 48 | map.set(after, { 49 | ...reference, 50 | next: pointer, 51 | }); 52 | } 53 | } 54 | 55 | abstract clear(): void; 56 | } 57 | -------------------------------------------------------------------------------- /src/is.ts: -------------------------------------------------------------------------------- 1 | /* c8 ignore start */ 2 | import { isLike } from "./like"; 3 | 4 | export function isAsyncIterable(value: unknown): value is AsyncIterable { 5 | return !!( 6 | isLike>(value) && 7 | typeof value[Symbol.asyncIterator] === "function" 8 | ); 9 | } 10 | 11 | export function isIterable(value: unknown): value is Iterable { 12 | return !!( 13 | isLike>(value) && 14 | typeof value[Symbol.iterator] === "function" 15 | ); 16 | } 17 | 18 | export function isIteratorYieldResult( 19 | value: unknown 20 | ): value is IteratorYieldResult { 21 | return !!( 22 | isLike>>(value) && 23 | typeof value.done === "boolean" && 24 | !value.done 25 | ); 26 | } 27 | 28 | export function isIteratorResult( 29 | value: unknown 30 | ): value is IteratorYieldResult { 31 | return !!( 32 | isLike>>(value) && 33 | typeof value.done === "boolean" 34 | ); 35 | } 36 | 37 | export function isRejected( 38 | value: PromiseSettledResult 39 | ): value is PromiseRejectedResult; 40 | export function isRejected( 41 | value: PromiseSettledResult 42 | ): value is R; 43 | export function isRejected( 44 | value: PromiseSettledResult 45 | ): value is R { 46 | return value?.status === "rejected"; 47 | } 48 | 49 | export function isFulfilled( 50 | value: PromiseSettledResult 51 | ): value is PromiseFulfilledResult { 52 | return value?.status === "fulfilled"; 53 | } 54 | 55 | export function isPromise(value: unknown): value is Promise { 56 | return isLike>(value) && typeof value.then === "function"; 57 | } 58 | 59 | export function isArray(value: unknown): value is T[] { 60 | return Array.isArray(value); 61 | } -------------------------------------------------------------------------------- /src/args.ts: -------------------------------------------------------------------------------- 1 | export type PromiseOrAsync = Promise | AsyncIterable | PromiseFn; 2 | export type PromiseFn = () => PromiseOrAsync; 3 | export type PromiseArg = PromiseOrAsync | PromiseOrAsync[]; 4 | export type PromiseArgs = PromiseArg[]; 5 | 6 | /* by defining that we must have at least one arg, we are defining that we have a defined length too */ 7 | export type SinglePromiseArgTuple = 8 | | [PromiseOrAsync, ...PromiseOrAsync[]] 9 | | Readonly<[PromiseOrAsync, ...PromiseOrAsync[]]>; 10 | export type PromiseArgTuple = 11 | | [SinglePromiseArgTuple] 12 | | SinglePromiseArgTuple; 13 | 14 | export type SinglePromiseTuple< 15 | TArgs extends PromiseArgTuple = PromiseArgTuple 16 | > = { 17 | [K in keyof TArgs]: K extends `${number}` | number 18 | ? TArgs[K] extends PromiseOrAsync 19 | ? R 20 | : never 21 | : TArgs[K]; 22 | } & { length: TArgs["length"] }; 23 | export type PromiseTuple> = 24 | TArgs[0] extends SinglePromiseArgTuple 25 | ? SinglePromiseTuple 26 | : SinglePromiseTuple; 27 | export type PromiseTupleIntermediateArray< 28 | RA extends { length: number } 29 | > = { 30 | [K in keyof RA]: K extends `${number}` | number ? RA[K] | undefined : RA[K]; 31 | } & { length: RA["length"] }; 32 | export type PromiseTupleIntermediate> = 33 | PromiseTupleIntermediateArray>; 34 | 35 | export type PromiseSettledTuple> = { 36 | [K in keyof TArgs]: TArgs[K] extends PromiseOrAsync 37 | ? PromiseSettledResult 38 | : never; 39 | } & { length: TArgs["length"] }; 40 | export type PromiseSettledTupleIntermediate< 41 | TArgs extends PromiseArgTuple 42 | > = PromiseTupleIntermediateArray>; 43 | -------------------------------------------------------------------------------- /src/tests/sync.ts: -------------------------------------------------------------------------------- 1 | import { aSyncThing, TheSyncThing } from "../the-sync-thing"; 2 | import { isIterable, isIteratorYieldResult } from "../is"; 3 | import { anAsyncThing } from "../the-thing"; 4 | import { ok } from "../like"; 5 | 6 | async function withThing(thing: TheSyncThing) { 7 | function runSync() { 8 | ok(isIterable(thing)); 9 | const [one, two, three, ...rest] = thing; 10 | console.log({ one, two, three, rest }); 11 | } 12 | async function run() { 13 | const [one, two, three, ...rest] = await thing; 14 | console.log({ one, two, three, rest }); 15 | } 16 | async function runPromise() { 17 | const [one, two, three, ...rest] = await new Promise>( 18 | (resolve, reject) => thing.then(resolve, reject) 19 | ); 20 | console.log({ one, two, three, rest }); 21 | } 22 | async function runAsync() { 23 | const [one, two, three, ...rest] = await anAsyncThing(thing); 24 | console.log({ one, two, three, rest }); 25 | } 26 | 27 | runSync(); 28 | console.log("run"); 29 | await run(); 30 | console.log("runPromise"); 31 | await runPromise(); 32 | console.log("runAsync"); 33 | await runAsync(); 34 | console.log("await thing"); 35 | 36 | console.log([...(await thing)]); 37 | 38 | async function forAwait() { 39 | const result = await thing[Symbol.asyncIterator]().next(); 40 | 41 | if (isIteratorYieldResult(result)) { 42 | console.log(result.value); 43 | for (const next of result.value) { 44 | console.log({ next }); 45 | } 46 | } 47 | for await (const snapshot of thing) { 48 | for (const next of snapshot) { 49 | console.log({ next }); 50 | } 51 | } 52 | } 53 | 54 | function forSync() { 55 | ok(isIterable(thing)); 56 | for (const next of thing) { 57 | console.log({ next }); 58 | } 59 | } 60 | 61 | await forAwait(); 62 | forSync(); 63 | await forAwait(); 64 | forSync(); 65 | } 66 | 67 | await withThing(aSyncThing(1)); 68 | await withThing(aSyncThing([1, 2, 3, 4])); 69 | 70 | console.log("async sync:"); 71 | for await (const asyncSync of anAsyncThing([1, 2, 3, 4, 5] as const)) { 72 | await withThing(aSyncThing(asyncSync)); 73 | } 74 | -------------------------------------------------------------------------------- /src/tests/readme.ts: -------------------------------------------------------------------------------- 1 | import { all, allSettled } from "../"; 2 | 3 | { 4 | // logs [] 5 | console.log(await all()); 6 | // logs [1, 2] 7 | console.log(await all(Promise.resolve(1), Promise.resolve(2))); 8 | // logs rejected 9 | console.log( 10 | await all(Promise.resolve(1), Promise.reject(2)).catch(() => "rejected") 11 | ); 12 | } 13 | 14 | { 15 | const wait = (timeout = 1, arg: unknown = undefined) => 16 | new Promise((resolve) => setTimeout(resolve, timeout, arg)); 17 | 18 | for await (const state of all( 19 | wait(10, "first index, second resolve"), 20 | wait(1, "second index, first resolve") 21 | )) { 22 | /* 23 | logs 24 | { state: [undefined, "second index, first resolve"] } 25 | { state: ["first index, second resolve", "second index, first resolve"] } 26 | */ 27 | console.log({ state }); 28 | } 29 | } 30 | 31 | { 32 | // logs [] 33 | console.log(await allSettled()); 34 | // logs [ 35 | // { value: 1, status: 'fulfilled' }, 36 | // { value: 2, status: 'fulfilled' } 37 | // ] 38 | console.log(await allSettled(Promise.resolve(1), Promise.resolve(2))); 39 | // logs [ 40 | // { value: 1, status: 'fulfilled' }, 41 | // { reason: 2, status: 'rejected' } 42 | // ] 43 | console.log(await allSettled(Promise.resolve(1), Promise.reject(2))); 44 | } 45 | 46 | { 47 | const wait = (timeout = 1, arg: unknown = undefined) => 48 | new Promise((resolve) => setTimeout(resolve, timeout, arg)); 49 | 50 | for await (const state of allSettled( 51 | wait(10, "A"), 52 | wait(1, "B"), 53 | wait(15).then(() => Promise.reject("C")) 54 | )) { 55 | /* 56 | logs 57 | { 58 | state: [ undefined, { value: 'B', status: 'fulfilled' }, undefined ] 59 | } 60 | { 61 | state: [ 62 | { value: 'A', status: 'fulfilled' }, 63 | { value: 'B', status: 'fulfilled' }, 64 | undefined 65 | ] 66 | } 67 | { 68 | state: [ 69 | { value: 'A', status: 'fulfilled' }, 70 | { value: 'B', status: 'fulfilled' }, 71 | { reason: 'C', status: 'rejected' } 72 | ] 73 | } 74 | */ 75 | console.log({ state }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /.github/workflows/release-actions.yml: -------------------------------------------------------------------------------- 1 | name: release-actions 2 | on: 3 | push: 4 | branches: 5 | - main 6 | release: 7 | types: 8 | - created 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | env: 13 | NO_COVERAGE_BADGE_UPDATE: 1 14 | steps: 15 | - uses: actions/checkout@v2 16 | with: 17 | fetch-depth: 0 18 | - uses: actions/setup-node@v2 19 | with: 20 | node-version: "16.x" 21 | registry-url: "https://registry.npmjs.org" 22 | - uses: denoland/setup-deno@v1 23 | with: 24 | deno-version: "v1.x" 25 | - run: | 26 | yarn install 27 | - run: yarn build 28 | # yarn coverage === c8 + yarn test 29 | - run: yarn coverage 30 | - run: yarn test:deno 31 | - name: Package Registry Publish - npm 32 | run: | 33 | git config user.name "${{ github.actor }}" 34 | git config user.email "${{ github.actor}}@users.noreply.github.com" 35 | npm set "registry=https://registry.npmjs.org/" 36 | npm set "@virtualstate:registry=https://registry.npmjs.org/" 37 | npm set "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" 38 | npm publish --access=public 39 | continue-on-error: true 40 | env: 41 | YARN_TOKEN: ${{ secrets.YARN_TOKEN }} 42 | NPM_TOKEN: ${{ secrets.YARN_TOKEN }} 43 | NODE_AUTH_TOKEN: ${{ secrets.YARN_TOKEN }} 44 | - uses: actions/setup-node@v2 45 | with: 46 | node-version: "16.x" 47 | registry-url: "https://npm.pkg.github.com" 48 | - name: Package Registry Publish - GitHub 49 | run: | 50 | git config user.name "${{ github.actor }}" 51 | git config user.email "${{ github.actor}}@users.noreply.github.com" 52 | npm set "registry=https://npm.pkg.github.com/" 53 | npm set "@virtualstate:registry=https://npm.pkg.github.com/virtualstate" 54 | npm set "//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}" 55 | npm publish --access=public 56 | env: 57 | YARN_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | NPM_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | continue-on-error: true 61 | -------------------------------------------------------------------------------- /src/all.ts: -------------------------------------------------------------------------------- 1 | import { allSettledGenerator } from "./all-settled"; 2 | import { anAsyncThing, TheAsyncThing } from "./the-thing"; 3 | import { isLike } from "./like"; 4 | import { 5 | PromiseArgs, 6 | PromiseArgTuple, 7 | PromiseTuple, 8 | PromiseTupleIntermediate, 9 | } from "./args"; 10 | 11 | export const PromiseAllRejectEarly = Symbol.for( 12 | "@virtualstate/promise/all/rejectEarly" 13 | ); 14 | 15 | export interface PromiseContext { 16 | [PromiseAllRejectEarly]?: unknown; 17 | } 18 | 19 | export function all>( 20 | this: unknown, 21 | ...promises: TArgs 22 | ): TheAsyncThing, PromiseTuple>; 23 | export function all( 24 | this: unknown, 25 | ...promises: PromiseArgs 26 | ): TheAsyncThing; 27 | export function all( 28 | this: unknown, 29 | ...promises: PromiseArgs 30 | ): TheAsyncThing { 31 | return anAsyncThing(allGenerator.call(this, ...promises)); 32 | } 33 | 34 | export function allGenerator>( 35 | this: unknown, 36 | ...promises: TArgs 37 | ): AsyncIterable>; 38 | export function allGenerator( 39 | this: unknown, 40 | ...promises: PromiseArgs 41 | ): AsyncIterable; 42 | export async function* allGenerator( 43 | this: unknown, 44 | ...promises: PromiseArgs 45 | ): AsyncIterable { 46 | let rejected; 47 | for await (const status of allSettledGenerator(...promises)) { 48 | rejected = status.filter(isPromiseRejectedResult); 49 | if ( 50 | isLike(this) && 51 | this?.[PromiseAllRejectEarly] && 52 | rejected.length 53 | ) { 54 | throw aggregateError(rejected); 55 | } 56 | if (rejected.length) continue; // Wait until we have accumulated all rejected 57 | yield status.map( 58 | (status): T => (status?.status === "fulfilled" ? status.value : undefined) 59 | ); 60 | } 61 | if (rejected?.length) { 62 | throw aggregateError(rejected); 63 | } 64 | 65 | function isPromiseRejectedResult( 66 | status: PromiseSettledResult 67 | ): status is PromiseRejectedResult { 68 | return status?.status === "rejected"; 69 | } 70 | 71 | function aggregateError(rejected: PromiseRejectedResult[]) { 72 | if (rejected.length === 1) { 73 | return rejected[0].reason; 74 | } else { 75 | const reasons = rejected.map(({ reason }) => reason); 76 | return new AggregateError(reasons); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '15 10 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /src/the-sync-thing.tsx: -------------------------------------------------------------------------------- 1 | import { isIterable } from "./is"; 2 | 3 | type TheAsyncSyncThing = AsyncIterable> & 4 | Promise>; 5 | 6 | export type TheSyncThing = TheAsyncSyncThing & 7 | Iterator & 8 | Partial>; 9 | 10 | export function aSyncThing>(sync: I): TheSyncThing; 11 | export function aSyncThing(sync: T): TheSyncThing; 12 | export function aSyncThing(sync: T): TheSyncThing { 13 | let iterator: Iterator; 14 | 15 | const symbols = sync ? Object.getOwnPropertySymbols(sync) : []; 16 | const symbolOptions = {}; 17 | for (const symbol of symbols) { 18 | const descriptor = Object.getOwnPropertyDescriptor(sync, symbol); 19 | if (!descriptor) continue; 20 | Object.defineProperty(symbolOptions, symbol, descriptor); 21 | } 22 | 23 | const thing: TheSyncThing & Iterable = { 24 | ...symbolOptions, 25 | async then(resolve, reject) { 26 | void reject; 27 | return resolve(isIterable(sync) ? sync : makeIterableFromThing()); 28 | }, 29 | /* c8 ignore start */ 30 | async catch() { 31 | throw new Error("promise.catch not supported with sync promise"); 32 | }, 33 | async finally() { 34 | throw new Error("promise.finally not supported with sync promise"); 35 | }, 36 | /* c8 ignore end */ 37 | async *[Symbol.asyncIterator]() { 38 | if (isIterable(sync)) { 39 | yield sync; 40 | } else { 41 | yield makeIterableFromSync(); 42 | } 43 | }, 44 | *[Symbol.iterator]() { 45 | if (isIterable(sync)) { 46 | yield* sync; 47 | } else { 48 | yield sync; 49 | } 50 | }, 51 | next(...args: [] | [unknown]) { 52 | iterator = 53 | iterator ?? 54 | (isIterable(sync) ? sync[Symbol.iterator]() : thing[Symbol.iterator]()); 55 | return iterator.next(...args); 56 | }, 57 | return(...args: [] | [unknown]) { 58 | const result: IteratorResult = iterator?.return?.(...args); 59 | iterator = undefined; 60 | return result ?? { done: true, value: undefined }; 61 | }, 62 | throw(...args: [] | [unknown]) { 63 | const result: IteratorResult = iterator?.throw?.(...args); 64 | iterator = undefined; 65 | return result ?? { done: true, value: undefined }; 66 | }, 67 | get [Symbol.toStringTag]() { 68 | return "TheSyncThing"; 69 | }, 70 | }; 71 | return thing; 72 | 73 | function makeIterableFromSync() { 74 | return { 75 | *[Symbol.iterator]() { 76 | yield sync; 77 | }, 78 | }; 79 | } 80 | 81 | function makeIterableFromThing() { 82 | return { 83 | *[Symbol.iterator]() { 84 | yield* thing; 85 | }, 86 | }; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/all-settled.ts: -------------------------------------------------------------------------------- 1 | import { anAsyncThing, TheAsyncThing } from "./the-thing"; 2 | import { union } from "@virtualstate/union"; 3 | import { isAsyncIterable } from "./is"; 4 | import { 5 | PromiseArgs, 6 | PromiseArgTuple, 7 | PromiseOrAsync, 8 | PromiseSettledTuple, 9 | PromiseSettledTupleIntermediate, 10 | } from "./args"; 11 | 12 | export function allSettled>( 13 | this: unknown, 14 | ...promises: TArgs 15 | ): TheAsyncThing< 16 | PromiseSettledTupleIntermediate, 17 | PromiseSettledTuple 18 | >; 19 | export function allSettled( 20 | ...promises: PromiseArgs 21 | ): TheAsyncThing[]>; 22 | export function allSettled(...promises: PromiseArgs): TheAsyncThing { 23 | return anAsyncThing(allSettledGenerator(...promises)); 24 | } 25 | 26 | export function allSettledGenerator>( 27 | this: unknown, 28 | ...promises: TArgs 29 | ): AsyncIterable>; 30 | export function allSettledGenerator( 31 | ...promises: PromiseArgs 32 | ): AsyncIterable[]>; 33 | export async function* allSettledGenerator( 34 | ...promises: PromiseArgs 35 | ): AsyncIterable { 36 | type KnownPromiseResult = [number, PromiseSettledResult]; 37 | const input = promises.flatMap((value) => value); 38 | const knownPromises = [...input].map(map); 39 | const results = Array.from>({ length: input.length }); 40 | let yielded = false; 41 | for await (const state of union(knownPromises)) { 42 | for (const item of state) { 43 | if (!item) continue; 44 | const [index, result] = item; 45 | results[index] = result; 46 | } 47 | // Copy out the results so that we don't give out access to this potentially shared array 48 | yield [...results]; 49 | yielded = true; 50 | } 51 | if (!yielded) yield []; 52 | 53 | async function* map( 54 | promise: PromiseOrAsync, 55 | index: number 56 | ): AsyncIterable { 57 | try { 58 | const isFunction = typeof promise === "function"; 59 | const isObject = typeof promise === "object"; 60 | const isFunctionOrObject = isFunction || isObject; 61 | if (isFunctionOrObject && "then" in promise) { 62 | return yield [index, { value: await promise, status: "fulfilled" }]; 63 | } 64 | if (!isAsyncIterable(promise)) { 65 | if (isFunction) { 66 | const value = promise(); 67 | return yield* map(value, index); 68 | } 69 | return yield [ 70 | index, 71 | { reason: new Error("Unknown input"), status: "rejected" }, 72 | ]; 73 | } 74 | for await (const value of promise) { 75 | yield [index, { value, status: "fulfilled" }]; 76 | } 77 | } catch (reason) { 78 | yield [index, { reason, status: "rejected" }]; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/tests/the-thing.ts: -------------------------------------------------------------------------------- 1 | import { anAsyncThing } from "../the-thing"; 2 | import { ok } from "../like"; 3 | 4 | { 5 | const input = Math.random(); 6 | const result = await anAsyncThing(Promise.resolve(input)); 7 | ok(input === result); 8 | } 9 | { 10 | const input = Math.random(); 11 | let finallySet = false; 12 | const result = await anAsyncThing(Promise.resolve(input)).finally(() => { 13 | finallySet = true; 14 | }); 15 | ok(input === result); 16 | ok(finallySet); 17 | } 18 | 19 | { 20 | const input1 = Math.random(); 21 | const input2 = Math.random(); 22 | 23 | const result = await anAsyncThing( 24 | (async function* () { 25 | yield input1; 26 | yield input2; 27 | })() 28 | ); 29 | 30 | ok(result === input2); 31 | } 32 | 33 | { 34 | const input = Math.random(); 35 | 36 | let results = []; 37 | for await (const result of anAsyncThing(Promise.resolve(input))) { 38 | ok([input].includes(result)); 39 | results.push(result); 40 | } 41 | ok(results.length === 1); 42 | ok(results[0] === input); 43 | } 44 | 45 | { 46 | const input1 = Math.random(); 47 | const input2 = Math.random(); 48 | 49 | let results = []; 50 | for await (const result of anAsyncThing( 51 | (async function* () { 52 | yield input1; 53 | yield input2; 54 | })() 55 | )) { 56 | ok([input1, input2].includes(result)); 57 | results.push(result); 58 | } 59 | ok(results.length === 2); 60 | ok(results[0] === input1); 61 | ok(results[1] === input2); 62 | } 63 | { 64 | const input1 = Math.random(); 65 | const input2 = Math.random(); 66 | 67 | const iterator = (async function* () { 68 | yield input1; 69 | yield input2; 70 | })()[Symbol.asyncIterator](); 71 | 72 | let results = []; 73 | for await (const result of anAsyncThing({ 74 | next() { 75 | return iterator.next(); 76 | }, 77 | })) { 78 | ok([input1, input2].includes(result)); 79 | results.push(result); 80 | } 81 | console.log(results); 82 | ok(results.length === 2); 83 | ok(results[0] === input1); 84 | ok(results[1] === input2); 85 | } 86 | 87 | { 88 | const input = Math.random(); 89 | const iterator = anAsyncThing(Promise.resolve(input)); 90 | ok(input === (await iterator.next()).value); 91 | await iterator.return(); 92 | } 93 | { 94 | const input = Math.random(); 95 | const iterator = anAsyncThing(Promise.resolve(input)); 96 | ok(input === (await iterator.next()).value); 97 | const caught = await iterator.throw(1).catch((caught) => caught); 98 | ok(caught); 99 | } 100 | 101 | { 102 | const input = Math.random(); 103 | const iterator = anAsyncThing(Promise.resolve(input)); 104 | await iterator.return(); 105 | } 106 | 107 | { 108 | const input = Math.random(); 109 | const iterator = anAsyncThing(Promise.resolve(input)); 110 | await iterator.throw(); 111 | } 112 | 113 | { 114 | const input = Math.random(); 115 | const iterator = anAsyncThing(Promise.resolve(input)); 116 | ok(typeof iterator[Symbol.toStringTag] === "string"); 117 | } 118 | -------------------------------------------------------------------------------- /scripts/correct-import-extensions.js: -------------------------------------------------------------------------------- 1 | import FileHound from "filehound"; 2 | import { promises as fs } from "fs"; 3 | import path from "path"; 4 | import { promisify } from "util"; 5 | // 6 | // const packages = await FileHound.create() 7 | // .paths(`packages`) 8 | // .directory() 9 | // .depth(1) 10 | // .find(); 11 | 12 | const buildPaths = ["esnext"]; 13 | 14 | for (const buildPath of buildPaths) { 15 | const filePaths = await FileHound.create() 16 | .paths(buildPath) 17 | .discard("node_modules") 18 | .ext("js") 19 | .find(); 20 | 21 | await Promise.all( 22 | filePaths.map(async (filePath) => { 23 | const initialContents = await fs.readFile(filePath, "utf-8"); 24 | 25 | const statements = initialContents.match( 26 | /(?:(?:import|export)(?: .+ from)? ".+";|(?:import\(".+"\)))/g 27 | ); 28 | 29 | if (!statements) { 30 | return; 31 | } 32 | 33 | const importMap = process.env.IMPORT_MAP 34 | ? JSON.parse(await fs.readFile(process.env.IMPORT_MAP, "utf-8")) 35 | : undefined; 36 | const contents = await statements.reduce( 37 | async (contentsPromise, statement) => { 38 | const contents = await contentsPromise; 39 | const url = statement.match(/"(.+)"/)[1]; 40 | if (importMap?.imports?.[url]) { 41 | const replacement = importMap.imports[url]; 42 | if (!replacement.includes("./src")) { 43 | return contents.replace( 44 | statement, 45 | statement.replace(url, replacement) 46 | ); 47 | } 48 | const shift = filePath 49 | .split("/") 50 | // Skip top folder + file 51 | .slice(2) 52 | // Replace with shift up directory 53 | .map(() => "..") 54 | .join("/"); 55 | return contents.replace( 56 | statement, 57 | statement.replace( 58 | url, 59 | replacement.replace("./src", shift).replace(/\.tsx?$/, ".js") 60 | ) 61 | ); 62 | } else { 63 | return contents.replace(statement, await getReplacement(url)); 64 | } 65 | 66 | async function getReplacement(url) { 67 | const [stat, indexStat] = await Promise.all([ 68 | fs 69 | .stat(path.resolve(path.dirname(filePath), url + ".js")) 70 | .catch(() => {}), 71 | fs 72 | .stat(path.resolve(path.dirname(filePath), url + "/index.js")) 73 | .catch(() => {}), 74 | ]); 75 | 76 | if (stat && stat.isFile()) { 77 | return statement.replace(url, url + ".js"); 78 | } else if (indexStat && indexStat.isFile()) { 79 | return statement.replace(url, url + "/index.js"); 80 | } 81 | return statement; 82 | } 83 | }, 84 | Promise.resolve(initialContents) 85 | ); 86 | 87 | await fs.writeFile(filePath, contents, "utf-8"); 88 | }) 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /src/the-thing.tsx: -------------------------------------------------------------------------------- 1 | import { isAsyncIterable, isIterable } from "./is"; 2 | 3 | export type TheAsyncThing = Promise & 4 | AsyncIterable & 5 | AsyncIterator; 6 | 7 | export type TheAsyncThingInput = Partial< 8 | AsyncIterable | AsyncIterator | Promise 9 | >; 10 | 11 | export function anAsyncThing( 12 | async: TheAsyncThingInput 13 | ): TheAsyncThing; 14 | export function anAsyncThing>( 15 | sync: I 16 | ): TheAsyncThing; 17 | export function anAsyncThing( 18 | async: TheAsyncThingInput 19 | ): TheAsyncThing { 20 | let iterator: AsyncIterator, promise: Promise; 21 | 22 | const symbols = async ? Object.getOwnPropertySymbols(async) : []; 23 | const symbolOptions = {}; 24 | for (const symbol of symbols) { 25 | const descriptor = Object.getOwnPropertyDescriptor(async, symbol); 26 | if (!descriptor) continue; 27 | Object.defineProperty(symbolOptions, symbol, descriptor); 28 | } 29 | 30 | const thing: TheAsyncThing = { 31 | ...symbolOptions, 32 | async then(resolve, reject) { 33 | return getPromise().then(resolve, reject); 34 | }, 35 | async catch(reject) { 36 | return getPromise().catch(reject); 37 | }, 38 | async finally(fn) { 39 | return getPromise().finally(fn); 40 | }, 41 | async *[Symbol.asyncIterator]() { 42 | if (isAsyncIterable(async)) { 43 | return yield* async; 44 | } else if ("then" in async && async.then) { 45 | yield await new Promise((resolve, reject) => 46 | async.then(resolve, reject) 47 | ); 48 | } else if (isSyncIterableValue(async)) { 49 | yield async; 50 | } else if ("next" in async && async.next) { 51 | let result; 52 | let nextValue; 53 | do { 54 | result = await async.next(); 55 | if (isYieldedResult(result)) { 56 | nextValue = yield result.value; 57 | } 58 | } while (!result.done); 59 | } 60 | 61 | function isSyncIterableValue(value: unknown): value is T { 62 | return isIterable(value); 63 | } 64 | }, 65 | async next(...args: [] | [unknown]) { 66 | iterator = iterator ?? thing[Symbol.asyncIterator](); 67 | return iterator.next(...args); 68 | }, 69 | async return(...args: [] | [unknown]) { 70 | if (!iterator) return; 71 | return iterator.return(...args); 72 | }, 73 | async throw(...args: [] | [unknown]) { 74 | if (!iterator) return; 75 | return iterator.throw(...args); 76 | }, 77 | get [Symbol.toStringTag]() { 78 | return "TheAsyncThing"; 79 | }, 80 | }; 81 | return thing; 82 | 83 | function getPromise(): Promise { 84 | promise = promise || asPromise(); 85 | return promise; 86 | async function asPromise(): Promise { 87 | if ("then" in async && async.then) { 88 | return new Promise((resolve, reject) => async.then(resolve, reject)); 89 | } 90 | let value: T; 91 | for await (value of thing) { 92 | } 93 | return value; 94 | } 95 | } 96 | 97 | function isYieldedResult( 98 | value: IteratorResult 99 | ): value is IteratorYieldResult { 100 | return !value.done; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/tests/downstream.ts: -------------------------------------------------------------------------------- 1 | /* c8 ignore start */ 2 | import { createDownstreamGenerator } from "../downstream"; 3 | import { union } from "@virtualstate/union"; 4 | import {Push} from "../push"; 5 | 6 | async function* create() { 7 | const a = (async function* () { 8 | console.log("index:a start"); 9 | for (let i = 0; i <= 10; i += 1) { 10 | yield i; 11 | await new Promise((resolve) => setTimeout(resolve, 100 * Math.random())); 12 | } 13 | console.log("index:a fin"); 14 | })(); 15 | const b = (async function* () { 16 | console.log("index:b start"); 17 | for (let i = 0; i <= 40; i += 1) { 18 | yield i; 19 | await new Promise((resolve) => setTimeout(resolve, 100 * Math.random())); 20 | } 21 | console.log("index:b fin"); 22 | })(); 23 | const c = (async function* () { 24 | console.log("index:c start"); 25 | for (let i = 0; i <= 20; i += 1) { 26 | yield i; 27 | await new Promise((resolve) => setTimeout(resolve, 100 * Math.random())); 28 | } 29 | console.log("index:c fin"); 30 | })(); 31 | const d = (async function* () { 32 | console.log("index:d start"); 33 | for (let i = 0; i <= 30; i += 1) { 34 | yield i; 35 | await new Promise((resolve) => setTimeout(resolve, 100 * Math.random())); 36 | } 37 | console.log("index:d fin"); 38 | })(); 39 | 40 | yield [a, b, c, d]; 41 | await new Promise((resolve) => setTimeout(resolve, 100)); 42 | yield [ 43 | a, 44 | b, 45 | (async function* () { 46 | console.log("index:1:2 start"); 47 | for (let i = 0; i <= 4; i += 1) { 48 | yield i; 49 | await new Promise((resolve) => 50 | setTimeout(resolve, 100 * Math.random()) 51 | ); 52 | } 53 | console.log("index:1:2 fin"); 54 | })(), 55 | ]; 56 | console.log("fin"); 57 | } 58 | console.log("==== start"); 59 | 60 | const push = new Push>(); 61 | 62 | async function* yielding() { 63 | push.push( 64 | (async function* () { 65 | console.log("1"); 66 | yield 1; 67 | console.log("fin 1"); 68 | })() 69 | ); 70 | push.push( 71 | (async function* () { 72 | console.log("2"); 73 | yield 2; 74 | console.log("fin 2"); 75 | })() 76 | ); 77 | await new Promise((resolve) => setTimeout(resolve, 10)); 78 | push.push( 79 | (async function* () { 80 | console.log("3"); 81 | // await new Promise(resolve => setTimeout(resolve, 1)); 82 | yield 3; 83 | console.log("fin 3"); 84 | })() 85 | ); 86 | await new Promise((resolve) => setTimeout(resolve, 10)); 87 | console.log("closing"); 88 | push.close(); 89 | yield 1; 90 | console.log("finished"); 91 | } 92 | 93 | async function* unionPush() { 94 | console.log("union push start"); 95 | for await (const snapshot of union(push)) { 96 | console.log("union push snapshot"); 97 | yield snapshot; 98 | console.log("union push snapshot next"); 99 | } 100 | console.log("union push done"); 101 | } 102 | 103 | for await (const snapshot of union([yielding(), unionPush()])) { 104 | console.log({ snapshot }); 105 | } 106 | 107 | console.log("=== start next"); 108 | 109 | for await (const snapshot of createDownstreamGenerator(create())) { 110 | console.log({ snapshot }); 111 | } 112 | 113 | console.log("==== done"); 114 | -------------------------------------------------------------------------------- /src/tests/line.ts: -------------------------------------------------------------------------------- 1 | import {Line} from "../line"; 2 | import {ok} from "../like"; 3 | 4 | { 5 | 6 | const line = new Line(); 7 | 8 | line.push(1); 9 | line.push(2); 10 | line.push(3); 11 | 12 | line.close(); 13 | 14 | const iterator = line[Symbol.asyncIterator](); 15 | 16 | ok((await iterator.next()).value === 1); 17 | ok((await iterator.next()).value === 2); 18 | ok((await iterator.next()).value === 3); 19 | ok((await iterator.previous()).value === 2); 20 | ok((await iterator.next()).value === 3); 21 | ok((await iterator.previous()).value === 2); 22 | ok((await iterator.previous()).value === 1); 23 | ok((await iterator.previous()).done); 24 | 25 | // 1, 2, 3 26 | for await (const value of iterator) { 27 | console.log({ value }); 28 | } 29 | 30 | ok((await iterator.next()).done); 31 | ok((await iterator.previous()).value === 3); 32 | ok((await iterator.previous()).value === 2); 33 | ok((await iterator.previous()).value === 1); 34 | 35 | // 2, 3 36 | for await (const value of iterator) { 37 | console.log({ value }); 38 | } 39 | 40 | async function start() { 41 | let result; 42 | 43 | do { 44 | result = await iterator.previous(); 45 | } while (!result.done); 46 | } 47 | 48 | await start(); 49 | 50 | ok((await iterator.next()).value === 1); 51 | ok((await iterator.next()).value === 2); 52 | ok((await iterator.next()).value === 3); 53 | ok((await iterator.next()).done); 54 | 55 | ok((await iterator.previous()).value === 3); 56 | ok((await iterator.previous()).value === 2); 57 | ok((await iterator.next()).value === 3); 58 | ok((await iterator.previous()).value === 2); 59 | ok((await iterator.next()).value === 3); 60 | } 61 | 62 | { 63 | 64 | const line = new Line(); 65 | 66 | line.push(1); 67 | line.push(2); 68 | line.push(3); 69 | 70 | const iterator = line[Symbol.asyncIterator](); 71 | 72 | ok((await iterator.next()).value === 1); 73 | ok((await iterator.next()).value === 2); 74 | ok((await iterator.next()).value === 3); 75 | 76 | let promise = iterator.next(); 77 | 78 | let expected = 4 + Math.random(); 79 | line.push(expected); 80 | 81 | ok((await promise).value === expected); 82 | 83 | ok((await iterator.previous()).value === 3); 84 | ok((await iterator.previous()).value === 2); 85 | ok((await iterator.previous()).value === 1); 86 | ok((await iterator.previous()).done); 87 | 88 | line.push(expected + 1); 89 | line.close(); 90 | 91 | ok((await iterator.next()).value === 1); 92 | ok((await iterator.next()).value === 2); 93 | ok((await iterator.next()).value === 3); 94 | ok((await iterator.next()).value === expected); 95 | ok((await iterator.next()).value === expected + 1); 96 | ok((await iterator.next()).done); 97 | 98 | let reversed = Line.reverse(line); 99 | 100 | for await (const value of reversed) { 101 | console.log({ value }); 102 | } 103 | 104 | reversed = Line.reverse(line); 105 | 106 | ok((await reversed.next()).value === expected + 1); 107 | ok((await reversed.next()).value === expected); 108 | ok((await reversed.next()).value === 3); 109 | ok((await reversed.next()).value === 2); 110 | ok((await reversed.next()).value === 1); 111 | ok((await reversed.next()).done); 112 | 113 | 114 | 115 | 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/tests/split-fn.ts: -------------------------------------------------------------------------------- 1 | import { split } from "../split"; 2 | import { ok } from "../like"; 3 | import { anAsyncThing } from "../the-thing"; 4 | 5 | { 6 | const [a, b, c] = split(async function* () { 7 | yield [1, 2, 3]; 8 | yield [4, 5, 6]; 9 | }); 10 | 11 | const results = [await a, await b, await c]; 12 | console.log({ results }); 13 | ok(results[0] === 4); 14 | ok(results[1] === 5); 15 | ok(results[2] === 6); 16 | } 17 | { 18 | const fn = split(async function* () { 19 | yield [1, 2, 3]; 20 | yield [4, 5, 6]; 21 | }); 22 | const [a, b, c] = fn(); 23 | 24 | const results = [await a, await b, await c]; 25 | console.log({ results }); 26 | ok(results[0] === 4); 27 | ok(results[1] === 5); 28 | ok(results[2] === 6); 29 | } 30 | 31 | async function* Times(times?: number) { 32 | console.log({ times, arguments }); 33 | yield [1, 2, 3]; 34 | if (times > 1) { 35 | yield [4, 5, 6]; 36 | } 37 | if (times > 2) { 38 | yield [7, 8, 9]; 39 | } 40 | } 41 | 42 | { 43 | const fn = split(Times); 44 | const [a, b, c] = fn(1); 45 | const results = [await a, await b, await c]; 46 | console.log({ results }); 47 | ok(results[0] === 1); 48 | ok(results[1] === 2); 49 | ok(results[2] === 3); 50 | } 51 | 52 | { 53 | const fn = split(Times); 54 | const [a, b, c] = fn(3); 55 | const results = [await a, await b, await c]; 56 | console.log({ results }); 57 | ok(results[0] === 7); 58 | ok(results[1] === 8); 59 | ok(results[2] === 9); 60 | } 61 | 62 | { 63 | const read = split(Times); 64 | 65 | const even = await anAsyncThing(read.filter((value) => value % 2 === 0)); 66 | 67 | console.log({ even }); 68 | ok(Array.isArray(even)); 69 | ok(even.length === 1); 70 | ok(even[0] === 2); 71 | } 72 | 73 | { 74 | const read = split(Times); 75 | 76 | const even = await anAsyncThing(read().filter((value) => value % 2 === 0)); 77 | 78 | console.log({ even }); 79 | ok(Array.isArray(even)); 80 | ok(even.length === 1); 81 | ok(even[0] === 2); 82 | } 83 | 84 | { 85 | const read = split(Times); 86 | 87 | const even = await anAsyncThing(read(2).filter((value) => value % 2 === 0)); 88 | 89 | console.log({ even }); 90 | ok(Array.isArray(even)); 91 | ok(even.length === 2); 92 | ok(even[0] === 4); 93 | ok(even[1] === 6); 94 | } 95 | 96 | { 97 | const read = split(Times); 98 | 99 | const even = await anAsyncThing(read(3).filter((value) => value % 2 === 0)); 100 | 101 | console.log({ even }); 102 | ok(Array.isArray(even)); 103 | ok(even.length === 1); 104 | ok(even[0] === 8); 105 | } 106 | 107 | { 108 | async function* sum(...all: number[]): AsyncIterable { 109 | const value = all.reduce((sum, value) => sum + value, 0); 110 | console.log({ sum: all, value }); 111 | yield value; 112 | } 113 | 114 | const one = split(sum)(1); 115 | 116 | const two = one(1); 117 | const five = two(3); 118 | const twenty = five(5, 5, 5); 119 | 120 | const [twentyResult] = twenty; 121 | const result = await twentyResult; 122 | 123 | console.log({ twenty, twentyResult, result }); 124 | 125 | ok(result === 20); 126 | } 127 | 128 | { 129 | const [eights] = split(Times)(3).filter((value) => value === 8); 130 | 131 | let total = 0; 132 | for await (const eight of eights) { 133 | console.log({ eight }); 134 | total += 1; 135 | ok(eight === 8); 136 | } 137 | console.log({ total }); 138 | ok(total === 1); 139 | } 140 | 141 | { 142 | const [eight] = await split(Times)(3).filter((value) => value === 8); 143 | ok(eight === 8); 144 | } 145 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [conduct+axiom@fabiancook.dev](mailto:conduct+axiom@fabiancook.dev). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /src/tests/typed.ts: -------------------------------------------------------------------------------- 1 | import { all, allSettled } from "../"; 2 | import { ok } from "../like"; 3 | import { sleep } from "./sleep"; 4 | 5 | { 6 | const [a, b] = await all( 7 | Promise.resolve(1 as const), 8 | Promise.resolve(2 as const) 9 | ); 10 | 11 | console.log({ a, b }); 12 | 13 | ok(a === 1); 14 | ok(b === 2); 15 | } 16 | 17 | { 18 | const [a, b] = await all([ 19 | Promise.resolve(1 as const), 20 | Promise.resolve(2 as const), 21 | ]); 22 | 23 | console.log({ a, b }); 24 | 25 | ok(a === 1); 26 | ok(b === 2); 27 | } 28 | 29 | { 30 | for await (const [a, b] of all( 31 | sleep(10, 1 as const), 32 | sleep(15, 2 as const) 33 | )) { 34 | console.log({ state: { a, b } }); 35 | if (a) ok(a === 1); 36 | if (b) ok(b === 2); 37 | } 38 | } 39 | 40 | { 41 | const [a, b, c] = await all( 42 | async function* () { 43 | yield 1 as const; 44 | yield 2 as const; 45 | }, 46 | (async function* () { 47 | yield 3 as const; 48 | yield 4 as const; 49 | })(), 50 | async function () { 51 | await sleep(10); 52 | return 8 as const; 53 | } 54 | ); 55 | 56 | console.log({ a, b, c }); 57 | 58 | ok(a === 2); 59 | ok(b === 4); 60 | ok(c === 8); 61 | } 62 | 63 | { 64 | for await (const [a, b, c] of all( 65 | async function* () { 66 | yield 1 as const; 67 | yield 2 as const; 68 | }, 69 | (async function* () { 70 | yield 3 as const; 71 | yield 4 as const; 72 | })(), 73 | async function () { 74 | await sleep(10); 75 | return 8 as const; 76 | } 77 | )) { 78 | console.log({ state: { a, b, c } }); 79 | if (a) ok(([1, 2] as const).includes(a)); 80 | if (b) ok(([3, 4] as const).includes(b)); 81 | if (c) ok(([8] as const).includes(c)); 82 | } 83 | } 84 | 85 | { 86 | const [a, b] = await allSettled( 87 | Promise.resolve(1 as const), 88 | Promise.resolve(2 as const) 89 | ); 90 | 91 | console.log({ a, b }); 92 | 93 | ok(a.status === "fulfilled" && a.value === 1); 94 | ok(b.status === "fulfilled" && b.value === 2); 95 | } 96 | 97 | { 98 | for await (const [a, b] of allSettled( 99 | sleep(10, 1 as const), 100 | sleep(15, 2 as const) 101 | )) { 102 | console.log({ state: { a, b } }); 103 | if (a) ok(a.status === "fulfilled" && a.value === 1); 104 | if (b) ok(b.status === "fulfilled" && b.value === 2); 105 | } 106 | } 107 | 108 | { 109 | for await (const [a, b, c, d] of allSettled( 110 | async function* () { 111 | yield 1 as const; 112 | yield 2 as const; 113 | }, 114 | (async function* () { 115 | yield 3 as const; 116 | yield 4 as const; 117 | })(), 118 | (async function* () { 119 | yield 5 as const; 120 | yield 6 as const; 121 | await sleep(5); 122 | throw await Promise.reject(7); 123 | })(), 124 | async function () { 125 | await sleep(10); 126 | return 8 as const; 127 | } 128 | )) { 129 | console.log({ state: { a, b, c, d } }); 130 | if (a) ok(a.status === "fulfilled" && ([1, 2] as const).includes(a.value)); 131 | if (b) ok(b.status === "fulfilled" && ([3, 4] as const).includes(b.value)); 132 | if (c) 133 | ok( 134 | c.status === "fulfilled" 135 | ? ([5, 6] as const).includes(c.value) 136 | : c.reason === 7 137 | ); 138 | if (d) ok(d.status === "fulfilled" && ([8] as const).includes(d.value)); 139 | } 140 | } 141 | 142 | { 143 | const [a, b] = await allSettled(Promise.resolve(1), Promise.reject(2)); 144 | 145 | ok(a); 146 | ok(b); 147 | ok(a.status === "fulfilled"); 148 | ok(b.status === "rejected"); 149 | } 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `@virtualstate/promise` 2 | 3 | > Psst... There is a blog post at [fabiancook.dev](https://fabiancook.dev/2022/02/26/an-async-thing) with details on how 4 | > this project came to be, and the steps taken during implementation to define the included functionality. 5 | 6 | [//]: # (badges) 7 | 8 | ### Support 9 | 10 | ![Node.js supported](https://img.shields.io/badge/node-%3E%3D16.0.0-blue) ![Deno supported](https://img.shields.io/badge/deno-%3E%3D1.17.0-blue) 11 | 12 | ### Test Coverage 13 | 14 | ![96.23%25 lines covered](https://img.shields.io/badge/lines-96.23%25-brightgreen) ![96.23%25 statements covered](https://img.shields.io/badge/statements-96.23%25-brightgreen) ![92.81%25 functions covered](https://img.shields.io/badge/functions-92.81%25-brightgreen) ![93.06%25 branches covered](https://img.shields.io/badge/branches-93.06%25-brightgreen) 15 | 16 | [//]: # (badges) 17 | 18 | ## `all` 19 | 20 | ```typescript 21 | import { all } from "@virtualstate/promise"; 22 | 23 | // logs [] 24 | console.log(await all()); 25 | // logs [1, 2] 26 | console.log(await all(Promise.resolve(1), Promise.resolve(2))); 27 | // logs rejected 28 | console.log( 29 | await all(Promise.resolve(1), Promise.reject(2)) 30 | .catch(() => "rejected") 31 | ); 32 | ``` 33 | 34 | ```typescript 35 | 36 | const wait = (timeout = 1, arg = undefined) => new Promise(resolve => setTimeout(resolve, timeout, arg)); 37 | 38 | for await (const state of all( 39 | wait(10, "first index, second resolve"), 40 | wait(1, "second index, first resolve") 41 | )) { 42 | /* 43 | logs 44 | { state: [undefined, "second index, first resolve"] } 45 | { state: ["first index, second resolve", "second index, first resolve"] } 46 | */ 47 | console.log({ state }); 48 | } 49 | ``` 50 | 51 | 52 | ## `allSettled` 53 | 54 | ```typescript 55 | import { allSettled } from "@virtualstate/promise"; 56 | 57 | // logs [] 58 | console.log(await allSettled()); 59 | // logs [ 60 | // { value: 1, status: 'fulfilled' }, 61 | // { value: 2, status: 'fulfilled' } 62 | // ] 63 | console.log(await allSettled(Promise.resolve(1), Promise.resolve(2))); 64 | // logs [ 65 | // { value: 1, status: 'fulfilled' }, 66 | // { reason: 2, status: 'rejected' } 67 | // ] 68 | console.log(await allSettled(Promise.resolve(1), Promise.reject(2))); 69 | ``` 70 | 71 | ```typescript 72 | 73 | const wait = (timeout = 1, arg = undefined) => new Promise(resolve => setTimeout(resolve, timeout, arg)); 74 | 75 | for await (const state of allSettled( 76 | wait(10, "A"), 77 | wait(1, "B"), 78 | wait(15).then(() => Promise.reject("C")) 79 | )) { 80 | /* 81 | logs 82 | { 83 | state: [ undefined, { value: 'B', status: 'fulfilled' }, undefined ] 84 | } 85 | { 86 | state: [ 87 | { value: 'A', status: 'fulfilled' }, 88 | { value: 'B', status: 'fulfilled' }, 89 | undefined 90 | ] 91 | } 92 | { 93 | state: [ 94 | { value: 'A', status: 'fulfilled' }, 95 | { value: 'B', status: 'fulfilled' }, 96 | { reason: 'C', status: 'rejected' } 97 | ] 98 | } 99 | */ 100 | console.log({ state }); 101 | } 102 | ``` 103 | 104 | ## Contributing 105 | 106 | Please see [Contributing](./CONTRIBUTING.md) 107 | 108 | ## Code of Conduct 109 | 110 | This project and everyone participating in it is governed by the [Code of Conduct listed here](./CODE-OF-CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [conduct@fabiancook.dev](mailto:conduct@fabiancook.dev). 111 | 112 | ## Licence 113 | 114 | This repository is licensed under the [MIT](https://choosealicense.com/licenses/mit/) license. 115 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@virtualstate/promise", 3 | "version": "1.4.0", 4 | "main": "./esnext/index.js", 5 | "module": "./esnext/index.js", 6 | "types": "./esnext/index.d.ts", 7 | "typesVersions": { 8 | "*": { 9 | "*": [ 10 | "./esnext/index.d.ts" 11 | ], 12 | "all": [ 13 | "./esnext/all.d.ts" 14 | ], 15 | "all-settled": [ 16 | "./esnext/all-settled.d.ts" 17 | ], 18 | "the-thing": [ 19 | "./esnext/the-thing.d.ts" 20 | ], 21 | "the-sync-thing": [ 22 | "./esnext/the-sync-thing.d.ts" 23 | ], 24 | "downstream": [ 25 | "./esnext/downstream.d.ts" 26 | ], 27 | "tests": [ 28 | "./esnext/tests/index.d.ts" 29 | ] 30 | } 31 | }, 32 | "type": "module", 33 | "sideEffects": false, 34 | "keywords": [], 35 | "exports": { 36 | ".": "./esnext/index.js", 37 | "./all": "./esnext/all.js", 38 | "./all-settled": "./esnext/all-settled.js", 39 | "./the-thing": "./esnext/the-thing.js", 40 | "./the-sync-thing": "./esnext/the-sync-thing.js", 41 | "./downstream": "./esnext/downstream.js", 42 | "./tests": "./esnext/tests/index.js" 43 | }, 44 | "repository": { 45 | "type": "git", 46 | "url": "git+https://github.com/virtualstate/promise.git" 47 | }, 48 | "bugs": { 49 | "url": "https://github.com/virtualstate/promise/issues" 50 | }, 51 | "homepage": "https://github.com/virtualstate/promise#readme", 52 | "author": "Fabian Cook ", 53 | "license": "MIT", 54 | "peerDependencies": { 55 | "@virtualstate/union": "*" 56 | }, 57 | "dependencies": {}, 58 | "devDependencies": { 59 | "@babel/cli": "^7.15.4", 60 | "@babel/core": "^7.15.4", 61 | "@babel/preset-env": "^7.15.4", 62 | "@opennetwork/http-representation": "^3.0.0", 63 | "@rollup/plugin-node-resolve": "^13.1.1", 64 | "@rollup/plugin-typescript": "^8.3.0", 65 | "@types/chance": "^1.1.3", 66 | "@types/jest": "^27.0.1", 67 | "@types/mkdirp": "^1.0.2", 68 | "@types/node": "^17.0.1", 69 | "@types/rimraf": "^3.0.2", 70 | "@types/uuid": "^8.3.3", 71 | "@types/whatwg-url": "^8.2.1", 72 | "@virtualstate/astro-renderer": "^2.48.1", 73 | "@virtualstate/dom": "^2.48.1", 74 | "@virtualstate/examples": "^2.48.1", 75 | "@virtualstate/fringe": "^2.48.1", 76 | "@virtualstate/hooks": "^2.48.1", 77 | "@virtualstate/hooks-extended": "^2.48.1", 78 | "@virtualstate/union": "^2.48.1", 79 | "@virtualstate/x": "^2.48.1", 80 | "c8": "^7.11.3", 81 | "chance": "^1.1.8", 82 | "cheerio": "^1.0.0-rc.10", 83 | "core-js": "^3.17.2", 84 | "dom-lite": "^20.2.0", 85 | "filehound": "^1.17.4", 86 | "jest": "^27.1.0", 87 | "jest-playwright-preset": "^1.7.0", 88 | "mkdirp": "^1.0.4", 89 | "playwright": "^1.17.1", 90 | "prettier": "^2.7.1", 91 | "rimraf": "^3.0.2", 92 | "rollup": "^2.61.1", 93 | "rollup-plugin-babel": "^4.4.0", 94 | "rollup-plugin-ignore": "^1.0.10", 95 | "ts-jest": "^27.0.5", 96 | "ts-node": "^10.2.1", 97 | "typescript": "^5.1.6", 98 | "urlpattern-polyfill": "^1.0.0-rc2", 99 | "v8-to-istanbul": "^8.1.0" 100 | }, 101 | "scripts": { 102 | "build": "rm -rf esnext && tsc", 103 | "postbuild": "mkdir -p coverage && node scripts/post-build.js", 104 | "generate": "yarn build && node esnext/generate.js", 105 | "prepublishOnly": "npm run build", 106 | "test": "yarn build && node --enable-source-maps esnext/tests/index.js", 107 | "test:deno": "yarn build && deno run --allow-read --allow-net --import-map=import-map-deno.json esnext/tests/index.js", 108 | "test:deno:reload": "yarn test:deno:r", 109 | "test:deno:r": "yarn build && deno run -r --allow-read --allow-net --import-map=import-map-deno.json esnext/tests/index.js", 110 | "test:inspect": "yarn build && node --enable-source-maps --inspect-brk esnext/tests/index.js", 111 | "coverage": "yarn build && c8 node esnext/tests/index.js && yarn postbuild" 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/tests/iterable-lifecycle.ts: -------------------------------------------------------------------------------- 1 | import { 2 | asyncIterableLifecycle, 3 | IterableLifecycle, 4 | iterableLifecycle, 5 | IterableLifecycleIterator 6 | } from "../iterable-lifecycle"; 7 | import {ok} from "../like"; 8 | 9 | export default 1; 10 | 11 | export type LifecycleEvent = 12 | | { 13 | type: "next"; 14 | args: unknown[]; 15 | } 16 | | { 17 | type: "next-return"; 18 | args: unknown[]; 19 | } 20 | | { 21 | type: "return"; 22 | args: unknown[]; 23 | } 24 | | { 25 | type: "return-return"; 26 | args: unknown[]; 27 | } 28 | | { 29 | type: "throw"; 30 | args: unknown[]; 31 | }; 32 | 33 | export function events(fn?: (event: LifecycleEvent) => void): IterableLifecycle & { events: LifecycleEvent[] } { 34 | const events: LifecycleEvent[] = []; 35 | function push(event: LifecycleEvent) { 36 | events.push(event); 37 | fn?.(event); 38 | } 39 | return { 40 | events, 41 | next: { 42 | next(...args) { 43 | push({ 44 | type: "next", 45 | args 46 | }); 47 | return undefined; 48 | }, 49 | return(...args) { 50 | push({ 51 | type: "next-return", 52 | args 53 | }); 54 | return undefined; 55 | }, 56 | }, 57 | return: { 58 | next(...args) { 59 | push({ 60 | type: "return", 61 | args 62 | }); 63 | return undefined; 64 | }, 65 | return(...args) { 66 | push({ 67 | type: "return-return", 68 | args 69 | }); 70 | return undefined; 71 | }, 72 | }, 73 | throw(...args) { 74 | push({ 75 | type: "throw", 76 | args 77 | }); 78 | return undefined; 79 | }, 80 | } 81 | } 82 | 83 | { 84 | const lifecycle = events(); 85 | const source = [1, 2, 3, 4, 5]; 86 | const result = [ 87 | ...iterableLifecycle(source, lifecycle), 88 | ]; 89 | console.log(source); 90 | console.log(result); 91 | console.log(lifecycle.events); 92 | 93 | ok(result.length === source.length); 94 | ok(lifecycle.events.length === ((source.length * 2) + 2 + 2)); 95 | 96 | for (const [index, value] of source.entries()) { 97 | ok(result[index] === value); 98 | } 99 | } 100 | 101 | { 102 | const lifecycle = events(); 103 | const source = [1, 2, 3]; 104 | const iterator: Iterator = iterableLifecycle(source, lifecycle)[Symbol.iterator](); 105 | 106 | iterator.next(5); 107 | iterator.next(6); 108 | iterator.next(7); 109 | // This is the final next, it will trigger the return as we have reached the 110 | // end of our source 111 | iterator.next(8); 112 | // This is beyond what will be seen 113 | iterator.next(9); 114 | iterator.return(10); 115 | 116 | console.log(lifecycle.events); 117 | 118 | ok(lifecycle.events.length === ((source.length * 2) + 2 + 2)) 119 | } 120 | { 121 | const lifecycle = events(); 122 | const source = [1, 2, 3]; 123 | const iterator: Iterator = iterableLifecycle(source, lifecycle)[Symbol.iterator](); 124 | 125 | iterator.next(5); 126 | iterator.next(6); 127 | iterator.next(7); 128 | // The passed value here will not be seen 129 | iterator.return(8); 130 | 131 | console.log(lifecycle.events); 132 | } 133 | 134 | { 135 | const lifecycle = events(); 136 | const source = { 137 | async *[Symbol.asyncIterator]() { 138 | yield 1; 139 | yield 2; 140 | yield 3; 141 | } 142 | }; 143 | const iterator: AsyncIterator = asyncIterableLifecycle(source, lifecycle)[Symbol.asyncIterator](); 144 | 145 | await iterator.next(5); 146 | await iterator.next(6); 147 | await iterator.next(7); 148 | // This is the final next, it will trigger the return as we have reached the 149 | // end of our source 150 | await iterator.next(8); 151 | // This is beyond what will be seen 152 | await iterator.next(9); 153 | await iterator.return(10); 154 | 155 | console.log(lifecycle.events); 156 | ok(lifecycle.events.length === 10) 157 | } -------------------------------------------------------------------------------- /src/tests/push.ts: -------------------------------------------------------------------------------- 1 | import { p, Push } from "../push"; 2 | import { ok } from "../like"; 3 | import { isIteratorYieldResult, isRejected } from "../is"; 4 | import { anAsyncThing } from "../the-thing"; 5 | 6 | async function withCreator(create: () => Push) { 7 | { 8 | const push = create(); 9 | 10 | const iterator = push[Symbol.asyncIterator](); 11 | 12 | push.push(1); 13 | push.push(2); 14 | push.close(); 15 | 16 | const result1 = await iterator.next(); 17 | console.log({ result1 }); 18 | const result2 = await iterator.next(); 19 | console.log({ result2 }); 20 | const result3 = await iterator.next(); 21 | console.log({ result3 }); 22 | 23 | ok(isIteratorYieldResult(result1)); 24 | ok(isIteratorYieldResult(result2)); 25 | ok(!isIteratorYieldResult(result3)); 26 | } 27 | 28 | { 29 | async function fn(push: Push) { 30 | const values = []; 31 | for await (const snapshot of push) { 32 | console.log({ snapshot }); 33 | values.push(snapshot); 34 | } 35 | console.log("done"); 36 | return values; 37 | } 38 | 39 | { 40 | const push = create(); 41 | 42 | const promise = fn(push); 43 | 44 | push.push(1); 45 | push.push(2); 46 | push.close(); 47 | 48 | const result = await promise; 49 | 50 | ok(result.includes(1)); 51 | ok(result.includes(2)); 52 | } 53 | 54 | { 55 | const push = create(); 56 | 57 | await new Promise(queueMicrotask); 58 | await new Promise(queueMicrotask); 59 | 60 | const promise = fn(push); 61 | 62 | push.push(1); 63 | push.push(2); 64 | push.close(); 65 | 66 | const result = await promise; 67 | 68 | ok(result.includes(1)); 69 | ok(result.includes(2)); 70 | } 71 | } 72 | 73 | { 74 | const push = create(); 75 | push.close(); 76 | const iterator = push[Symbol.asyncIterator](); 77 | const { done } = await iterator.next(); 78 | ok(done); 79 | } 80 | 81 | { 82 | const push = create(); 83 | const iterator = push[Symbol.asyncIterator](); 84 | const promise = iterator.next(); 85 | 86 | await new Promise(queueMicrotask); 87 | await new Promise(queueMicrotask); 88 | 89 | push.push(1); 90 | 91 | const result = await promise; 92 | 93 | ok(isIteratorYieldResult(result)); 94 | 95 | const nextPromise = iterator.next(); 96 | 97 | await new Promise(queueMicrotask); 98 | await new Promise(queueMicrotask); 99 | 100 | push.push(2); 101 | 102 | ok(isIteratorYieldResult(await nextPromise)); 103 | 104 | const donePromise = iterator.next(); 105 | 106 | await new Promise(queueMicrotask); 107 | await new Promise(queueMicrotask); 108 | 109 | push.close(); 110 | 111 | ok(!isIteratorYieldResult(await donePromise)); 112 | 113 | ok(!isIteratorYieldResult(await iterator.return())); 114 | } 115 | 116 | { 117 | const push = create(); 118 | push.push(1); 119 | push.push(2); 120 | push.throw("3"); 121 | const iterator = push[Symbol.asyncIterator](); 122 | ok(isIteratorYieldResult(await iterator.next())); 123 | ok(isIteratorYieldResult(await iterator.next())); 124 | 125 | const [status] = await Promise.allSettled([iterator.next()]); 126 | ok(isRejected(status)); 127 | ok(status.reason === "3"); 128 | } 129 | 130 | { 131 | const push = create(); 132 | const thing = anAsyncThing(push); 133 | 134 | push.push(1); 135 | push.push(2); 136 | push.close(); 137 | 138 | const two = await thing; 139 | 140 | ok(two === 2); 141 | } 142 | } 143 | 144 | await withCreator(() => new Push()); 145 | await withCreator(() => p()); 146 | 147 | { 148 | const push = new Push(); 149 | const thing = anAsyncThing(push); 150 | 151 | push.push(1); 152 | push.push(2); 153 | push.push(3); 154 | push.close(); 155 | 156 | for await (const snapshot of thing) { 157 | console.log({ snapshot }); 158 | ok(snapshot === 1 || snapshot === 2 || snapshot === 3); 159 | } 160 | } 161 | 162 | { 163 | const push = p(); 164 | 165 | push(1); 166 | push(2); 167 | push(3); 168 | 169 | push.close(); 170 | 171 | for await (const snapshot of push) { 172 | console.log({ snapshot }); 173 | ok(snapshot === 1 || snapshot === 2 || snapshot === 3); 174 | } 175 | 176 | } 177 | 178 | { 179 | const push = p(); 180 | 181 | push(1); 182 | push(2); 183 | push(3); 184 | 185 | push.close(); 186 | 187 | const result = await push; 188 | 189 | console.log({ result }); 190 | ok(result === 3); 191 | 192 | } -------------------------------------------------------------------------------- /src/downstream.ts: -------------------------------------------------------------------------------- 1 | /* c8 ignore start */ 2 | 3 | import { union } from "@virtualstate/union"; 4 | import { isAsyncIterable } from "./is"; 5 | import { isLike } from "./like"; 6 | import { anAsyncThing, TheAsyncThing } from "./the-thing"; 7 | import {Push} from "./push"; 8 | 9 | const IsKeySymbol = Symbol("IsKey"); 10 | type Key = { [IsKeySymbol]: true }; 11 | function isKey(value: unknown): value is Key { 12 | return isKeyLike(value) && value[IsKeySymbol] === true; 13 | 14 | function isKeyLike(value: unknown): value is Partial { 15 | return typeof value === "object" && IsKeySymbol in value; 16 | } 17 | } 18 | 19 | function createKey(): Key { 20 | return { [IsKeySymbol]: true }; 21 | } 22 | 23 | type Keyed = [Key, T]; 24 | type KeyedSnapshot = Keyed[]; 25 | type KeyedAsyncIterable = AsyncIterable>; 26 | type KeyedAsyncIterableInput = [object, AsyncIterable]; 27 | 28 | function isKeyedAsyncIterableInput( 29 | input: unknown 30 | ): input is KeyedAsyncIterableInput { 31 | if (!Array.isArray(input)) return false; 32 | if (input.length !== 2) return false; 33 | const [key, value] = input; 34 | return typeof key === "object" && isAsyncIterable(value); 35 | } 36 | 37 | export type DownstreamInput = AsyncIterable< 38 | (T | AsyncIterable | KeyedAsyncIterableInput)[] 39 | >; 40 | 41 | /** 42 | * @experimental not part of the external api 43 | */ 44 | export function createDownstream( 45 | input: DownstreamInput 46 | ): TheAsyncThing { 47 | return anAsyncThing(createDownstreamGenerator(input)); 48 | } 49 | 50 | /** 51 | * @experimental not part of the external api 52 | */ 53 | export async function* createDownstreamGenerator( 54 | input: DownstreamInput 55 | ): AsyncIterable { 56 | const pushIterables = new Push>(); 57 | 58 | const externalKeyMap = new WeakMap(); 59 | const keyMap = new WeakMap, Key>(); 60 | const done = new WeakMap(); 61 | let sourceDone = false; 62 | let last = undefined; 63 | // let latestKeys: Set; 64 | for await (const [snapshot, apply] of union([ 65 | source(), 66 | union(pushIterables), 67 | ])) { 68 | // console.log({ snapshot, apply }); 69 | if (!isLike<(T | Key)[]>(snapshot)) continue; 70 | const values = new Map( 71 | isLike>(apply) ? apply.filter(Array.isArray) : [] 72 | ); 73 | // latestKeys = new Set([...snapshot.filter(isKey)]) 74 | const current = snapshot 75 | .map((value) => (isT(value) ? value : values.get(value))) 76 | .filter((value) => typeof value !== undefined); 77 | if (!isSame(last, current)) { 78 | last = undefined; 79 | yield current; 80 | } 81 | last = current; 82 | } 83 | 84 | function isSame(left?: unknown[], right?: unknown[]) { 85 | if (!left || !right) return false; 86 | if (left.length !== right.length) { 87 | return false; 88 | } 89 | if (!left.length) return true; 90 | return left.every((value, index) => value == right[index]); 91 | } 92 | 93 | function isT(value: T | Key): value is T { 94 | return !isKey(value); 95 | } 96 | 97 | async function* source(): AsyncIterable<(T | Key)[]> { 98 | let keys; 99 | for await (const snapshot of input) { 100 | const mapped = snapshot.map((value): T | Key => { 101 | if (isAsyncIterable(value)) { 102 | const existingKey = keyMap.get(value); 103 | if (existingKey) return existingKey; 104 | const key = createKey(); 105 | keyMap.set(value, key); 106 | pushIterables.push(keyed(key, value)); 107 | return key; 108 | } else { 109 | if (isKeyedAsyncIterableInput(value)) { 110 | const [externalKey, iterable] = value; 111 | const existingKey = externalKeyMap.get(externalKey); 112 | if (existingKey) { 113 | return existingKey; 114 | } 115 | const key = createKey(); 116 | externalKeyMap.set(externalKey, key); 117 | pushIterables.push(keyed(key, iterable)); 118 | return key; 119 | } else { 120 | return value; 121 | } 122 | } 123 | }); 124 | keys = mapped.filter(isKey); 125 | yield mapped; 126 | } 127 | pushIterables.close(); 128 | sourceDone = true; 129 | } 130 | 131 | async function* keyed( 132 | key: Key, 133 | iterable: AsyncIterable 134 | ): KeyedAsyncIterable { 135 | let value; 136 | for await (value of iterable) { 137 | yield [key, value]; 138 | } 139 | done.set(key, true); 140 | } 141 | } -------------------------------------------------------------------------------- /scripts/post-build.js: -------------------------------------------------------------------------------- 1 | import "./correct-import-extensions.js"; 2 | import { promises as fs } from "fs"; 3 | import { rollup } from "rollup"; 4 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 5 | import ignore from "rollup-plugin-ignore"; 6 | import babel from "rollup-plugin-babel"; 7 | import { dirname, resolve } from "path"; 8 | 9 | const { pathname } = new URL(import.meta.url); 10 | const cwd = resolve(dirname(pathname), ".."); 11 | 12 | { 13 | // /Volumes/Extreme/Users/fabian/src/virtualstate/esnext/tests/app-history.playwright.wpt.js 14 | // /Volumes/Extreme/Users/fabian/src/virtualstate/app-history/esnext/tests/app-history.playwright.wpt.js 15 | 16 | console.log({ 17 | cwd, 18 | path: 19 | `/Volumes/Extreme/Users/fabian/src/virtualstate/app-history/esnext/tests/app-history.playwright.wpt.js` === 20 | `${cwd}/esnext/tests/app-history.playwright.wpt.js`, 21 | p: `${cwd}/esnext/tests/app-history.playwright.wpt.js`, 22 | }); 23 | 24 | const bundle = await rollup({ 25 | input: "./esnext/tests/index.js", 26 | plugins: [ 27 | ignore([ 28 | "playwright", 29 | "fs", 30 | "path", 31 | "uuid", 32 | "cheerio", 33 | "@virtualstate/app-history", 34 | "@virtualstate/app-history-imported", 35 | `${cwd}/esnext/tests/app-history.playwright.js`, 36 | `${cwd}/esnext/tests/app-history.playwright.wpt.js`, 37 | `${cwd}/esnext/tests/dependencies-input.js`, 38 | `${cwd}/esnext/tests/dependencies.js`, 39 | "./app-history.playwright.js", 40 | "./app-history.playwright.wpt.js", 41 | ]), 42 | nodeResolve(), 43 | ], 44 | inlineDynamicImports: true, 45 | treeshake: { 46 | preset: "smallest", 47 | moduleSideEffects: "no-external", 48 | }, 49 | }); 50 | await bundle.write({ 51 | sourcemap: true, 52 | output: { 53 | file: "./esnext/tests/rollup.js", 54 | }, 55 | inlineDynamicImports: true, 56 | format: "cjs", 57 | interop: "auto", 58 | globals: { 59 | "esnext/tests/app-history.playwright.js": "globalThis", 60 | }, 61 | }); 62 | } 63 | 64 | if (!process.env.NO_COVERAGE_BADGE_UPDATE) { 65 | const badges = []; 66 | 67 | const { name } = await fs.readFile("package.json", "utf-8").then(JSON.parse); 68 | 69 | badges.push( 70 | "### Support\n\n", 71 | "![Node.js supported](https://img.shields.io/badge/node-%3E%3D16.0.0-blue)", 72 | "![Deno supported](https://img.shields.io/badge/deno-%3E%3D1.17.0-blue)", 73 | // "![Chromium supported](https://img.shields.io/badge/chromium-%3E%3D98.0.4695.0-blue)", 74 | // "![Webkit supported](https://img.shields.io/badge/webkit-%3E%3D15.4-blue)", 75 | // "![Firefox supported](https://img.shields.io/badge/firefox-%3E%3D94.0.1-blue)" 76 | ); 77 | 78 | badges.push( 79 | "\n\n### Test Coverage\n\n" 80 | // `![nycrc config on GitHub](https://img.shields.io/nycrc/${name.replace(/^@/, "")})` 81 | ); 82 | 83 | // const wptResults = await fs 84 | // .readFile("coverage/wpt.results.json", "utf8") 85 | // .then(JSON.parse) 86 | // .catch(() => ({})); 87 | // if (wptResults?.total) { 88 | // const message = `${wptResults.pass}/${wptResults.total}`; 89 | // const name = "Web Platform Tests"; 90 | // badges.push( 91 | // `![${name} ${message}](https://img.shields.io/badge/${encodeURIComponent( 92 | // name 93 | // )}-${encodeURIComponent(message)}-brightgreen)` 94 | // ); 95 | // } 96 | 97 | const coverage = await fs 98 | .readFile("coverage/coverage-summary.json", "utf8") 99 | .then(JSON.parse) 100 | .catch(() => ({})); 101 | const coverageConfig = await fs.readFile(".nycrc", "utf8").then(JSON.parse); 102 | for (const [name, { pct }] of Object.entries(coverage?.total ?? {})) { 103 | const good = coverageConfig[name]; 104 | if (!good) continue; // not configured 105 | const color = pct >= good ? "brightgreen" : "yellow"; 106 | const message = `${pct}%25`; 107 | badges.push( 108 | `![${message} ${name} covered](https://img.shields.io/badge/${name}-${message}-${color})` 109 | ); 110 | } 111 | 112 | const tag = "[//]: # (badges)"; 113 | 114 | const readMe = await fs.readFile("README.md", "utf8"); 115 | const badgeStart = readMe.indexOf(tag); 116 | const badgeStartAfter = badgeStart + tag.length; 117 | if (badgeStart === -1) { 118 | throw new Error(`Expected to find "${tag}" in README.md`); 119 | } 120 | const badgeEnd = badgeStartAfter + readMe.slice(badgeStartAfter).indexOf(tag); 121 | const badgeEndAfter = badgeEnd + tag.length; 122 | const readMeBefore = readMe.slice(0, badgeStart); 123 | const readMeAfter = readMe.slice(badgeEndAfter); 124 | 125 | const readMeNext = `${readMeBefore}${tag}\n\n${badges.join( 126 | " " 127 | )}\n\n${tag}${readMeAfter}`; 128 | await fs.writeFile("README.md", readMeNext); 129 | console.log("Wrote coverage badges!"); 130 | } 131 | -------------------------------------------------------------------------------- /src/split/type.ts: -------------------------------------------------------------------------------- 1 | import { TheAsyncThing } from "../the-thing"; 2 | import { isLike, ok } from "../like"; 3 | import { PushOptions } from "../push"; 4 | 5 | export type Name = unknown; 6 | 7 | export interface FilterFn { 8 | (value: T, index: number, array: T[]): boolean; 9 | } 10 | export interface FilterIsFn { 11 | (value: T, index: number, array: T[]): value is Z; 12 | } 13 | 14 | export interface MapFn { 15 | (value: T, index: number, array: T[]): Promise | M; 16 | } 17 | 18 | export type AsyncMapEntry> = [K, A]; 19 | 20 | export interface AsyncMap> { 21 | get(key: K): V; 22 | set(key: K, value: V): never; 23 | delete(key: K): never; 24 | has(key: K): TheAsyncThing; 25 | size: never; 26 | } 27 | 28 | export type SplitInputAsyncIterable = AsyncIterable; 29 | 30 | export interface SplitInputFn { 31 | (...args: unknown[]): SplitInputAsyncIterable; 32 | } 33 | 34 | export type SplitInput = SplitInputAsyncIterable | SplitInputFn; 35 | 36 | export interface SplitProxyOptions { 37 | proxy: true; 38 | } 39 | 40 | export interface SplitAssertFn { 41 | (value: unknown): asserts value is T; 42 | } 43 | export interface SplitIsFn { 44 | (value: unknown): value is T; 45 | } 46 | 47 | export interface SplitOptions extends PushOptions, Partial { 48 | empty?: boolean; 49 | } 50 | 51 | export type TypedBaseSplitOptions = 52 | | { 53 | is: SplitIsFn; 54 | } 55 | | { 56 | assert: SplitAssertFn; 57 | }; 58 | 59 | export type TypedSplitOptions = SplitOptions & TypedBaseSplitOptions; 60 | 61 | export type SplitConcatSyncInput = T | T[] | Iterable; 62 | export type SplitConcatInput = 63 | | AsyncIterable> 64 | | SplitConcatSyncInput; 65 | 66 | export interface SplitAsyncIterable 67 | extends Iterable>, 68 | AsyncIterable { 69 | filter(fn: FilterFn): AsyncIterable; 70 | filter(fn: FilterIsFn): AsyncIterable; 71 | filter(fn: FilterIsFn): AsyncIterable; 72 | find(fn: FilterFn): TheAsyncThing; 73 | find(fn: FilterIsFn): TheAsyncThing; 74 | find(fn: FilterIsFn): TheAsyncThing; 75 | findIndex(fn: FilterFn): TheAsyncThing; 76 | copyWithin(target: number, start?: number, end?: number): AsyncIterable; 77 | map(fn: MapFn): AsyncIterable; 78 | take(count: number): AsyncIterable; 79 | entries(): AsyncIterable<[number, T][]>; 80 | flatMap(fn: MapFn): AsyncIterable; 81 | at(index: number): TheAsyncThing; 82 | includes(search: T, fromIndex?: number): TheAsyncThing; 83 | every(fn: FilterFn): TheAsyncThing; 84 | reverse(): AsyncIterable; 85 | concat(...args: SplitConcatSyncInput[]): AsyncIterable; 86 | concat(other: SplitConcatInput): AsyncIterable; 87 | mask(mask: AsyncIterable): AsyncIterable; 88 | push(other: SplitInput): AsyncIterable; 89 | push(other: SplitInput): AsyncIterable<(M | T)[]>; 90 | call(this: unknown, ...args: unknown[]): AsyncIterable; 91 | group( 92 | fn: MapFn 93 | ): Record>; 94 | groupToMap( 95 | fn: MapFn 96 | ): AsyncMap>; 97 | bind( 98 | this: unknown, 99 | ...args: unknown[] 100 | ): (...args: unknown[]) => AsyncIterable; 101 | } 102 | 103 | export interface Split extends SplitAsyncIterable, Promise { 104 | filter(fn: FilterFn): Split; 105 | filter(fn: FilterIsFn): Split; 106 | filter(fn: FilterIsFn): Split; 107 | at(index: number): TheAsyncThing; 108 | map( 109 | fn: MapFn, 110 | options?: TypedSplitOptions | SplitOptions 111 | ): Split; 112 | take(count: number): Split; 113 | concat(...args: T[]): Split; 114 | concat(other: SplitConcatInput): Split; 115 | mask(mask: AsyncIterable): Split; 116 | copyWithin(target: number, start?: number, end?: number): Split; 117 | entries(): Split<[number, T]>; 118 | flatMap( 119 | fn: MapFn, 120 | options?: TypedSplitOptions | SplitOptions 121 | ): Split; 122 | group( 123 | fn: MapFn 124 | ): Record>; 125 | groupToMap( 126 | fn: MapFn 127 | ): AsyncMap>; 128 | reverse(): Split; 129 | push( 130 | other: SplitInput, 131 | otherOptions?: TypedSplitOptions | SplitOptions 132 | ): Split; 133 | push( 134 | other: SplitInput, 135 | otherOptions?: TypedSplitOptions | SplitOptions 136 | ): Split; 137 | call(this: unknown, ...args: unknown[]): Split; 138 | bind(this: unknown, ...args: unknown[]): SplitFn; 139 | } 140 | 141 | export function assertSplitInputFn( 142 | input: unknown 143 | ): asserts input is SplitInputFn { 144 | ok(typeof input === "function"); 145 | } 146 | 147 | export interface SplitFn extends Split { 148 | (this: unknown, ...args: unknown[]): SplitFn; 149 | call(this: unknown, ...args: unknown[]): SplitFn; 150 | bind(this: unknown, ...args: unknown[]): SplitFn; 151 | } 152 | 153 | export function isSplitAt = Split>( 154 | value: unknown 155 | ): value is Pick { 156 | return isLike>(value) && typeof value.at === "function"; 157 | } 158 | -------------------------------------------------------------------------------- /src/blend/index.ts: -------------------------------------------------------------------------------- 1 | import { PushWriter } from "../push"; 2 | 3 | export interface BlenderTargetFn { 4 | (value: T): unknown | void | Promise; 5 | } 6 | 7 | export type BlenderTarget = BlenderTargetFn | PushWriter; 8 | 9 | export interface BlendedIndexLike { 10 | source: unknown; 11 | target: unknown; 12 | } 13 | 14 | export interface BlendedIndex extends BlendedIndexLike { 15 | source: number; 16 | target: number; 17 | } 18 | 19 | export interface BlendOptions { 20 | blended?: I[]; 21 | random?: boolean; 22 | } 23 | 24 | export interface Blended extends BlendedIndex { 25 | promise: Promise; 26 | } 27 | 28 | export interface BlenderBlend { 29 | blend(options?: BlendOptions): I[]; 30 | } 31 | 32 | export interface BlenderConnect< 33 | I extends BlendedIndexLike = BlendedIndex, 34 | B = Blended 35 | > extends BlenderBlend { 36 | connect(options?: BlendOptions): B[]; 37 | } 38 | 39 | export interface Blender< 40 | T = unknown, 41 | I extends BlendedIndexLike = BlendedIndex, 42 | B = Blended 43 | > extends BlenderConnect { 44 | source(source: AsyncIterable, at?: I["source"]): I["source"]; 45 | target(target: BlenderTarget, at?: I["target"]): I["target"]; 46 | } 47 | 48 | export interface BlenderOptions extends BlendOptions { 49 | close?: boolean; 50 | } 51 | 52 | export function blend(options?: BlenderOptions): Blender { 53 | const targets: BlenderTarget[] = []; 54 | const sources: AsyncIterable[] = []; 55 | 56 | const connected = new WeakSet(); 57 | 58 | function pushAtTarget(target: number, value: T) { 59 | const writer = targets[target]; 60 | if (!writer) return; 61 | if (typeof writer === "function") { 62 | return writer(value); 63 | } 64 | return writer.push(value); 65 | } 66 | 67 | function throwAtTarget(target: number, reason: T) { 68 | const writer = targets[target]; 69 | if (!writer) return; 70 | if (typeof writer === "function") { 71 | return; 72 | } 73 | return writer.throw?.(reason); 74 | } 75 | 76 | function closeTarget(target: number) { 77 | if (!options?.close) return; 78 | const writer = targets[target]; 79 | if (!writer) return; 80 | if (typeof writer === "function") { 81 | return; 82 | } 83 | if (connected.has(writer)) { 84 | return; 85 | } 86 | return writer.close?.(); 87 | } 88 | 89 | function shouldReconnect(index: number, source: AsyncIterable) { 90 | return sources[index] !== source; 91 | } 92 | 93 | async function connect(source: number, targets: number[]): Promise { 94 | const iterable = sources[source]; 95 | if (!iterable) return; 96 | if (connected.has(iterable)) return; 97 | const reconnect = shouldReconnect.bind(undefined, source, iterable); 98 | try { 99 | connected.add(iterable); 100 | for await (const value of iterable) { 101 | if (reconnect()) break; 102 | for (const target of targets) { 103 | pushAtTarget(target, value); 104 | } 105 | if (reconnect()) break; 106 | } 107 | connected.delete(iterable); 108 | if (reconnect()) { 109 | // Swap connection 110 | return await connect(source, targets); 111 | } 112 | } catch (error) { 113 | connected.delete(iterable); 114 | for (const target of targets) { 115 | throwAtTarget(source, error); 116 | } 117 | throw await Promise.reject(error); 118 | } finally { 119 | connected.delete(iterable); 120 | } 121 | } 122 | 123 | function blend(additionalOptions?: BlendOptions) { 124 | const allOptions = { 125 | ...options, 126 | ...additionalOptions, 127 | }; 128 | const { blended: inputBlend, random } = allOptions; 129 | const result: BlendedIndex[] = []; 130 | if (inputBlend?.length) { 131 | for (const blend of inputBlend) { 132 | result.push(blend); 133 | } 134 | } 135 | if (random) { 136 | const usedTargets = new Set(result.map(({ target }) => target)); 137 | const usedSources = new Set(result.map(({ source }) => source)); 138 | const targetsRemaining = [...targets.keys()].filter( 139 | (index) => !usedTargets.has(index) 140 | ); 141 | const sourcesRemaining = [...sources.keys()].filter( 142 | (index) => !usedSources.has(index) 143 | ); 144 | while (targetsRemaining.length && sourcesRemaining.length) { 145 | const targetsRemainingIndex = Math.max( 146 | 0, 147 | Math.round(Math.random() * targetsRemaining.length - 1) 148 | ); 149 | const sourcesRemainingIndex = Math.max( 150 | 0, 151 | Math.round(Math.random() * sourcesRemaining.length - 1) 152 | ); 153 | const target = targetsRemaining[targetsRemainingIndex]; 154 | const source = sourcesRemaining[sourcesRemainingIndex]; 155 | result.push({ 156 | target, 157 | source, 158 | }); 159 | targetsRemaining.splice(targetsRemainingIndex, 1); 160 | sourcesRemaining.splice(sourcesRemainingIndex, 1); 161 | } 162 | } 163 | return result; 164 | } 165 | 166 | return { 167 | source(source, at) { 168 | if (typeof at === "number") { 169 | sources[at] = source; 170 | return at; 171 | } 172 | return sources.push(source) - 1; 173 | }, 174 | target(target, at) { 175 | if (typeof at === "number") { 176 | targets[at] = target; 177 | return at; 178 | } 179 | return targets.push(target) - 1; 180 | }, 181 | blend, 182 | connect(additionalOptions) { 183 | const blended = blend(additionalOptions); 184 | const sources = [ 185 | ...new Set(blended.map(({ source }) => source)) 186 | ]; 187 | let remaining = [...blended]; 188 | function done(finished: BlendedIndex[]) { 189 | remaining = remaining.filter(value => !finished.includes(value)) 190 | const targets = finished.map(({ target }) => target); 191 | const closeTargets = targets.filter(target => { 192 | const index = remaining.findIndex(value => value.target === target); 193 | return index === -1; 194 | }); 195 | for (const target of closeTargets) { 196 | closeTarget(target); 197 | } 198 | } 199 | return sources.flatMap((source): Blended[] => { 200 | const indexes = blended 201 | .filter(value => value.source === source); 202 | const targets = indexes 203 | .map(({ target }) => target); 204 | const promise = connect(source, targets).finally( 205 | () => done(indexes) 206 | ); 207 | return targets.map(target => ({ 208 | source, 209 | target, 210 | promise 211 | })); 212 | }); 213 | }, 214 | }; 215 | } 216 | -------------------------------------------------------------------------------- /src/line/index.ts: -------------------------------------------------------------------------------- 1 | import {Push, PushAsyncIteratorOptions, PushIteratorYieldResult, PushOptions} from "../push"; 2 | import {isIteratorYieldResult} from "../is"; 3 | import {ok} from "../like"; 4 | 5 | const Pointer = Symbol.for("@virtualstate/promise/Push/asyncIterator/pointer"); 6 | const ReverseAsyncIterator = Symbol.for("@virtualstate/promise/Push/reverseAsyncIterator"); 7 | 8 | function isPushIteratorResult(value: unknown): value is PushIteratorYieldResult & { [Pointer]?: object } { 9 | return isIteratorYieldResult(value); 10 | } 11 | 12 | const asyncIterator = Push.prototype[Symbol.asyncIterator]; 13 | 14 | export interface LineIterator extends AsyncIterableIterator { 15 | previous(): Promise> 16 | } 17 | 18 | /** 19 | * @experimental 20 | */ 21 | export class Line extends Push { 22 | 23 | static reverse(line: Line) { 24 | return line[ReverseAsyncIterator](); 25 | } 26 | 27 | constructor(options?: Omit) { 28 | super({ 29 | keep: true, 30 | ...options 31 | }); 32 | } 33 | 34 | [ReverseAsyncIterator] = (): AsyncIterableIterator => { 35 | let iterator: LineIterator; 36 | const line = this; 37 | return { 38 | async next() { 39 | if (!iterator) { 40 | iterator = line[Symbol.asyncIterator](); 41 | let result; 42 | do { 43 | // "Load" our iterator... yeah, we want to reach the end. 44 | // If we never reach the end, we can not read from the end 45 | // This only applies to async iterators 46 | // 47 | // If you don't want to go through this loading and manually 48 | // control the cycle of the iterator, 49 | result = await iterator.next(); 50 | } while (!result.done); 51 | } 52 | return iterator.previous(); 53 | }, 54 | async return() { 55 | return iterator?.return?.(); 56 | }, 57 | async *[Symbol.asyncIterator]() { 58 | let result; 59 | do { 60 | result = await this.next(); 61 | if (isIteratorYieldResult(result)) { 62 | yield result.value; 63 | } 64 | } while (!result.done); 65 | await this.return?.(); 66 | } 67 | } 68 | } 69 | 70 | [Symbol.asyncIterator] = (options?: PushAsyncIteratorOptions): LineIterator => { 71 | const { values } = this; 72 | let iterator: AsyncIterator; 73 | 74 | const pointers: object[] = []; 75 | 76 | let index: number = -1; 77 | let done = false; 78 | 79 | // If we are already closed, we can make a jump 80 | /** 81 | * @experimental it works though 82 | */ 83 | if (!this.open) { 84 | pointers.push(...this.resolvedPointers); 85 | index = -2; 86 | done = true; 87 | } else { 88 | iterator = asyncIterator.call(this,{ 89 | [Pointer]: true, 90 | ...options 91 | }) 92 | } 93 | 94 | function getIndexDeferred(index: number) { 95 | const pointer = pointers[index]; 96 | if (pointer) { 97 | const { 98 | value: { 99 | deferred 100 | } 101 | } = values.get(pointer); 102 | ok(deferred); 103 | ok(deferred.settled); 104 | ok(deferred.status === "fulfilled"); 105 | return deferred; 106 | } 107 | } 108 | 109 | return { 110 | async next() { 111 | if (index !== -1) { 112 | let nextIndex; 113 | // -2 means we got to the start again, so lets try from 114 | // the start of the list 115 | if (index === -2) { 116 | nextIndex = 0; 117 | } else { 118 | nextIndex = index + 1; 119 | } 120 | const deferred = getIndexDeferred(nextIndex); 121 | if (deferred) { 122 | index = nextIndex; 123 | const value = await deferred.promise; 124 | return { 125 | value, 126 | done: false 127 | } 128 | } 129 | // If we don't have a deferred, we are adding to the end 130 | } 131 | if (done) { 132 | return this.return(); 133 | } 134 | const result = await iterator.next(); 135 | if (result.done) { 136 | done = true; 137 | } 138 | if (done) { 139 | return this.return(); 140 | } 141 | if (isPushIteratorResult(result)) { 142 | const pointer = result[Pointer]; 143 | ok(pointer); 144 | // The current length matches the next index 145 | index = pointers.length; 146 | pointers.push(pointer); 147 | } 148 | return result; 149 | }, 150 | async previous() { 151 | const nextIndex = index - 1; 152 | const deferred = getIndexDeferred(nextIndex); 153 | if (!deferred) { 154 | // Mark that we tried to go further backwards than available 155 | index = -2; 156 | return { done: true, value: undefined }; 157 | } 158 | index = nextIndex; 159 | const value = await deferred.promise; 160 | return { 161 | value, 162 | done: false 163 | }; 164 | }, 165 | async return() { 166 | done = true; 167 | index = pointers.length; 168 | if (iterator?.return) { 169 | await iterator.return(); 170 | } 171 | iterator = undefined; 172 | return { done: true, value: undefined } 173 | }, 174 | async * [Symbol.asyncIterator]() { 175 | let result; 176 | do { 177 | result = await this.next(); 178 | if (isIteratorYieldResult(result)) { 179 | yield result.value 180 | } 181 | } while (!result.done); 182 | await this.return(); 183 | } 184 | } 185 | 186 | } 187 | 188 | 189 | } -------------------------------------------------------------------------------- /src/tests/blend.ts: -------------------------------------------------------------------------------- 1 | import { blend } from "../blend"; 2 | import { all } from "../all"; 3 | import { ok } from "../like"; 4 | import { Push } from "../push"; 5 | import { union } from "@virtualstate/union"; 6 | import {events} from "./iterable-lifecycle"; 7 | import {asyncIterableLifecycle} from "../iterable-lifecycle"; 8 | 9 | { 10 | const blender = blend({ random: true }); 11 | 12 | blender.source({ 13 | async *[Symbol.asyncIterator]() { 14 | yield 1; 15 | yield 1; 16 | yield 1; 17 | yield 1; 18 | }, 19 | }); 20 | blender.source({ 21 | async *[Symbol.asyncIterator]() { 22 | yield 2; 23 | yield 3; 24 | yield 4; 25 | yield 5; 26 | }, 27 | }); 28 | blender.source({ 29 | async *[Symbol.asyncIterator]() { 30 | yield 9; 31 | yield 6; 32 | yield 3; 33 | yield 1; 34 | }, 35 | }); 36 | blender.target(console.log); 37 | blender.target(console.log); 38 | blender.target(console.log); 39 | 40 | const blended = blender.connect(); 41 | 42 | console.log({ blended }); 43 | 44 | await all(...blended.map(({ promise }) => promise)); 45 | } 46 | 47 | { 48 | const blender = blend({ random: true }); 49 | 50 | const expectedInitial = Symbol("Expected initial symbol"); 51 | const unexpected = Symbol("Unexpected symbol"); 52 | 53 | const replace = blender.source({ 54 | async *[Symbol.asyncIterator]() { 55 | yield expectedInitial; 56 | await new Promise((resolve) => setTimeout(resolve, 200)); 57 | yield unexpected; 58 | yield unexpected; 59 | yield unexpected; 60 | yield unexpected; 61 | }, 62 | }); 63 | 64 | const expectedSecond = Symbol("Expected second symbol"); 65 | const expectedThird = Symbol("Expected third symbol"); 66 | const expectedFourth = Symbol("Expected fourth symbol"); 67 | 68 | blender.source({ 69 | async *[Symbol.asyncIterator]() { 70 | yield expectedSecond; 71 | await new Promise((resolve) => setTimeout(resolve, 200)); 72 | yield expectedThird; 73 | yield expectedFourth; 74 | }, 75 | }); 76 | 77 | const results: unknown[] = []; 78 | 79 | blender.target((value) => results.push(value)); 80 | blender.target((value) => results.push(value)); 81 | 82 | const blended = blender.connect(); 83 | 84 | console.log({ blended }); 85 | 86 | const expected = Symbol("Expected Symbol"); 87 | 88 | await all([ 89 | all(blended.map(({ promise }) => promise)), 90 | async () => { 91 | await new Promise((resolve) => setTimeout(resolve, 5)); 92 | blender.source( 93 | { 94 | async *[Symbol.asyncIterator]() { 95 | yield expected; 96 | }, 97 | }, 98 | replace 99 | ); 100 | }, 101 | ]); 102 | 103 | console.log({ results }); 104 | 105 | ok(results.includes(expectedInitial)); 106 | ok(results.includes(expected)); 107 | ok(results.includes(expectedSecond)); 108 | ok(results.includes(expectedThird)); 109 | ok(results.includes(expectedFourth)); 110 | ok(!results.includes(unexpected)); 111 | } 112 | 113 | { 114 | const blender = blend({ close: true }); 115 | 116 | const source = new Push(); 117 | source.push(1); 118 | source.push(2); 119 | source.close(); 120 | 121 | const sourceIndex = blender.source(source); 122 | 123 | const target = new Push(); 124 | const targetIndex = blender.target(target); 125 | 126 | const [{ promise }] = blender.connect({ 127 | blended: [ 128 | { 129 | source: sourceIndex, 130 | target: targetIndex, 131 | }, 132 | ], 133 | }); 134 | 135 | // Target will now be loaded 136 | await promise; 137 | 138 | const results = []; 139 | 140 | for await (const snapshot of target) { 141 | results.push(snapshot); 142 | } 143 | 144 | console.log(results); 145 | 146 | ok(results.length === 2); 147 | ok(results[0] === 1); 148 | ok(results[1] === 2); 149 | } 150 | 151 | { 152 | const blender = blend({ close: true }); 153 | 154 | const lifecycle = events(); 155 | const source = new Push(); 156 | source.push(1); 157 | source.push(2); 158 | source.close(); 159 | 160 | const sourceIndex = blender.source(asyncIterableLifecycle(source, lifecycle)); 161 | 162 | const targetA = new Push(); 163 | const targetAIndex = blender.target(targetA); 164 | const targetB = new Push(); 165 | const targetBIndex = blender.target(targetB); 166 | 167 | const [{ promise: promiseA }, { promise: promiseB }] = blender.connect({ 168 | blended: [ 169 | { 170 | source: sourceIndex, 171 | target: targetAIndex, 172 | }, 173 | { 174 | source: sourceIndex, 175 | target: targetBIndex, 176 | }, 177 | ], 178 | }); 179 | 180 | // Target will now be loaded 181 | await Promise.all([promiseA, promiseB]); 182 | 183 | const initialEvents = lifecycle.events.length; 184 | ok(initialEvents); 185 | 186 | const results = []; 187 | 188 | for await (const snapshot of union([targetA, targetB])) { 189 | results.push(snapshot); 190 | } 191 | 192 | console.log(results); 193 | ok(initialEvents === lifecycle.events.length); 194 | 195 | ok(results.length === 2); 196 | ok(results[0][0] === 1); 197 | ok(results[0][1] === 1); 198 | ok(results[1][0] === 2); 199 | ok(results[1][1] === 2); 200 | 201 | // The source should have been read once 202 | ok((((results.length * 2) + 4)) === initialEvents); 203 | } 204 | 205 | { 206 | const lifecycleA = events(); 207 | const lifecycleB = events(); 208 | 209 | const blender = blend({ close: true }); 210 | 211 | const sourceA = new Push(); 212 | sourceA.push(1); 213 | sourceA.push(2); 214 | sourceA.close(); 215 | const sourceAIndex = blender.source(asyncIterableLifecycle(sourceA, lifecycleA)); 216 | 217 | const sourceB = new Push(); 218 | sourceB.push(3); 219 | sourceB.push(4); 220 | sourceB.close(); 221 | const sourceBIndex = blender.source(asyncIterableLifecycle(sourceB, lifecycleB)); 222 | 223 | const target = new Push(); 224 | const targetIndex = blender.target(target); 225 | 226 | const [{ promise }] = blender.connect({ 227 | blended: [ 228 | { 229 | source: sourceAIndex, 230 | target: targetIndex, 231 | }, 232 | { 233 | source: sourceBIndex, 234 | target: targetIndex, 235 | }, 236 | ], 237 | }); 238 | 239 | // Target will now be loaded 240 | await promise; 241 | 242 | const initialEventsA = lifecycleA.events.length; 243 | const initialEventsB = lifecycleB.events.length; 244 | 245 | // Ensure we have already read our sources by this point 246 | ok(initialEventsA); 247 | ok(initialEventsB); 248 | ok(initialEventsA === initialEventsB); 249 | 250 | const results = []; 251 | 252 | for await (const snapshot of target) { 253 | results.push(snapshot); 254 | } 255 | 256 | console.log({ 257 | results, 258 | A: lifecycleA.events.length, 259 | B: lifecycleB.events.length 260 | }); 261 | 262 | // Ensure we haven't read again from our sources 263 | ok(initialEventsA === lifecycleA.events.length); 264 | ok(initialEventsB === lifecycleB.events.length); 265 | 266 | ok(results.length === 4); 267 | ok(results.includes(1)); 268 | ok(results.includes(2)); 269 | ok(results.includes(3)); 270 | ok(results.includes(4)); 271 | } 272 | -------------------------------------------------------------------------------- /src/iterable-lifecycle.ts: -------------------------------------------------------------------------------- 1 | import {isIteratorResult, isIteratorYieldResult, isPromise} from "./is"; 2 | 3 | /** 4 | * @experimental 5 | * @internal 6 | */ 7 | export interface IterableLifecycleIterator< 8 | T, 9 | Result = IteratorResult | undefined 10 | > { 11 | next(arg?: unknown, arg2?: unknown): Result; 12 | return?(arg?: unknown, arg2?: unknown): Result; 13 | throw?(reason?: unknown, arg2?: unknown): Result; 14 | } 15 | 16 | /** 17 | * @experimental 18 | * @internal 19 | */ 20 | export interface IterableLifecycle< 21 | T, 22 | I extends IterableLifecycleIterator = IterableLifecycleIterator 23 | > { 24 | next: Omit | I["next"]; 25 | return?: Omit | I["return"]; 26 | throw?: I["throw"]; 27 | } 28 | 29 | interface IteratorLifecycleContext = IterableLifecycleIterator> { 30 | iterator: I, 31 | lifecycle: IterableLifecycle, 32 | result?: IteratorResult; 33 | returned?: unknown; 34 | done: boolean; 35 | } 36 | 37 | interface AsyncIteratorLifecycleContext extends IteratorLifecycleContext> { 38 | 39 | } 40 | 41 | interface SyncIteratorLifecycleContext extends IteratorLifecycleContext> { 42 | 43 | } 44 | 45 | function *iterateLifecycle>(context: C): Iterable { 46 | try { 47 | do { 48 | yield * lifecycleNext(); 49 | yield * lifecycleIteratorNext(); 50 | yield * lifecycleNextReturn(); 51 | } while (!context.done); 52 | } catch (reason) { 53 | yield * lifecycleThrow(reason); 54 | yield * lifecycleIteratorThrow(reason); 55 | } finally { 56 | yield * lifecycleReturn(); 57 | yield * lifecycleIteratorReturn(); 58 | yield * lifecycleReturnReturn(); 59 | } 60 | 61 | function *lifecycleNext() { 62 | if (typeof context.lifecycle.next === "function") { 63 | yield context.lifecycle.next(context.returned); 64 | } else if (context.lifecycle.next?.next) { 65 | yield context.lifecycle.next.next(context.returned); 66 | } 67 | } 68 | 69 | function *lifecycleIteratorNext() { 70 | yield context.iterator.next(context.returned); 71 | } 72 | 73 | function *lifecycleIteratorThrow(reason: unknown) { 74 | yield context.iterator.throw?.(reason, context.result); 75 | } 76 | 77 | function *lifecycleIteratorReturn() { 78 | yield context.iterator.return?.(context.returned, context.result); 79 | } 80 | 81 | function *lifecycleNextReturn() { 82 | if (typeof context.lifecycle.next !== "function" && context.lifecycle.next.return) { 83 | yield context.lifecycle.next.return(context.returned, context.result); 84 | } 85 | } 86 | 87 | function *lifecycleReturn() { 88 | if (typeof context.lifecycle.return === "function") { 89 | yield context.lifecycle.return(context.returned); 90 | } else if (context.lifecycle.return?.next) { 91 | yield context.lifecycle.return.next(context.returned); 92 | } 93 | } 94 | 95 | function *lifecycleReturnReturn() { 96 | if (typeof context.lifecycle.return !== "function" && context.lifecycle.return?.return) { 97 | yield context.lifecycle.return.return(context.returned); 98 | } 99 | } 100 | 101 | function *lifecycleThrow(reason: unknown) { 102 | if (typeof context.lifecycle.throw === "function") { 103 | yield context.lifecycle.throw(reason, context.result); 104 | } 105 | } 106 | } 107 | 108 | /** 109 | * @param iterable 110 | * @param lifecycle 111 | * @experimental 112 | * @internal 113 | */ 114 | export function iterableLifecycle( 115 | iterable: Iterable, 116 | lifecycle: IterableLifecycle 117 | ): Iterable { 118 | return { 119 | [Symbol.iterator]() { 120 | const context: SyncIteratorLifecycleContext = { 121 | done: false, 122 | iterator: iterable[Symbol.iterator](), 123 | lifecycle 124 | }; 125 | const lifecycleIterator: IterableLifecycleIterator = iterateLifecycle(context)[Symbol.iterator](); 126 | const iterator: Iterator = { 127 | next(returned: unknown): IteratorResult { 128 | context.returned = returned; 129 | const lifecycleResult = lifecycleIterator.next(); 130 | if (!isIteratorYieldResult(lifecycleResult)) { 131 | return { done: true, value: undefined }; 132 | } 133 | const result = lifecycleResult.value; 134 | if (isIteratorResult(result)) { 135 | if (isIteratorYieldResult(result)) { 136 | return context.result = result; 137 | } else if (result.done) { 138 | context.done = true; 139 | } 140 | } 141 | return iterator.next(returned); 142 | }, 143 | return(returned: unknown) { 144 | context.returned = returned ?? context.returned; 145 | context.done = true; 146 | const result = lifecycleIterator.return(); 147 | if (isIteratorResult(result)) { 148 | return result; 149 | } 150 | return { done: true, value: undefined }; 151 | }, 152 | throw(reason: unknown) { 153 | context.done = true; 154 | const result = lifecycleIterator.throw(reason); 155 | if (isIteratorResult(result)) { 156 | return result; 157 | } 158 | return { done: true, value: undefined }; 159 | } 160 | } 161 | return iterator; 162 | } 163 | } 164 | } 165 | 166 | 167 | /** 168 | * @param iterable 169 | * @param lifecycle 170 | * @experimental 171 | * @internal 172 | */ 173 | export function asyncIterableLifecycle( 174 | iterable: AsyncIterable, 175 | lifecycle: IterableLifecycle 176 | ): AsyncIterable { 177 | return { 178 | [Symbol.asyncIterator]() { 179 | const context: AsyncIteratorLifecycleContext = { 180 | done: false, 181 | iterator: iterable[Symbol.asyncIterator](), 182 | lifecycle 183 | }; 184 | const lifecycleIterator: IterableLifecycleIterator = iterateLifecycle(context)[Symbol.iterator](); 185 | const iterator: AsyncIterator = { 186 | async next(returned: unknown): Promise> { 187 | context.returned = returned; 188 | const lifecycleResult = lifecycleIterator.next(); 189 | if (!isIteratorYieldResult(lifecycleResult)) { 190 | return { done: true, value: undefined }; 191 | } 192 | let result = lifecycleResult.value; 193 | if (isPromise(result)) { 194 | result = await result; 195 | } 196 | if (isIteratorResult(result)) { 197 | if (isIteratorYieldResult(result)) { 198 | return context.result = result; 199 | } else if (result.done) { 200 | context.done = true; 201 | } 202 | } 203 | return iterator.next(returned); 204 | }, 205 | async return(returned: unknown) { 206 | context.returned = returned ?? context.returned; 207 | context.done = true; 208 | const result = lifecycleIterator.return(); 209 | if (isIteratorResult(result)) { 210 | return result; 211 | } 212 | return { done: true, value: undefined }; 213 | }, 214 | async throw(reason: unknown) { 215 | context.done = true; 216 | const result = lifecycleIterator.throw(reason); 217 | if (isIteratorResult(result)) { 218 | return result; 219 | } 220 | return { done: true, value: undefined }; 221 | } 222 | } 223 | return iterator; 224 | } 225 | } 226 | } 227 | 228 | -------------------------------------------------------------------------------- /src/push/index.ts: -------------------------------------------------------------------------------- 1 | import { WeakLinkedList } from "./weak-linked-list"; 2 | import { defer, Deferred } from "../defer"; 3 | import { ok } from "../like"; 4 | import {anAsyncThing} from "../the-thing"; 5 | 6 | const Pointer = Symbol.for("@virtualstate/promise/Push/asyncIterator/pointer"); 7 | 8 | export interface PushAsyncIterableIterator extends AsyncIterableIterator { 9 | clone(): PushAsyncIterableIterator 10 | } 11 | 12 | export interface PushAsyncIteratorOptions extends Record { 13 | /** 14 | * @internal 15 | */ 16 | [Pointer]?: boolean | object 17 | } 18 | 19 | export interface PushFn extends Push, Promise { 20 | (value: T): unknown; 21 | } 22 | 23 | export type PushIteratorYieldResult = IteratorYieldResult & Record 24 | export type PushIteratorResult = IteratorResult | PushIteratorYieldResult 25 | 26 | export function p(options?: PushOptions): PushFn { 27 | return createPushFn(options); 28 | } 29 | 30 | export function createPushFn(options?: PushOptions) { 31 | const target = new Push(options); 32 | const async = anAsyncThing(target); 33 | const unknown: object = function push(value: T) { 34 | return target.push(value); 35 | } 36 | define(unknown); 37 | return unknown; 38 | 39 | function define(push: object): asserts push is PushFn { 40 | Object.defineProperties(push, { 41 | [Symbol.asyncIterator]: { 42 | value: target[Symbol.asyncIterator].bind(target) 43 | }, 44 | push: { 45 | value: target.push.bind(target) 46 | }, 47 | close: { 48 | value: target.close.bind(target) 49 | }, 50 | throw: { 51 | value: target.throw.bind(target) 52 | }, 53 | break: { 54 | value: target.break.bind(target) 55 | }, 56 | active: { 57 | get() { 58 | return target.active; 59 | } 60 | }, 61 | open: { 62 | get() { 63 | return target.open; 64 | } 65 | }, 66 | then: { 67 | value: async.then.bind(async) 68 | }, 69 | catch: { 70 | value: async.catch.bind(async) 71 | }, 72 | finally: { 73 | value: async.finally.bind(async) 74 | } 75 | }) 76 | } 77 | 78 | 79 | } 80 | 81 | export interface PushOptions { 82 | keep?: boolean; 83 | } 84 | 85 | interface PushPair { 86 | deferred: Deferred; 87 | waiting: Deferred; 88 | } 89 | 90 | export interface PushWriter { 91 | readonly active?: boolean; 92 | readonly open?: boolean; 93 | push(value: T): unknown; 94 | throw?(reason?: unknown): unknown; 95 | close?(): unknown; 96 | break?(): unknown; 97 | wait?(): Promise; 98 | } 99 | 100 | export class Push implements AsyncIterable, PushWriter { 101 | /** 102 | * @internal 103 | * @protected 104 | */ 105 | protected values = new WeakLinkedList>(); 106 | 107 | /** 108 | * @internal 109 | * @protected 110 | */ 111 | protected pointer: object = {}; 112 | /** 113 | * @internal 114 | * @private 115 | */ 116 | private previous: object | undefined = undefined; 117 | /** 118 | * @internal 119 | * @private 120 | */ 121 | private closed: object | undefined = undefined; 122 | /** 123 | * @internal 124 | * @private 125 | */ 126 | private complete = false; 127 | /** 128 | * @internal 129 | * @private 130 | */ 131 | private microtask: object | undefined = undefined; 132 | 133 | /** 134 | * @internal 135 | * @private 136 | */ 137 | private hold: object; 138 | 139 | /** 140 | * @internal 141 | * @private 142 | */ 143 | private get resolvedPointer() { 144 | // Setting hold to undefined clears the initial pointer 145 | // available meaning this push instance's weak list can 146 | // start to forget deferred values 147 | // 148 | // If a pointer is available in sameMicrotask, or 149 | // an iterator as a pointer still, the pointers 150 | // value will still be available 151 | return this.hold ?? this.microtask ?? this.pointer; 152 | } 153 | 154 | /** 155 | * @internal 156 | * @protected 157 | */ 158 | protected get resolvedPointers() { 159 | const pointers = []; 160 | let pointer = this.resolvedPointer; 161 | ok(pointer) 162 | let result 163 | do { 164 | result = this.values.get(pointer); 165 | if (result.next) { 166 | pointers.push(pointer); 167 | pointer = result.next; 168 | } else { 169 | pointer = undefined; 170 | } 171 | } while (pointer); 172 | return pointers; 173 | } 174 | 175 | // async iterators count is just a possible number, it doesn't 176 | // represent what is actually available in memory 177 | // some iterators may be dropped without reducing this value 178 | /** 179 | * @internal 180 | * @private 181 | */ 182 | private asyncIterators: number = 0; 183 | 184 | get active() { 185 | return this.open && this.asyncIterators > 0; 186 | } 187 | 188 | get open() { 189 | return !(this.closed ?? this.complete); 190 | } 191 | 192 | constructor(private options?: PushOptions) { 193 | this.hold = this.pointer; 194 | this._nextDeferred(); 195 | } 196 | 197 | async wait() { 198 | const { values, pointer } = this; 199 | const { 200 | value: { waiting }, 201 | } = values.get(pointer); 202 | return waiting.promise; 203 | } 204 | 205 | push(value: T): unknown { 206 | ok(this.open, "Already closed"); 207 | const { values, pointer } = this; 208 | const { 209 | value: { deferred, waiting }, 210 | } = values.get(pointer); 211 | this.previous = pointer; 212 | this.pointer = {}; 213 | this._nextDeferred(); 214 | deferred.resolve(value); 215 | if (this.active) { 216 | return waiting.promise; 217 | } else { 218 | return Promise.resolve(); 219 | } 220 | } 221 | 222 | throw(reason?: unknown): unknown { 223 | if (!this.open) return; 224 | const wasActive = this.active; 225 | const { values, pointer } = this; 226 | this.closed = pointer; 227 | const { 228 | value: { deferred, waiting }, 229 | } = values.get(pointer); 230 | deferred.reject(reason); 231 | if (wasActive) { 232 | return waiting.promise; 233 | } else { 234 | return Promise.resolve(); 235 | } 236 | } 237 | 238 | break() { 239 | this.complete = true; 240 | if (!this.open) return; 241 | const wasActive = this.active; 242 | const { values, pointer } = this; 243 | this.closed = pointer; 244 | const { 245 | value: { deferred, waiting }, 246 | } = values.get(pointer); 247 | deferred.resolve(undefined); 248 | if (wasActive) { 249 | return waiting.promise; 250 | } else { 251 | return Promise.resolve(); 252 | } 253 | } 254 | 255 | close(): unknown { 256 | if (!this.open) return; 257 | const wasActive = this.active; 258 | const { values, pointer } = this; 259 | this.closed = pointer; 260 | const { 261 | value: { deferred, waiting }, 262 | } = values.get(pointer); 263 | deferred.resolve(undefined); 264 | if (wasActive) { 265 | return waiting.promise; 266 | } else { 267 | return Promise.resolve(); 268 | } 269 | } 270 | 271 | private _nextDeferred = () => { 272 | const deferred = defer(); 273 | const waiting = defer(); 274 | const { values, pointer, previous } = this; 275 | values.insert(previous, pointer, { deferred, waiting }); 276 | if (!this.active) { 277 | void deferred.promise.catch((error) => void error); 278 | } 279 | // If we have no other pointers in this microtask 280 | // we will queue a task for them to be cleared 281 | // This allows for fading of pointers on a task by task 282 | // basis, while not keeping them around longer than they 283 | // need to be 284 | // 285 | // An async iterator created in the same microtask as a pointer 286 | // will be able to access that pointer 287 | if (!this.microtask) { 288 | this.microtask = pointer; 289 | queueMicrotask(() => { 290 | if (this.microtask === pointer) { 291 | this.microtask = undefined; 292 | } 293 | }); 294 | } 295 | }; 296 | 297 | [Symbol.asyncIterator](options?: PushAsyncIteratorOptions): AsyncIterableIterator { 298 | let pointer = this.resolvedPointer; 299 | 300 | let optionsPointer = options?.[Pointer]; 301 | if (typeof optionsPointer === "object" && this.values.has(optionsPointer)) { 302 | pointer = optionsPointer; 303 | } 304 | if (optionsPointer) { 305 | // Don't keep the external pointer if we don't need to 306 | optionsPointer = true; 307 | } 308 | 309 | this.asyncIterators += 1; 310 | if (!this.options?.keep) { 311 | this.hold = undefined; 312 | } 313 | const values = this.values; 314 | const resolved = new WeakSet(); 315 | 316 | const next = async (): Promise> => { 317 | if (this.complete || !pointer || pointer === this.closed) { 318 | return clear(); 319 | } 320 | if (resolved.has(pointer)) { 321 | const result = this.values.get(pointer); 322 | ok(result.next, "Expected next after deferred resolved"); 323 | pointer = result.next; 324 | } 325 | const { 326 | value: { deferred, waiting }, 327 | } = values.get(pointer); 328 | waiting.resolve(); 329 | const value = await deferred.promise; 330 | if (this.complete || pointer === this.closed) { 331 | return clear(); 332 | } 333 | resolved.add(pointer); 334 | const result: PushIteratorYieldResult = { done: false, value }; 335 | if (optionsPointer) { 336 | result[Pointer] = pointer; 337 | } 338 | return result; 339 | }; 340 | 341 | const clear = (): IteratorResult => { 342 | this.asyncIterators -= 1; 343 | if (!this.active && !this.options?.keep) { 344 | this.closed = undefined; 345 | this.complete = true; 346 | } 347 | 348 | function resolveWaiting(pointer: object): void { 349 | const item = values.get(pointer); 350 | if (!item) return; 351 | const { 352 | value: { waiting }, 353 | next, 354 | } = item; 355 | waiting.resolve(undefined); 356 | if (!next) return; 357 | return resolveWaiting(next); 358 | } 359 | 360 | if (pointer) { 361 | resolveWaiting(pointer); 362 | pointer = undefined; 363 | } 364 | return { done: true, value: undefined }; 365 | }; 366 | 367 | function getClone(source: AsyncIterableIterator): PushAsyncIterableIterator { 368 | return { 369 | next() { 370 | return source.next() 371 | }, 372 | async return() { 373 | // Explicitly not finishing the core iterable as this is a clone 374 | return { done: true, value: undefined } 375 | }, 376 | [Symbol.asyncIterator]() { 377 | return iterator; 378 | }, 379 | clone() { 380 | // We could return iterator here, but better to give a new 381 | // instance which is what would be expected here. 382 | return getClone(source); 383 | } 384 | } 385 | } 386 | 387 | const iterator: PushAsyncIterableIterator = { 388 | next, 389 | async return() { 390 | return clear(); 391 | }, 392 | [Symbol.asyncIterator]() { 393 | return iterator; 394 | }, 395 | clone() { 396 | return getClone(iterator); 397 | } 398 | }; 399 | 400 | return iterator; 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /src/tests/split.ts: -------------------------------------------------------------------------------- 1 | import { split } from "../split"; 2 | import { ok } from "../like"; 3 | import { union } from "@virtualstate/union"; 4 | import {isArray, isAsyncIterable} from "../is"; 5 | 6 | { 7 | const [a, b, c] = split({ 8 | async *[Symbol.asyncIterator]() { 9 | yield [1, 2, 3]; 10 | yield [4, 5, 6]; 11 | }, 12 | }); 13 | 14 | let total = 0; 15 | 16 | for await (const snapshot of a) { 17 | console.log(snapshot); 18 | ok(snapshot === 1 || snapshot === 4); 19 | total += 1; 20 | } 21 | for await (const snapshot of b) { 22 | console.log(snapshot); 23 | ok(snapshot === 2 || snapshot === 5); 24 | total += 1; 25 | } 26 | for await (const snapshot of c) { 27 | console.log(snapshot); 28 | ok(snapshot === 3 || snapshot === 6); 29 | total += 1; 30 | } 31 | // Cannot double read same iterable after complete 32 | for await (const snapshot of c) { 33 | console.log({ snapshot }); 34 | ok(false, "should not get here"); 35 | total += 1; 36 | } 37 | 38 | console.log({ total }); 39 | ok(total === 6); 40 | } 41 | 42 | { 43 | const read = split({ 44 | async *[Symbol.asyncIterator]() { 45 | yield [1, 2, 3]; 46 | yield [4, 5, 6]; 47 | }, 48 | }); 49 | 50 | for await (const snapshot of read) { 51 | console.log({ snapshot }); 52 | ok(Array.isArray(snapshot)); 53 | ok(snapshot.length === 3); 54 | ok( 55 | (snapshot[0] === 1 && snapshot[1] === 2 && snapshot[2] === 3) || 56 | (snapshot[0] === 4 && snapshot[1] === 5 && snapshot[2] === 6) 57 | ); 58 | } 59 | } 60 | 61 | { 62 | const read = split({ 63 | async *[Symbol.asyncIterator]() { 64 | yield [1, 2, 3]; 65 | yield [4, 5, 6]; 66 | }, 67 | }); 68 | 69 | const result = await read; 70 | console.log({ result }); 71 | ok(Array.isArray(result)); 72 | ok(result[0] === 4); 73 | ok(result[1] === 5); 74 | ok(result[2] === 6); 75 | } 76 | 77 | { 78 | const read = split({ 79 | async *[Symbol.asyncIterator]() { 80 | yield [1, 2, 3]; 81 | yield [4, 5, 6]; 82 | }, 83 | }); 84 | 85 | const five = await read.filter((value) => value === 5); 86 | console.log({ five }); 87 | ok(Array.isArray(five)); 88 | ok(five[0] === 5); 89 | } 90 | 91 | { 92 | const read = split({ 93 | async *[Symbol.asyncIterator]() { 94 | yield [1, 2, 3]; 95 | yield [4, 5, 6]; 96 | }, 97 | }); 98 | 99 | const even = await read.filter((value) => value % 2 === 0); 100 | console.log({ even }); 101 | ok(Array.isArray(even)); 102 | ok(!even.includes(2)); // Only includes the final snapshot 103 | ok(even.includes(4)); 104 | ok(even.includes(6)); 105 | 106 | ok(!even.includes(1)); 107 | ok(!even.includes(3)); 108 | ok(!even.includes(5)); 109 | } 110 | 111 | { 112 | const read = split({ 113 | async *[Symbol.asyncIterator]() { 114 | yield [1, 2, 3]; 115 | yield [4, 5, 6]; 116 | }, 117 | }); 118 | 119 | for await (const even of read.filter((value) => value % 2 === 0)) { 120 | console.log({ even }); 121 | ok(Array.isArray(even)); 122 | ok(even.includes(2) || (even.includes(4) && even.includes(6))); 123 | ok(!even.includes(1)); 124 | ok(!even.includes(3)); 125 | ok(!even.includes(5)); 126 | } 127 | } 128 | 129 | { 130 | const read = split({ 131 | async *[Symbol.asyncIterator]() { 132 | yield [1, 2, 3]; 133 | yield [4, 5, 6]; 134 | }, 135 | }); 136 | 137 | const [first] = read.filter((value) => value % 2 === 0); 138 | 139 | for await (const even of first) { 140 | console.log({ even }); 141 | ok(typeof even === "number"); 142 | ok(even === 2 || even === 4); 143 | } 144 | } 145 | 146 | { 147 | const read = split({ 148 | async *[Symbol.asyncIterator]() { 149 | yield [1, 2, 3]; 150 | yield [4, 5, 6]; 151 | }, 152 | }); 153 | 154 | const first = read.filter((value) => value % 2 === 0).at(0); 155 | 156 | for await (const even of first) { 157 | console.log({ even }); 158 | ok(typeof even === "number"); 159 | ok(even === 2 || even === 4); 160 | } 161 | } 162 | 163 | { 164 | const read = split({ 165 | async *[Symbol.asyncIterator]() { 166 | yield [1, 2, 3]; 167 | yield [4, 5, 6]; 168 | }, 169 | }); 170 | 171 | const [, last] = read.filter((value) => value % 2 === 0); 172 | 173 | for await (const even of last) { 174 | console.log({ even }); 175 | ok(typeof even === "number"); 176 | ok(even === 6); 177 | } 178 | } 179 | 180 | { 181 | const [, last] = await split({ 182 | async *[Symbol.asyncIterator]() { 183 | yield [1, 2, 3]; 184 | yield [4, 5, 6]; 185 | }, 186 | }).filter((value) => value % 2 === 0); 187 | console.log({ last }); 188 | ok(typeof last === "number"); 189 | ok(last === 6); 190 | } 191 | { 192 | const last = await split({ 193 | async *[Symbol.asyncIterator]() { 194 | yield [1, 2, 3]; 195 | yield [4, 5, 6]; 196 | }, 197 | }) 198 | .filter((value) => value % 2 === 0) 199 | .at(1); 200 | console.log({ last }); 201 | ok(typeof last === "number"); 202 | ok(last === 6); 203 | } 204 | 205 | { 206 | const [a, b, c] = await split({ 207 | async *[Symbol.asyncIterator]() { 208 | yield [1, 2, 3]; 209 | yield [4, 5, 6]; 210 | }, 211 | }); 212 | console.log({ a, b, c }); 213 | ok(a === 4); 214 | ok(b === 5); 215 | ok(c === 6); 216 | } 217 | 218 | { 219 | const [a, b, c] = split({ 220 | async *[Symbol.asyncIterator]() { 221 | yield [1, 2, 3]; 222 | yield [4, 5, 6]; 223 | }, 224 | }); 225 | console.log({ a, b, c }); 226 | ok((await a) === 4); 227 | ok((await b) === 5); 228 | ok((await c) === 6); 229 | } 230 | 231 | { 232 | const read = split({ 233 | async *[Symbol.asyncIterator]() { 234 | yield [1, 2, 3]; 235 | yield [4, 5, 6]; 236 | }, 237 | }); 238 | 239 | const [first, last] = read.filter((value) => value % 2 === 0); 240 | ok((await first) === 4); 241 | ok((await last) === 6); 242 | } 243 | { 244 | const [first, middle, last] = split({ 245 | async *[Symbol.asyncIterator]() { 246 | yield [1, 2, 3]; 247 | yield [4, 5, 6]; 248 | }, 249 | }); 250 | 251 | const [a, b, c] = split(union([first, middle, last])); 252 | 253 | ok((await a) === 4); 254 | ok((await b) === 5); 255 | ok((await c) === 6); 256 | } 257 | { 258 | const [first, middle, last] = split({ 259 | async *[Symbol.asyncIterator]() { 260 | yield [1, 2, 3]; 261 | yield [4, 5, 6]; 262 | }, 263 | }); 264 | 265 | const [a, b, c] = split(union([middle, last, first])); 266 | 267 | ok((await a) === 5); 268 | ok((await b) === 6); 269 | ok((await c) === 4); 270 | } 271 | 272 | { 273 | const [first, middle, last] = split({ 274 | async *[Symbol.asyncIterator]() { 275 | yield [1, 2, 3]; 276 | yield [4, 5, 6]; 277 | }, 278 | }); 279 | 280 | for await (const snapshot of union([middle, last, first])) { 281 | console.log({ snapshot }); 282 | ok(Array.isArray(snapshot)); 283 | ok(snapshot.length === 3); 284 | ok( 285 | (snapshot[0] === 2 && snapshot[1] === 3 && snapshot[2] === 1) || 286 | (snapshot[0] === 5 && snapshot[1] === 6 && snapshot[2] === 4) 287 | ); 288 | } 289 | } 290 | 291 | { 292 | const [first, , last] = split({ 293 | async *[Symbol.asyncIterator]() { 294 | yield [1, 2, 3]; 295 | yield [4, 5, 6]; 296 | }, 297 | }); 298 | 299 | for await (const snapshot of union([last, first])) { 300 | console.log({ snapshot }); 301 | ok(Array.isArray(snapshot)); 302 | ok(snapshot.length === 2); 303 | ok( 304 | (snapshot[0] === 3 && snapshot[1] === 1) || 305 | (snapshot[0] === 6 && snapshot[1] === 4) 306 | ); 307 | } 308 | } 309 | 310 | { 311 | const [ones] = split({ 312 | async *[Symbol.asyncIterator]() { 313 | yield [1, 2, 3]; 314 | yield [4, 5, 6]; 315 | yield [1, 2, 3]; 316 | }, 317 | }).filter((value) => value === 1); 318 | 319 | let total = 0; 320 | for await (const one of ones) { 321 | console.log({ one }); 322 | total += 1; 323 | ok(one === 1); 324 | } 325 | ok(total === 2); 326 | } 327 | { 328 | const ones = split({ 329 | async *[Symbol.asyncIterator]() { 330 | yield [1, 2, 3]; 331 | yield [4, 5, 6]; 332 | yield [1, 2, 3]; 333 | }, 334 | }) 335 | .filter((value) => value === 1) 336 | .at(0); 337 | 338 | let total = 0; 339 | for await (const one of ones) { 340 | console.log({ one }); 341 | total += 1; 342 | ok(one === 1); 343 | } 344 | ok(total === 2); 345 | } 346 | { 347 | const [twos] = split({ 348 | async *[Symbol.asyncIterator]() { 349 | yield [1, 2, 3]; 350 | yield [4, 5, 6]; 351 | yield [1, 2, 3]; 352 | }, 353 | }).filter((value) => value === 2); 354 | 355 | let total = 0; 356 | for await (const two of twos) { 357 | console.log({ two }); 358 | total += 1; 359 | ok(two === 2); 360 | } 361 | ok(total === 2); 362 | } 363 | { 364 | const twos = split({ 365 | async *[Symbol.asyncIterator]() { 366 | yield [1, 2, 3]; 367 | yield [4, 5, 6]; 368 | yield [1, 2, 3]; 369 | }, 370 | }) 371 | .filter((value) => value === 2) 372 | .at(0); 373 | 374 | let total = 0; 375 | for await (const two of twos) { 376 | console.log({ two }); 377 | total += 1; 378 | ok(two === 2); 379 | } 380 | ok(total === 2); 381 | } 382 | { 383 | const [two] = await split({ 384 | async *[Symbol.asyncIterator]() { 385 | yield [1, 2, 3]; 386 | yield [4, 5, 6]; 387 | yield [1, 2, 3]; 388 | }, 389 | }).filter((value) => value === 2); 390 | console.log({ two }); 391 | ok(two === 2); 392 | } 393 | 394 | { 395 | const [twos] = split({ 396 | async *[Symbol.asyncIterator]() { 397 | yield [1, 2, 3]; 398 | yield [4, 5, 6]; 399 | yield [1, 2, 3]; 400 | }, 401 | }).filter((value) => value === 2); 402 | const two = await twos; 403 | console.log({ two }); 404 | ok(two === 2); 405 | } 406 | 407 | { 408 | const trues = split( 409 | { 410 | async *[Symbol.asyncIterator]() { 411 | yield [1, 2, 3]; 412 | yield [4, 5, 6]; 413 | yield [7, 8, 9]; 414 | }, 415 | }, 416 | { 417 | empty: false, 418 | } 419 | ) 420 | .map((value) => value >= 5) 421 | .filter(Boolean); 422 | 423 | console.log({ trues }); 424 | 425 | let total = 0; 426 | 427 | for await (const snapshot of trues) { 428 | console.log({ snapshot }); 429 | total += snapshot.length; 430 | } 431 | 432 | console.log({ total }); 433 | 434 | ok(total === 5); 435 | } 436 | 437 | { 438 | const trues = await split({ 439 | async *[Symbol.asyncIterator]() { 440 | yield [1, 2, 3]; 441 | yield [4, 5, 6]; 442 | yield [7, 8, 9]; 443 | }, 444 | }) 445 | .map((value) => value >= 5) 446 | .filter(Boolean) 447 | .take(1); 448 | 449 | console.log({ trues }); 450 | 451 | ok(trues.length === 2); 452 | } 453 | 454 | { 455 | const result = await split({ 456 | async *[Symbol.asyncIterator]() { 457 | yield [4, 5, 6]; 458 | yield [1, 8, 9]; 459 | }, 460 | }).find((value) => value >= 5); 461 | 462 | console.log({ result }); 463 | 464 | ok(result === 8); 465 | } 466 | { 467 | const result = await split({ 468 | async *[Symbol.asyncIterator]() { 469 | yield [4, 5, 6]; 470 | yield [1, 8, 9]; 471 | }, 472 | }) 473 | .filter((value) => value >= 5) 474 | .take(1) // If we used 2 here, we would get 8 as a result 475 | .at(0); 476 | 477 | console.log({ result }); 478 | 479 | ok(result === 5); 480 | } 481 | { 482 | const result = await split({ 483 | async *[Symbol.asyncIterator]() { 484 | yield [4, 5, 6]; 485 | yield [1, 8, 9]; 486 | }, 487 | }) 488 | .filter((value) => value >= 5) 489 | .take(2) 490 | .at(0); 491 | 492 | console.log({ result }); 493 | 494 | ok(result === 8); 495 | } 496 | 497 | { 498 | const result = await split({ 499 | async *[Symbol.asyncIterator]() { 500 | while (true) yield 1; 501 | }, 502 | }) 503 | .take(3) 504 | .at(0); 505 | 506 | ok(result === 1); 507 | } 508 | 509 | { 510 | const result = await split({ 511 | async *[Symbol.asyncIterator]() { 512 | for (let i = 0; ; i += 1) { 513 | yield i; 514 | } 515 | }, 516 | }) 517 | .take(100) 518 | .at(0); 519 | ok(result === 99); 520 | } 521 | 522 | { 523 | const every = await split({ 524 | async *[Symbol.asyncIterator]() { 525 | yield 1; 526 | yield 1; 527 | }, 528 | }).every((value) => value === 1); 529 | console.log({ every }); 530 | ok(every === true); 531 | } 532 | { 533 | const every = await split({ 534 | async *[Symbol.asyncIterator]() { 535 | yield 0; 536 | yield 1; 537 | }, 538 | }).every((value) => value === 1); 539 | console.log({ every }); 540 | ok(every === true); 541 | } 542 | { 543 | // Every can change from false to true, as each split 544 | // is multiple snapshots of state 545 | const every = split({ 546 | async *[Symbol.asyncIterator]() { 547 | yield 0; 548 | yield 1; 549 | }, 550 | }).every((value) => value === 1); 551 | 552 | let index = -1; 553 | for await (const snapshot of every) { 554 | index += 1; 555 | console.log({ index, snapshot }); 556 | if (index === 0) { 557 | ok(!snapshot); 558 | } else if (index === 1) { 559 | ok(snapshot); 560 | } 561 | } 562 | console.log({ index }); 563 | ok(index === 1); 564 | } 565 | 566 | { 567 | const every = await split({ 568 | async *[Symbol.asyncIterator]() { 569 | yield 1; 570 | yield 0; 571 | }, 572 | }).every((value) => value === 1); 573 | console.log({ every }); 574 | ok(every === false); 575 | } 576 | 577 | { 578 | // Every can change from true to false, as each split 579 | // is multiple snapshots of state 580 | const every = split({ 581 | async *[Symbol.asyncIterator]() { 582 | yield 1; 583 | yield 0; 584 | }, 585 | }).every((value) => value === 1); 586 | 587 | let index = -1; 588 | for await (const snapshot of every) { 589 | index += 1; 590 | console.log({ index, snapshot }); 591 | if (index === 0) { 592 | ok(snapshot); 593 | } else if (index === 1) { 594 | ok(!snapshot); 595 | } 596 | } 597 | console.log({ index }); 598 | ok(index === 1); 599 | } 600 | 601 | { 602 | const index = await split({ 603 | async *[Symbol.asyncIterator]() { 604 | yield 1; 605 | yield [0, 1]; 606 | }, 607 | }).findIndex((value) => value === 1); 608 | ok(index === 1); 609 | } 610 | { 611 | const index = split({ 612 | async *[Symbol.asyncIterator]() { 613 | yield 1; 614 | yield [0, 1]; 615 | yield [0, 0, 1]; 616 | }, 617 | }).findIndex((value) => value === 1); 618 | 619 | let expectedIndex = -1; 620 | 621 | for await (const snapshot of index) { 622 | expectedIndex += 1; 623 | ok(snapshot === expectedIndex); 624 | } 625 | 626 | ok(expectedIndex === 2); 627 | } 628 | 629 | { 630 | const [a, b, c, d, e] = await split({ 631 | async *[Symbol.asyncIterator]() { 632 | yield 1; 633 | yield [0, 1]; 634 | yield [0, 0, 1]; 635 | }, 636 | }).concat([2, 3]); 637 | 638 | console.log([a, b, c, d, e]); 639 | 640 | ok(a === 0); 641 | ok(b == 0); 642 | ok(c === 1); 643 | ok(d === 2); 644 | ok(e === 3); 645 | } 646 | { 647 | const [a, b, c, d, e] = split({ 648 | async *[Symbol.asyncIterator]() { 649 | yield 1; 650 | yield [0, 1]; 651 | yield [0, 0, 1]; 652 | }, 653 | }).concat([2, 3]); 654 | 655 | for await (const snapshot of union([a, b, c, d, e])) { 656 | const filtered = snapshot.filter((value) => typeof value === "number"); 657 | console.log({ snapshot, filtered }); 658 | 659 | if (filtered.length === 3) { 660 | ok(filtered[0] === 1); 661 | ok(filtered[1] === 2); 662 | ok(filtered[2] === 3); 663 | } else if (filtered.length === 4) { 664 | ok(filtered[0] == 0); 665 | ok(filtered[1] === 1); 666 | ok(filtered[2] === 2); 667 | ok(filtered[3] === 3); 668 | } else if (filtered.length === 5) { 669 | ok(filtered[0] === 0); 670 | ok(filtered[1] == 0); 671 | ok(filtered[2] === 1); 672 | ok(filtered[3] === 2); 673 | ok(filtered[4] === 3); 674 | } else { 675 | ok(false); 676 | } 677 | } 678 | } 679 | 680 | { 681 | const result = await split({ 682 | async *[Symbol.asyncIterator]() { 683 | yield 1; 684 | yield [0, 1]; 685 | yield [0, 0, 1]; 686 | }, 687 | }).concat({ 688 | async *[Symbol.asyncIterator]() { 689 | yield 1; 690 | yield [0, 1]; 691 | yield [0, 0, 1]; 692 | }, 693 | }); 694 | 695 | console.log(result); 696 | ok(result.length === 6); 697 | } 698 | { 699 | const [a, b, c, d, e, f] = split({ 700 | async *[Symbol.asyncIterator]() { 701 | yield 1; 702 | yield [0, 1]; 703 | yield [0, 0, 1]; 704 | }, 705 | }).concat({ 706 | async *[Symbol.asyncIterator]() { 707 | yield 1; 708 | yield [0, 1]; 709 | yield [0, 0, 1]; 710 | }, 711 | }); 712 | 713 | for await (const snapshot of union([a, b, c, d, e, f])) { 714 | ok(snapshot.length === 6); 715 | const filtered = snapshot.filter((value) => typeof value === "number"); 716 | console.log({ snapshot, filtered }); 717 | ok(filtered.length >= 1); 718 | } 719 | } 720 | 721 | { 722 | const [, , c] = split({ 723 | async *[Symbol.asyncIterator]() { 724 | yield 1; 725 | yield [2, 1]; 726 | yield [3, 2, 1]; 727 | }, 728 | }).copyWithin(2); 729 | 730 | const result = await c; 731 | console.log({ result }); 732 | ok(result === 3); 733 | } 734 | { 735 | const [, , c] = split({ 736 | async *[Symbol.asyncIterator]() { 737 | yield 1; 738 | yield [2, 1]; 739 | yield [3, 2, 1]; 740 | }, 741 | }) 742 | .concat([0, 0, 0]) 743 | .copyWithin(2); 744 | 745 | let index = -1; 746 | for await (const snapshot of c) { 747 | ok(typeof snapshot === "number"); 748 | index += 1; 749 | console.log({ index, snapshot }); 750 | if (index === 0) { 751 | ok(snapshot === 1); 752 | } else if (index === 1) { 753 | ok(snapshot === 2); 754 | } else if (index === 2) { 755 | ok(snapshot === 3); 756 | } else { 757 | ok(false); 758 | } 759 | } 760 | } 761 | 762 | { 763 | const expected = Math.random(); 764 | const [a] = split({ 765 | async *[Symbol.asyncIterator]() { 766 | yield expected; 767 | }, 768 | }).entries(); 769 | const [index, value] = await a; 770 | console.log({ index, value }); 771 | ok(index === 0); 772 | ok(value === expected); 773 | } 774 | 775 | { 776 | const { 777 | one: [ones], 778 | } = split({ 779 | async *[Symbol.asyncIterator]() { 780 | yield [1, 2, 3]; 781 | yield [4, 5, 6]; 782 | yield [1, 2, 3]; 783 | }, 784 | }).group((value) => (value === 1 ? "one" : "unknown")); 785 | 786 | let total = 0; 787 | for await (const one of ones) { 788 | console.log({ one }); 789 | total += 1; 790 | ok(one === 1); 791 | } 792 | ok(total === 2); 793 | } 794 | 795 | { 796 | const [ones] = split({ 797 | async *[Symbol.asyncIterator]() { 798 | yield [1, 2, 3]; 799 | yield [4, 5, 6]; 800 | yield [1, 2, 3]; 801 | }, 802 | }) 803 | .groupToMap((value) => (value === 1 ? "one" : "unknown")) 804 | .get("one"); 805 | 806 | let total = 0; 807 | for await (const one of ones) { 808 | console.log({ one }); 809 | total += 1; 810 | ok(one === 1); 811 | } 812 | ok(total === 2); 813 | } 814 | 815 | { 816 | const has = await split({ 817 | async *[Symbol.asyncIterator]() { 818 | yield [1, 2, 3]; 819 | yield [4, 5, 6]; 820 | yield [1, 2, 3]; 821 | }, 822 | }) 823 | .groupToMap((value) => (value === 1 ? "one" : "unknown")) 824 | .has("one"); 825 | console.log({ has }); 826 | ok(has); 827 | } 828 | { 829 | const has = split({ 830 | async *[Symbol.asyncIterator]() { 831 | yield [1, 2, 3]; 832 | yield [4, 5, 6]; 833 | yield [1, 2, 3]; 834 | }, 835 | }) 836 | .groupToMap((value) => (value === 1 ? "one" : "unknown")) 837 | .has("one"); 838 | let count = 0; 839 | let total = 0; 840 | for await (const snapshot of has) { 841 | console.log({ has: snapshot }); 842 | total += 1; 843 | count += snapshot ? 1 : 0; 844 | } 845 | console.log({ total }); 846 | ok(count === 2); 847 | } 848 | 849 | { 850 | const [ones] = split({ 851 | async *[Symbol.asyncIterator]() { 852 | yield [1, 2, 3]; 853 | yield [4, 5, 6]; 854 | yield [1, 2, 3]; 855 | }, 856 | }) 857 | .push({ 858 | async *[Symbol.asyncIterator]() { 859 | yield 1; 860 | }, 861 | }) 862 | .push({ 863 | async *[Symbol.asyncIterator]() { 864 | yield 0; 865 | }, 866 | }) 867 | .push({ 868 | async *[Symbol.asyncIterator]() { 869 | yield 1; 870 | }, 871 | }) 872 | .groupToMap((value) => (value === 1 ? "one" : "unknown")) 873 | .get("one"); 874 | 875 | let total = 0; 876 | for await (const one of ones) { 877 | console.log({ one }); 878 | total += 1; 879 | ok(one === 1); 880 | } 881 | console.log({ total }); 882 | ok(total === 4); 883 | } 884 | 885 | { 886 | const info = split( 887 | { 888 | async *[Symbol.asyncIterator]() { 889 | console.log("Running"); 890 | yield 1; 891 | yield 2; 892 | yield 3; 893 | console.log("Finished"); 894 | }, 895 | }, 896 | { 897 | keep: true, 898 | } 899 | ); 900 | 901 | let initial = 0; 902 | for await (const snapshot of info) { 903 | console.log({ snapshot }); 904 | initial += 1; 905 | } 906 | console.log({ initial }); 907 | ok(initial === 3); 908 | 909 | let next = 0; 910 | for await (const snapshot of info) { 911 | console.log({ snapshot }); 912 | next += 1; 913 | } 914 | console.log({ next }); 915 | ok(next === 3); 916 | 917 | const [values] = info; 918 | 919 | let totalSnapshot = 0; 920 | let total = 0; 921 | for await (const snapshot of values) { 922 | console.log({ snapshot }); 923 | totalSnapshot += snapshot; 924 | total += 1; 925 | } 926 | console.log({ total, totalSnapshot }); 927 | ok(total === 3); 928 | ok(totalSnapshot === 6); // 1 + 2 + 3 929 | } 930 | 931 | { 932 | const input = { 933 | async * [Symbol.asyncIterator]() { 934 | yield [1, 2, 3]; 935 | yield [4, 5, 6]; 936 | yield [7, 8, 9]; 937 | } 938 | } 939 | const mask = { 940 | async * [Symbol.asyncIterator]() { 941 | yield [false, true, false]; 942 | yield [true, false, true]; 943 | yield [false, false, true]; 944 | } 945 | } 946 | 947 | const result = split(input).mask(mask); 948 | 949 | const seen: unknown[] = []; 950 | 951 | for await (const snapshot of result) { 952 | ok(isArray(snapshot)); 953 | console.log({ snapshot }); 954 | seen.push(...snapshot); 955 | } 956 | 957 | console.log({ seen }); 958 | ok(!seen.includes(1)); 959 | ok(seen.includes(2)); 960 | ok(!seen.includes(3)); 961 | ok(seen.includes(4)); 962 | ok(!seen.includes(5)); 963 | ok(seen.includes(6)); 964 | ok(!seen.includes(7)); 965 | ok(!seen.includes(8)); 966 | ok(seen.includes(9)); 967 | 968 | 969 | } 970 | 971 | 972 | { 973 | const input = { 974 | async * [Symbol.asyncIterator]() { 975 | yield [1, 2, 3]; 976 | yield [4, 5, 6]; 977 | yield [7, 8, 9]; 978 | } 979 | } 980 | const mask = { 981 | async * [Symbol.asyncIterator]() { 982 | yield false; 983 | yield [true, false, true]; 984 | yield true; 985 | } 986 | } 987 | 988 | const result = split(input).mask(mask); 989 | 990 | const seen: unknown[] = []; 991 | 992 | for await (const snapshot of result) { 993 | ok(isArray(snapshot)); 994 | console.log({ snapshot }); 995 | seen.push(...snapshot); 996 | } 997 | 998 | console.log({ seen }); 999 | ok(!seen.includes(1)); 1000 | ok(!seen.includes(2)); 1001 | ok(!seen.includes(3)); 1002 | ok(seen.includes(4)); 1003 | ok(!seen.includes(5)); 1004 | ok(seen.includes(6)); 1005 | ok(seen.includes(7)); 1006 | ok(seen.includes(8)); 1007 | ok(seen.includes(9)); 1008 | 1009 | 1010 | } 1011 | 1012 | { 1013 | const last = await split({ 1014 | async *[Symbol.asyncIterator]() { 1015 | yield [1, 2, 3]; 1016 | yield [4, 5, 6]; 1017 | }, 1018 | }).at(-1); 1019 | console.log({ last }); 1020 | ok(last === 6); 1021 | } 1022 | { 1023 | const middle = await split({ 1024 | async *[Symbol.asyncIterator]() { 1025 | yield [1, 2, 3]; 1026 | yield [4, 5, 6]; 1027 | }, 1028 | }).at(-2); 1029 | console.log({ middle }); 1030 | ok(middle === 5); 1031 | } 1032 | { 1033 | const first = await split({ 1034 | async *[Symbol.asyncIterator]() { 1035 | yield [1, 2, 3]; 1036 | yield [4, 5, 6]; 1037 | }, 1038 | }).at(-3); 1039 | console.log({ first }); 1040 | ok(first === 4); 1041 | } -------------------------------------------------------------------------------- /src/split/split.ts: -------------------------------------------------------------------------------- 1 | import { anAsyncThing, TheAsyncThing } from "../the-thing"; 2 | import { Push, PushOptions } from "../push"; 3 | import { isLike, no, ok } from "../like"; 4 | import { 5 | isArray, 6 | isAsyncIterable, 7 | isIterable, 8 | isIteratorYieldResult, 9 | isPromise, 10 | } from "../is"; 11 | import { 12 | FilterFn, 13 | Split as SplitCore, 14 | Name, 15 | SplitInput, 16 | assertSplitInputFn, 17 | SplitFn, 18 | SplitInputFn, 19 | SplitAsyncIterable, 20 | isSplitAt, 21 | MapFn, 22 | SplitOptions, 23 | SplitAssertFn, 24 | TypedSplitOptions, 25 | SplitConcatInput, 26 | SplitConcatSyncInput, 27 | AsyncMap, 28 | } from "./type"; 29 | import { union } from "@virtualstate/union"; 30 | 31 | export function split( 32 | input: SplitInputFn, 33 | options: TypedSplitOptions 34 | ): SplitFn; 35 | export function split( 36 | input: SplitInput, 37 | options: TypedSplitOptions 38 | ): SplitCore; 39 | export function split( 40 | input: SplitInputFn, 41 | options?: SplitOptions 42 | ): SplitFn; 43 | export function split( 44 | input: SplitInput, 45 | options?: SplitOptions 46 | ): SplitCore; 47 | export function split( 48 | input: SplitInput, 49 | options?: SplitOptions 50 | ): SplitCore { 51 | function assert(value: unknown): asserts value is T { 52 | if (!isLike>(options)) return; 53 | if ("assert" in options) { 54 | const fn: SplitAssertFn = options.assert; 55 | return fn(value); 56 | } 57 | if ("is" in options) { 58 | ok(options.is(value)); 59 | } 60 | } 61 | const symbols = options ? Object.getOwnPropertySymbols(options) : []; 62 | const symbolOptions = {}; 63 | for (const symbol of symbols) { 64 | const descriptor = Object.getOwnPropertyDescriptor(options, symbol); 65 | if (!descriptor) continue; 66 | Object.defineProperty(symbolOptions, symbol, descriptor); 67 | } 68 | 69 | const context = createSplitContext(input, options); 70 | const async = anAsyncThing(context); 71 | 72 | if (typeof input === "function") { 73 | return createFn(); 74 | } else { 75 | return createInstance(); 76 | } 77 | 78 | function createSplitContext( 79 | input: SplitInput, 80 | options?: SplitOptions 81 | ): SplitAsyncIterable { 82 | const targets = new Map>(); 83 | let mainTarget: Push | undefined = undefined; 84 | 85 | const { empty } = options ?? {}; 86 | 87 | let done = false, 88 | started = false; 89 | 90 | let readPromise: Promise | undefined = undefined; 91 | 92 | function getMainTarget() { 93 | return (mainTarget = mainTarget ?? new Push(options)); 94 | } 95 | 96 | function bind(that: unknown, ...args: unknown[]) { 97 | return function binder(this: unknown, ...more: unknown[]) { 98 | return call(that, ...args, ...more); 99 | }; 100 | } 101 | 102 | function* check(snapshot: M[]): Iterable { 103 | if (!empty && snapshot.length === 0) { 104 | return; 105 | } 106 | yield snapshot; 107 | } 108 | 109 | function call(that?: unknown, ...args: unknown[]): AsyncIterable { 110 | return { 111 | ...symbolOptions, 112 | [Symbol.asyncIterator]: asSnapshot, 113 | }; 114 | 115 | async function* asSnapshot() { 116 | if (options?.keep && !mainTarget) { 117 | void getMainTarget(); 118 | } 119 | let yielded = false; 120 | for await (const snapshot of innerCall()) { 121 | for (const output of check( 122 | Array.isArray(snapshot) 123 | ? snapshot 124 | : isIterable(snapshot) 125 | ? Array.from(snapshot) 126 | : [snapshot] 127 | )) { 128 | if (options?.keep) { 129 | for (const index of output.keys()) { 130 | // Ensure that the target is loaded before we start yielding 131 | // in our core 132 | // 133 | // Only do this if keep is indicated, this allows for the entire 134 | // source to be read at any point. 135 | // 136 | // keep when used with push allows the async iterables 137 | // to be read multiple times over as well! 138 | void atTarget(index); 139 | } 140 | } 141 | yield output; 142 | yielded = true; 143 | } 144 | } 145 | if (!yielded) { 146 | // Yield an empty result so that when we use a promise 147 | // we get a consistent result, never giving an undefined default 148 | // 149 | // This also means we get _at least one_ yield always through the main 150 | // target 151 | // 152 | // The push function will push this to mainTarget but to none of the 153 | // indexed targets 154 | yield []; 155 | } 156 | } 157 | 158 | async function* innerCall() { 159 | if (isAsyncIterable(input)) { 160 | yield* input; 161 | } else { 162 | assertSplitInputFn(input); 163 | yield* input.call(that, ...args); 164 | } 165 | } 166 | } 167 | 168 | const source = { 169 | async *[Symbol.asyncIterator]() { 170 | yield* call(); 171 | }, 172 | }; 173 | 174 | function start() { 175 | if (readPromise) return readPromise; 176 | readPromise = read(); 177 | void readPromise.catch((error) => void error); 178 | return readPromise; 179 | } 180 | 181 | async function close() { 182 | if (done) return; 183 | done = true; 184 | const promises = [...inner()].filter(Boolean); 185 | if (promises.length) { 186 | await Promise.any(promises); 187 | } 188 | 189 | function* inner() { 190 | if (mainTarget) { 191 | yield mainTarget.close(); 192 | } 193 | for (const target of targets.values()) { 194 | if (!target) continue; 195 | yield target.close(); 196 | } 197 | } 198 | } 199 | async function throwAtTargets(reason: unknown) { 200 | if (done) return; 201 | done = true; 202 | const promises = [...inner()].filter(Boolean); 203 | if (promises.length) { 204 | await Promise.any(promises); 205 | } 206 | 207 | function* inner() { 208 | yield mainTarget?.throw(reason); 209 | for (const target of targets.values()) { 210 | yield target?.throw(reason); 211 | } 212 | } 213 | } 214 | 215 | async function pushToTarget(snapshot: T[]) { 216 | const promises = [...inner()].filter(Boolean); 217 | if (promises.length) { 218 | await Promise.any(promises); 219 | } 220 | function* inner() { 221 | yield mainTarget?.push(snapshot); 222 | 223 | for (const [index, target] of targets.entries()) { 224 | if (index >= snapshot.length) { 225 | continue; 226 | } 227 | if (index < 0 && Math.abs(index) > snapshot.length) { 228 | continue; 229 | } 230 | const value = snapshot.at(index); // Using at here allows using -1 as an index 231 | assert(value); 232 | target.push(value); 233 | } 234 | } 235 | } 236 | 237 | async function read() { 238 | try { 239 | ok(!started); 240 | started = true; 241 | for await (const snapshot of source) { 242 | if (done) break; 243 | if (empty === false && snapshot.length === 0) { 244 | continue; 245 | } 246 | await pushToTarget(snapshot); 247 | } 248 | await close(); 249 | } catch (error) { 250 | await throwAtTargets(error); 251 | } 252 | } 253 | 254 | function getAsyncIterableOutput(target: Push): AsyncIterable { 255 | return { 256 | ...symbolOptions, 257 | async *[Symbol.asyncIterator]() { 258 | const promise = start(); 259 | yield* target; 260 | await promise; 261 | }, 262 | }; 263 | } 264 | function getOutput(target: Push): TheAsyncThing { 265 | const async = getAsyncIterableOutput(target); 266 | return anAsyncThing(async); 267 | } 268 | 269 | function atTarget(index: number) { 270 | const existing = targets.get(index); 271 | if (existing) return existing; 272 | const target = new Push(options); 273 | targets.set(index, target); 274 | return target; 275 | } 276 | 277 | function at(index: number) { 278 | // if (isSplitAt(input)) { 279 | // const result = input.at(index); 280 | // if (isAsyncIterable(result)) { 281 | // return createSplitContext(result).at(0); 282 | // } 283 | // } 284 | return getOutput(atTarget(index)); 285 | } 286 | 287 | function filter(fn: FilterFn): AsyncIterable { 288 | return { 289 | async *[Symbol.asyncIterator]() { 290 | for await (const snapshot of source) { 291 | yield* check(snapshot.filter(fn)); 292 | } 293 | }, 294 | }; 295 | } 296 | 297 | function flatMap(fn: MapFn): AsyncIterable { 298 | return { 299 | async *[Symbol.asyncIterator]() { 300 | for await (const snapshot of map(fn)) { 301 | yield* check(snapshot.flatMap((value) => value)); 302 | } 303 | }, 304 | }; 305 | } 306 | 307 | function map(fn: MapFn): AsyncIterable { 308 | return { 309 | async *[Symbol.asyncIterator]() { 310 | for await (const snapshot of source) { 311 | const result = snapshot.map(fn); 312 | const promises = result.filter>(isPromise); 313 | if (isSyncArray(result, promises)) { 314 | yield* check(result); 315 | } else { 316 | const promiseResults = await Promise.all(promises); 317 | const completeResults = result.map((value) => { 318 | if (isPromise(value)) { 319 | const index = promises.indexOf(value); 320 | return promiseResults[index]; 321 | } else { 322 | return value; 323 | } 324 | }); 325 | yield* check(completeResults); 326 | } 327 | } 328 | }, 329 | }; 330 | 331 | function isSyncArray( 332 | value: unknown[], 333 | promises: Promise[] 334 | ): value is M[] { 335 | return !promises.length; 336 | } 337 | } 338 | 339 | function take(count: number) { 340 | return { 341 | async *[Symbol.asyncIterator]() { 342 | let current = 0; 343 | for await (const snapshot of source) { 344 | for (const output of check(snapshot)) { 345 | yield output; 346 | current += 1; 347 | if (current >= count) { 348 | return; 349 | } 350 | } 351 | } 352 | }, 353 | }; 354 | } 355 | 356 | function find(fn: FilterFn): TheAsyncThing { 357 | return anAsyncThing({ 358 | ...symbolOptions, 359 | async *[Symbol.asyncIterator]() { 360 | for await (const [first] of filter(fn)) { 361 | yield first; 362 | } 363 | }, 364 | }); 365 | } 366 | 367 | function findIndex(fn: FilterFn): TheAsyncThing { 368 | return anAsyncThing({ 369 | ...symbolOptions, 370 | async *[Symbol.asyncIterator]() { 371 | for await (const snapshot of source) { 372 | yield snapshot.findIndex(fn); 373 | } 374 | }, 375 | }); 376 | } 377 | 378 | function every(fn: FilterFn): TheAsyncThing { 379 | return anAsyncThing({ 380 | ...symbolOptions, 381 | async *[Symbol.asyncIterator]() { 382 | let yielded = false; 383 | for await (const snapshot of source) { 384 | const every = snapshot.every(fn); 385 | yield every; 386 | yielded = true; 387 | } 388 | if (!yielded) { 389 | yield true; 390 | } 391 | }, 392 | }); 393 | } 394 | 395 | function concat( 396 | other: SplitConcatInput, 397 | ...rest: SplitConcatSyncInput[] 398 | ): AsyncIterable { 399 | return { 400 | async *[Symbol.asyncIterator]() { 401 | if (!isAsyncIterable(other)) { 402 | for await (const snapshot of source) { 403 | yield [...snapshot, ...asIterable(other), ...rest].flatMap( 404 | asArray 405 | ); 406 | } 407 | return; 408 | } 409 | 410 | for await (const [left, right] of union([source, other])) { 411 | yield [...asArray(left), ...asArray(right)].flatMap(asArray); 412 | } 413 | 414 | function asIterable(other: SplitConcatSyncInput): Iterable { 415 | return isIterable(other) ? other : [other]; 416 | } 417 | 418 | function asArray(other: SplitConcatSyncInput): T[] { 419 | return [...asIterable(other)]; 420 | } 421 | }, 422 | }; 423 | } 424 | 425 | function copyWithin( 426 | target: number, 427 | start?: number, 428 | end?: number 429 | ): AsyncIterable { 430 | return { 431 | async *[Symbol.asyncIterator]() { 432 | for await (const snapshot of source) { 433 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/copyWithin 434 | // 435 | // If target is at or greater than arr.length, nothing will be copied. 436 | // If target is positioned after start, the copied sequence will be trimmed to fit arr.length. 437 | // 438 | // Use concat beforehand and create a bigger array, then copy to the newly available space 439 | yield [...snapshot].copyWithin(target, start, end); 440 | } 441 | }, 442 | }; 443 | } 444 | 445 | function entries(): AsyncIterable<[number, T][]> { 446 | return { 447 | async *[Symbol.asyncIterator]() { 448 | for await (const snapshot of source) { 449 | yield [...snapshot.entries()]; 450 | } 451 | }, 452 | }; 453 | } 454 | 455 | function includes(search: T, fromIndex?: number): TheAsyncThing { 456 | return anAsyncThing({ 457 | ...symbolOptions, 458 | async *[Symbol.asyncIterator]() { 459 | let yielded = false; 460 | for await (const snapshot of source) { 461 | yield snapshot.includes(search, fromIndex); 462 | yielded = true; 463 | } 464 | if (!yielded) { 465 | yield false; 466 | } 467 | }, 468 | }); 469 | } 470 | 471 | function reverse(): AsyncIterable { 472 | return { 473 | async *[Symbol.asyncIterator]() { 474 | for await (const snapshot of source) { 475 | yield [...snapshot].reverse(); 476 | } 477 | }, 478 | }; 479 | } 480 | 481 | function push(other: SplitInput): AsyncIterable; 482 | function push(other: SplitInput): AsyncIterable<(M | T)[]>; 483 | function push(other: SplitInput): AsyncIterable { 484 | return { 485 | async *[Symbol.asyncIterator]() { 486 | yield* source; 487 | yield* createSplitContext(other); 488 | }, 489 | }; 490 | } 491 | 492 | function group( 493 | fn: MapFn 494 | ): Record> { 495 | const known: AsyncIterable[] = []; 496 | const indexes: Partial> = {}; 497 | let index = -1; 498 | 499 | function getIndex(key: K): number { 500 | const existing = indexes[key]; 501 | if (typeof existing === "number") { 502 | return existing; 503 | } 504 | const next = (index += 1); 505 | indexes[key] = next; 506 | return next; 507 | } 508 | 509 | const result = createSplitContext({ 510 | async *[Symbol.asyncIterator]() { 511 | for await (const snapshot of map((input, ...args) => { 512 | const maybeGroup = fn(input, ...args); 513 | if (isPromise(maybeGroup)) { 514 | return maybeGroup.then((group) => [group, input] as const); 515 | } else { 516 | return [maybeGroup, input] as const; 517 | } 518 | })) { 519 | const grouped = snapshot.reduce( 520 | (grouped: Partial>, [group, input]: [K, T]) => { 521 | grouped[group] = grouped[group] ?? []; 522 | grouped[group].push(input); 523 | return grouped; 524 | }, 525 | {} 526 | ); 527 | 528 | const aligned: T[][] = []; 529 | 530 | for (const [group, values] of Object.entries(grouped)) { 531 | ok(group); 532 | ok(values); 533 | aligned[getIndex(group)] = values; 534 | } 535 | 536 | for (const index of known.keys()) { 537 | aligned[index] = aligned[index] ?? []; 538 | } 539 | 540 | yield aligned; 541 | } 542 | }, 543 | }); 544 | const iterator = result[Symbol.iterator](); 545 | 546 | function getIndexedFromKnown(index: number) { 547 | while (!known[index]) { 548 | const result = iterator.next(); 549 | ok(isIteratorYieldResult(result)); 550 | known.push(result.value); 551 | } 552 | return known[index]; 553 | } 554 | 555 | const output = new Proxy(known, { 556 | get(_, name) { 557 | ok(name); 558 | const index = getIndex(name); 559 | return getIndexedFromKnown(index); 560 | }, 561 | }); 562 | ok>>(output); 563 | return output; 564 | } 565 | 566 | function groupToMap( 567 | fn: MapFn 568 | ): AsyncMap> { 569 | const grouped = group(fn); 570 | return { 571 | get(key: K): AsyncIterable { 572 | return grouped[key]; 573 | }, 574 | set(): never { 575 | return no(); 576 | }, 577 | has(key: K): TheAsyncThing { 578 | return anAsyncThing({ 579 | ...symbolOptions, 580 | async *[Symbol.asyncIterator](): AsyncIterator { 581 | let yielded = false; 582 | for await (const snapshot of grouped[key]) { 583 | yield snapshot.length > 0; 584 | yielded = true; 585 | } 586 | if (!yielded) { 587 | yield false; 588 | } 589 | }, 590 | }); 591 | }, 592 | delete(): never { 593 | return no(); 594 | }, 595 | get size() { 596 | return no(); 597 | }, 598 | }; 599 | } 600 | 601 | async function *synchronize(left: AsyncIterable, right: AsyncIterable): AsyncIterable<[L, R]> { 602 | let leftResult: IteratorResult, 603 | rightResult: IteratorResult; 604 | 605 | const leftIterator = left[Symbol.asyncIterator](); 606 | const rightIterator = right[Symbol.asyncIterator](); 607 | 608 | do { 609 | [leftResult, rightResult] = await Promise.all([ 610 | leftIterator.next(), 611 | rightIterator.next() 612 | ]); 613 | if (isIteratorYieldResult(leftResult) && isIteratorYieldResult(rightResult)) { 614 | yield [leftResult.value, rightResult.value]; 615 | } 616 | } while (!leftResult.done && !rightResult.done); 617 | 618 | await leftIterator.return?.(); 619 | await rightIterator.return?.(); 620 | } 621 | 622 | function mask(input: AsyncIterable) { 623 | return { 624 | async *[Symbol.asyncIterator]() { 625 | for await (const [ 626 | snapshot, 627 | mask 628 | ] of synchronize(source, input)) { 629 | if (isArray(mask)) { 630 | yield Object.entries(mask) 631 | .filter(([, include]) => include) 632 | .map(([index]) => snapshot[+index]); 633 | } else if (mask) { 634 | yield snapshot; 635 | } 636 | } 637 | } 638 | } 639 | } 640 | 641 | return { 642 | async *[Symbol.asyncIterator](): AsyncIterableIterator { 643 | yield* getAsyncIterableOutput(getMainTarget()); 644 | }, 645 | [Symbol.iterator]() { 646 | let index = -1; 647 | return { 648 | next() { 649 | const currentIndex = (index += 1); 650 | return { done: false, value: at(currentIndex) }; 651 | }, 652 | }; 653 | }, 654 | filter, 655 | find, 656 | findIndex, 657 | map, 658 | at, 659 | call, 660 | bind, 661 | take, 662 | every, 663 | concat, 664 | copyWithin, 665 | entries, 666 | flatMap, 667 | includes, 668 | reverse, 669 | group, 670 | groupToMap, 671 | push, 672 | mask, 673 | }; 674 | } 675 | 676 | function defineSymbols(object: unknown) { 677 | for (const symbol of symbols) { 678 | const descriptor = Object.getOwnPropertyDescriptor(symbolOptions, symbol); 679 | if (!descriptor) continue; 680 | Object.defineProperty(object, symbol, descriptor); 681 | } 682 | } 683 | 684 | function defineProperties(fn: unknown): asserts fn is SplitCore { 685 | // const source = fn; 686 | defineSymbols(fn); 687 | Object.defineProperties(fn, { 688 | [Symbol.asyncIterator]: { 689 | value: context[Symbol.asyncIterator], 690 | }, 691 | [Symbol.iterator]: { 692 | value: context[Symbol.iterator], 693 | }, 694 | find: { 695 | value: context.find, 696 | }, 697 | findIndex: { 698 | value: context.findIndex, 699 | }, 700 | filter: { 701 | value(fn: FilterFn) { 702 | return split(context.filter(fn), options); 703 | }, 704 | }, 705 | take: { 706 | value(count: number) { 707 | return split(context.take(count), options); 708 | }, 709 | }, 710 | map: { 711 | value(fn: MapFn, otherOptions?: SplitOptions) { 712 | return split(context.map(fn), otherOptions ?? options); 713 | }, 714 | }, 715 | flatMap: { 716 | value(fn: MapFn, otherOptions?: SplitOptions) { 717 | return split(context.flatMap(fn), otherOptions ?? options); 718 | }, 719 | }, 720 | concat: { 721 | value(other: SplitConcatInput) { 722 | return split(context.concat(other), options); 723 | }, 724 | }, 725 | mask: { 726 | value(other: AsyncIterable) { 727 | return split(context.mask(other), options); 728 | }, 729 | }, 730 | reverse: { 731 | value() { 732 | return split(context.reverse(), options); 733 | }, 734 | }, 735 | copyWithin: { 736 | value(target: number, start?: number, end?: number) { 737 | return split(context.copyWithin(target, start, end), options); 738 | }, 739 | }, 740 | entries: { 741 | value() { 742 | return split(context.entries(), { 743 | ...symbolOptions, 744 | keep: options?.keep, 745 | }); 746 | }, 747 | }, 748 | group: { 749 | value(fn: MapFn) { 750 | const proxied = new Proxy(context.group(fn), { 751 | get(target, name) { 752 | ok(name); 753 | const result = target[name]; 754 | return split(result, options); 755 | }, 756 | }); 757 | ok>>(proxied); 758 | return proxied; 759 | }, 760 | }, 761 | groupToMap: { 762 | value(fn: MapFn) { 763 | const map = context.groupToMap(fn); 764 | const get = map.get.bind(map); 765 | map.get = (key) => { 766 | return split(get(key), options); 767 | }; 768 | return map; 769 | }, 770 | }, 771 | push: { 772 | value( 773 | other: SplitInput, 774 | otherOptions?: TypedSplitOptions | SplitOptions 775 | ) { 776 | return split(context.push(other), { 777 | ...options, 778 | ...otherOptions 779 | }); 780 | }, 781 | }, 782 | at: { 783 | value: context.at, 784 | }, 785 | every: { 786 | value: context.every, 787 | }, 788 | includes: { 789 | value: context.includes, 790 | }, 791 | then: { 792 | value: async.then, 793 | }, 794 | catch: { 795 | value: async.catch, 796 | }, 797 | finally: { 798 | value: async.finally, 799 | }, 800 | }); 801 | } 802 | 803 | function isSplit(object: unknown): object is SplitCore { 804 | if (!isLike, undefined>>>(object)) return false; 805 | return ( 806 | typeof object.then === "function" && 807 | typeof object[Symbol.asyncIterator] === "function" && 808 | typeof object[Symbol.iterator] === "function" 809 | ); 810 | } 811 | 812 | function assertSplit(object: unknown): asserts object is SplitCore { 813 | ok(isSplit(object)); 814 | } 815 | 816 | function createFn() { 817 | const fn: unknown = function SplitFn(this: unknown, ...args: unknown[]) { 818 | return split(context.bind(this, ...args), options); 819 | }; 820 | defineProperties(fn); 821 | return fn; 822 | } 823 | 824 | function createInstance() { 825 | class Split { } 826 | defineProperties(Split.prototype); 827 | const instance = new Split(); 828 | assertSplit(instance); 829 | return instance; 830 | 831 | // class Split implements SplitCore, Promise { 832 | // 833 | // then: Promise["then"] = async.then.bind(async); 834 | // catch: Promise["catch"] = async.catch.bind(async); 835 | // finally: Promise["finally"] = async.finally.bind(async); 836 | // 837 | // get [Symbol.toStringTag]() { 838 | // return "[object Split]"; 839 | // } 840 | // 841 | // get [Symbol.asyncIterator]() { 842 | // return context[Symbol.asyncIterator]; 843 | // } 844 | // 845 | // get [Symbol.iterator]() { 846 | // return context[Symbol.iterator]; 847 | // } 848 | // 849 | // get at() { 850 | // return context.at; 851 | // } 852 | // 853 | // get find() { 854 | // return context.find; 855 | // } 856 | // 857 | // get findIndex() { 858 | // return context.findIndex; 859 | // } 860 | // 861 | // get every() { 862 | // return context.every; 863 | // } 864 | // 865 | // get includes() { 866 | // return context.includes; 867 | // } 868 | // 869 | // filter(fn: FilterFn) { 870 | // return split(context.filter(fn), options); 871 | // } 872 | // 873 | // take(count: number) { 874 | // return split(context.take(count), options); 875 | // } 876 | // 877 | // map( 878 | // fn: MapFn, 879 | // otherOptions?: TypedSplitOptions | SplitOptions 880 | // ) { 881 | // return split(context.map(fn), otherOptions ?? options); 882 | // } 883 | // 884 | // flatMap( 885 | // fn: MapFn, 886 | // otherOptions?: TypedSplitOptions | SplitOptions 887 | // ) { 888 | // return split(context.flatMap(fn), otherOptions ?? options); 889 | // } 890 | // 891 | // concat(other: SplitConcatInput, ...rest: T[]) { 892 | // if (rest.length) { 893 | // ok(other); 894 | // return split(context.concat(other, ...rest)); 895 | // } else { 896 | // return split(context.concat(other), options); 897 | // } 898 | // } 899 | // 900 | // copyWithin(target: number, start?: number, end?: number) { 901 | // return split(context.copyWithin(target, start, end), options); 902 | // } 903 | // 904 | // reverse() { 905 | // return split(context.reverse(), options); 906 | // } 907 | // 908 | // entries() { 909 | // return split(context.entries(), { 910 | // keep: options?.keep, 911 | // }); 912 | // } 913 | // 914 | // push( 915 | // other: SplitInput, 916 | // otherOptions?: TypedSplitOptions | SplitOptions 917 | // ) { 918 | // return split(context.push(other), otherOptions ?? options); 919 | // } 920 | // 921 | // group(fn: MapFn) { 922 | // const proxied = new Proxy(context.group(fn), { 923 | // get(target, name) { 924 | // ok(name); 925 | // const result = target[name]; 926 | // return split(result, options); 927 | // }, 928 | // }); 929 | // ok>>(proxied); 930 | // return proxied; 931 | // } 932 | // 933 | // groupToMap(fn: MapFn) { 934 | // const map = context.groupToMap(fn); 935 | // const get = map.get.bind(map); 936 | // map.get = (key) => { 937 | // return split(get(key), options); 938 | // }; 939 | // ok>>(map); 940 | // return map; 941 | // } 942 | // 943 | // call(that: unknown, ...args: unknown[]) { 944 | // return split(context.call.bind(undefined, that, ...args), options); 945 | // } 946 | // 947 | // bind(that: unknown, ...args: unknown[]) { 948 | // return split(context.bind(that, ...args), options); 949 | // } 950 | // } 951 | // return new Split(); 952 | } 953 | } 954 | --------------------------------------------------------------------------------