├── .changeset ├── README.md └── config.json ├── .eslintrc ├── .github └── workflows │ ├── main.yml │ ├── release.yml │ └── size.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierrc ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bob.config.js ├── jest.config.js ├── package.json ├── renovate.json ├── src ├── Sink.ts ├── applyAsyncIterableIteratorToSink.spec.ts ├── applyAsyncIterableIteratorToSink.ts ├── index.ts ├── isAsyncIterable.ts ├── makeAsyncIterableIteratorFromSink.ts ├── makePushPullAsyncIterableIterator.spec.ts ├── makePushPullAsyncIterableIterator.ts ├── operators │ ├── filter.ts │ └── map.ts ├── withHandlers.ts └── withHandlersFrom.ts ├── tsconfig.json └── yarn.lock /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.6.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "linked": [], 6 | "access": "restricted", 7 | "baseBranch": "main", 8 | "updateInternalDependencies": "patch", 9 | "ignore": [] 10 | } 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 2018, 5 | "sourceType": "module" 6 | }, 7 | "plugins": ["@typescript-eslint"], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "prettier" 12 | ], 13 | "env": { 14 | "es6": true, 15 | "node": true 16 | }, 17 | "ignorePatterns": ["dist"] 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ["12.x", "14.x", "16.x"] 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v2 16 | 17 | - name: Use Node ${{ matrix.node }} 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: ${{ matrix.node }} 21 | 22 | - name: Install deps and build (with cache) 23 | uses: bahmutov/npm-install@v1 24 | 25 | - name: Lint 26 | run: yarn ci:eslint 27 | 28 | - name: Test 29 | run: yarn test --ci --coverage 30 | 31 | - name: Build 32 | run: yarn build 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repo 14 | uses: actions/checkout@master 15 | with: 16 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 17 | fetch-depth: 0 18 | 19 | - name: Setup Node.js 14.x 20 | uses: actions/setup-node@master 21 | with: 22 | node-version: 14.x 23 | 24 | - name: Install Dependencies 25 | run: yarn 26 | 27 | - name: Create Release Pull Request or Publish to npm 28 | id: changesets 29 | uses: changesets/action@master 30 | with: 31 | # This expects you to have a script called release which does a build for your packages and calls changeset publish 32 | publish: yarn release 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: size 2 | on: [pull_request] 3 | jobs: 4 | size: 5 | runs-on: ubuntu-latest 6 | env: 7 | CI_JOB_NUMBER: 1 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: andresz1/size-limit-action@v1 11 | with: 12 | github_token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | } 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @n1ru4l/push-pull-async-iterable-iterator 2 | 3 | ## 3.2.0 4 | 5 | ### Minor Changes 6 | 7 | - 2d1d87d: Add operators `filter` and `map` 8 | - 2d1d87d: Add helpers `withHandlers` and `withHandlersFrom` 9 | 10 | ## 3.1.0 11 | 12 | ### Minor Changes 13 | 14 | - c1d143c: Change usage of type `AsyncIterableIterator` to `AsyncGenerator`. 15 | 16 | This library and other libraries such as graphql-js typed what should be `AsyncGenerator` as `AsyncIterableIterator`. 17 | 18 | The main difference between those two types is that on the former the `return` method is not optional. This resulted in confusion when using TypeScript as the `return` method is actually always present. 19 | 20 | Here are the TypeScript type definitions for comparison. 21 | 22 | ```ts 23 | interface AsyncGenerator 24 | extends AsyncIterator { 25 | // NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places. 26 | next(...args: [] | [TNext]): Promise>; 27 | return( 28 | value: TReturn | PromiseLike 29 | ): Promise>; 30 | throw(e: any): Promise>; 31 | [Symbol.asyncIterator](): AsyncGenerator; 32 | } 33 | ``` 34 | 35 | ```ts 36 | interface AsyncIterator { 37 | // NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places. 38 | next(...args: [] | [TNext]): Promise>; 39 | return?( 40 | value?: TReturn | PromiseLike 41 | ): Promise>; 42 | throw?(e?: any): Promise>; 43 | } 44 | 45 | interface AsyncIterableIterator extends AsyncIterator { 46 | [Symbol.asyncIterator](): AsyncIterableIterator; 47 | } 48 | ``` 49 | 50 | Unfortunately, the name of this library is now a bit misleading. `@n1ru4l/push-pull-async-generator` might be the be the better pick. For now I will not deprecate and rename it. 51 | 52 | ## 3.0.0 53 | 54 | ### Major Changes 55 | 56 | - 21a2470: drop support for Node 12; support ESM; use bob-the-bundler for bundling instead of tsdx 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-present Laurin Quast (laurinquast@googlemail.com) 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `@n1ru4l/push-pull-async-iterable-iterator` 2 | 3 | [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) 4 | [![npm version](https://img.shields.io/npm/v/@n1ru4l/push-pull-async-iterable-iterator)](https://www.npmjs.com/package/@n1ru4l/push-pull-async-iterable-iterator) 5 | [![npm downloads](https://img.shields.io/npm/dm/@n1ru4l/push-pull-async-iterable-iterator)](https://www.npmjs.com/package/@n1ru4l/push-pull-async-iterable-iterator) 6 | [![Dependents](https://img.shields.io/librariesio/dependents/npm/@n1ru4l/push-pull-async-iterable-iterator)](https://www.npmjs.com/browse/depended/@n1ru4l/push-pull-async-iterable-iterator) 7 | [![Build Status](https://img.shields.io/github/workflow/status/n1ru4l/push-pull-async-iterable-iterator/CI)](https://github.com/n1ru4l/push-pull-async-iterable-iterator/actions) 8 | 9 | Create an AsyncIterableIterator from anything (on any modern platform) while handling back-pressure! 10 | 11 | ```bash 12 | yarn install -E @n1ru4l/push-pull-async-iterable-iterator 13 | ``` 14 | 15 | **Standalone Usage** 16 | 17 | ```ts 18 | import { makePushPullAsyncIterableIterator } from "@n1ru4l/push-pull-async-iterable-iterator"; 19 | 20 | const { 21 | pushValue, 22 | asyncIterableIterator 23 | } = makePushPullAsyncIterableIterator(); 24 | pushValue(1); 25 | pushValue(2); 26 | pushValue(3); 27 | 28 | // prints 1, 2, 3 29 | for await (const value of asyncIterableIterator) { 30 | console.log(value); 31 | } 32 | ``` 33 | 34 | **Check if something is an AsyncIterable** 35 | 36 | ```ts 37 | import { isAsyncIterable } from "@n1ru4l/push-pull-async-iterable-iterator"; 38 | 39 | if (isAsyncIterable(something)) { 40 | for await (const value of something) { 41 | console.log(value); 42 | } 43 | } 44 | ``` 45 | 46 | _Note:_ On Safari iOS [`Symbol.asyncIterator` is not available](https://caniuse.com/mdn-javascript_builtins_symbol_asynciterator), therefore all async iterators used must be build using [AsyncGenerators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of#Iterating_over_async_generators). 47 | If a AsyncIterable that is NO AsyncGenerator is passed to `isAsyncIterable` on the Safari iOS environment, it will return the value `false`. 48 | 49 | **Wrap a Sink** 50 | 51 | ```ts 52 | import { makeAsyncIterableIteratorFromSink } from "@n1ru4l/push-pull-async-iterable-iterator"; 53 | // let's use some GraphQL client :) 54 | import { createClient } from "graphql-ws/lib/use/ws"; 55 | 56 | const client = createClient({ 57 | url: "ws://localhost:3000/graphql" 58 | }); 59 | 60 | const asyncIterableIterator = makeAsyncIterableIteratorFromSink(sink => { 61 | const dispose = client.subscribe( 62 | { 63 | query: "{ hello }" 64 | }, 65 | { 66 | next: sink.next, 67 | error: sink.error, 68 | complete: sink.complete 69 | } 70 | ); 71 | return () => dispose(); 72 | }); 73 | 74 | for await (const value of asyncIterableIterator) { 75 | console.log(value); 76 | } 77 | ``` 78 | 79 | **Apply an AsyncIterableIterator to a sink** 80 | 81 | ```tsx 82 | import Observable from "zen-observable"; 83 | import { 84 | makePushPullAsyncIterableIterator, 85 | applyAsyncIterableIteratorToSink 86 | } from "@n1ru4l/push-pull-async-iterable-iterator"; 87 | 88 | const { asyncIterableIterator } = makePushPullAsyncIterableIterator(); 89 | 90 | const observable = new Observable(sink => { 91 | const dispose = applyAsyncIterableIteratorToSink(asyncIterableIterator, sink); 92 | // dispose will be called when the observable subscription got destroyed 93 | // the dispose call will ensure that the async iterator is completed. 94 | return () => dispose(); 95 | }); 96 | 97 | const subscription = observable.subscribe({ 98 | next: console.log, 99 | complete: () => console.log("done."), 100 | error: () => console.log("error.") 101 | }); 102 | 103 | const interval = setInterval(() => { 104 | iterator.push("hi"); 105 | }, 1000); 106 | 107 | setTimeout(() => { 108 | subscription.unsubscribe(); 109 | clearInterval(interval); 110 | }, 5000); 111 | ``` 112 | 113 | **Put it all together** 114 | 115 | ```tsx 116 | import { Observable, RequestParameters, Variables } from "relay-runtime"; 117 | import { createClient } from "graphql-ws/lib/use/ws"; 118 | import { 119 | makeAsyncIterableFromSink, 120 | applyAsyncIterableIteratorToSink 121 | } from "@n1ru4l/push-pull-async-iterable-iterator"; 122 | import { createApplyLiveQueryPatch } from "@n1ru4l/graphql-live-query-patch"; 123 | 124 | const client = createClient({ 125 | url: "ws://localhost:3000/graphql" 126 | }); 127 | 128 | export const execute = (request: RequestParameters, variables: Variables) => { 129 | if (!request.text) { 130 | throw new Error("Missing document."); 131 | } 132 | const query = request.text; 133 | 134 | return Observable.create(sink => { 135 | // Create our asyncIterator from a Sink 136 | const executionResultIterator = makeAsyncIterableFromSink(wsSink => { 137 | const dispose = client.subscribe({ query }, wsSink); 138 | return () => dispose(); 139 | }); 140 | 141 | const applyLiveQueryPatch = createApplyLiveQueryPatch(); 142 | 143 | // apply some middleware to our asyncIterator 144 | const compositeIterator = applyLiveQueryPatch(executionResultIterator); 145 | 146 | // Apply our async iterable to the relay sink 147 | // unfortunately relay cannot consume an async iterable right now. 148 | const dispose = applyAsyncIterableIteratorToSink(compositeIterator, sink); 149 | // dispose will be called by relay when the observable is disposed 150 | // the dispose call will ensure that the async iterator is completed. 151 | return () => dispose(); 152 | }); 153 | }; 154 | ``` 155 | 156 | ## Operators 157 | 158 | This package also ships a few utilities that make your life easier! 159 | 160 | ### `map` 161 | 162 | Map a source 163 | 164 | ```ts 165 | import { map } from "@n1ru4l/push-pull-async-iterable-iterator"; 166 | 167 | async function* source() { 168 | yield 1; 169 | yield 2; 170 | yield 3; 171 | } 172 | 173 | const square = map((value: number): number => value * value); 174 | 175 | for await (const value of square(source())) { 176 | console.log(value); 177 | } 178 | // logs 1, 4, 9 179 | ``` 180 | 181 | ### `filter` 182 | 183 | Filter a source 184 | 185 | ```ts 186 | import { filter } from "@n1ru4l/push-pull-async-iterable-iterator"; 187 | 188 | async function* source() { 189 | yield 1; 190 | yield 2; 191 | yield 3; 192 | } 193 | 194 | const biggerThan1 = filter((value: number): number => value > 1); 195 | 196 | for await (const value of biggerThan1(source())) { 197 | console.log(value); 198 | } 199 | // logs 2, 3 200 | ``` 201 | 202 | ## Other helpers 203 | 204 | ### `withHandlers` 205 | 206 | Attach a return and throw handler to a source. 207 | 208 | ```ts 209 | import { withReturn } from "@n1ru4l/push-pull-async-iterable-iterator"; 210 | 211 | async function* source() { 212 | yield 1; 213 | yield 2; 214 | yield 3; 215 | } 216 | 217 | const sourceInstance = source(); 218 | 219 | const newSourceWithHandlers = withHandlers( 220 | sourceInstance, 221 | () => sourceInstance.return(), 222 | err => sourceInstance.throw(err) 223 | ); 224 | 225 | for await (const value of stream) { 226 | // ... 227 | } 228 | ``` 229 | -------------------------------------------------------------------------------- /bob.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | scope: "@n1ru4l" 5 | }; 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | "use strict"; 3 | 4 | module.exports = { 5 | roots: ["/src"], 6 | transform: { 7 | "^.+\\.tsx?$": "ts-jest" 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "3.2.0", 3 | "license": "MIT", 4 | "main": "dist/index.js", 5 | "module": "dist/index.mjs", 6 | "exports": { 7 | ".": { 8 | "require": "./dist/index.js", 9 | "import": "./dist/index.mjs" 10 | }, 11 | "./*": { 12 | "require": "./dist/*.js", 13 | "import": "./dist/*.mjs" 14 | } 15 | }, 16 | "typings": "dist/index.d.ts", 17 | "typescript": { 18 | "definition": "dist/index.d.ts" 19 | }, 20 | "engines": { 21 | "node": ">=12" 22 | }, 23 | "scripts": { 24 | "build": "tsc && bob build --single", 25 | "test": "jest", 26 | "prepare": "yarn build", 27 | "size": "size-limit", 28 | "analyze": "size-limit --why", 29 | "lint-staged": "lint-staged", 30 | "eslint": "eslint", 31 | "ci:eslint": "eslint src/**/*", 32 | "release": "changeset publish" 33 | }, 34 | "peerDependencies": {}, 35 | "name": "@n1ru4l/push-pull-async-iterable-iterator", 36 | "author": { 37 | "name": "Laurin Quast", 38 | "email": "laurinquast@googlemail.com", 39 | "url": "https://github.com/n1ru4l" 40 | }, 41 | "repository": "https://github.com/n1ru4l/push-pull-async-iterable-iterator", 42 | "size-limit": [ 43 | { 44 | "path": "dist/index.js", 45 | "limit": "1 KB" 46 | }, 47 | { 48 | "path": "dist/index.mjs", 49 | "limit": "1 KB" 50 | } 51 | ], 52 | "lint-staged": { 53 | "*.{js}": [ 54 | "eslint", 55 | "git add -f" 56 | ], 57 | "*.{js,json,css,md,ts,tsx}": [ 58 | "prettier --write", 59 | "git add -f" 60 | ] 61 | }, 62 | "devDependencies": { 63 | "@changesets/cli": "2.17.0", 64 | "@size-limit/preset-small-lib": "5.0.3", 65 | "@types/jest": "27.0.1", 66 | "@types/zen-observable": "0.8.3", 67 | "@typescript-eslint/eslint-plugin": "4.33.0", 68 | "@typescript-eslint/parser": "4.33.0", 69 | "bob-the-bundler": "1.5.1", 70 | "changesets": "1.0.2", 71 | "eslint": "7.32.0", 72 | "eslint-config-prettier": "8.3.0", 73 | "husky": "7.0.4", 74 | "jest": "27.0.6", 75 | "lint-staged": "11.2.6", 76 | "patch-package": "6.4.7", 77 | "size-limit": "5.0.3", 78 | "ts-jest": "27.0.7", 79 | "typescript": "4.5.4", 80 | "zen-observable": "0.8.15" 81 | }, 82 | "publishConfig": { 83 | "access": "public", 84 | "directory": "dist" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base"], 4 | "postUpdateOptions": ["yarnDedupeFewer"] 5 | } 6 | -------------------------------------------------------------------------------- /src/Sink.ts: -------------------------------------------------------------------------------- 1 | export type Sink = { 2 | next: (value: TValue) => void; 3 | error: (error: TError) => void; 4 | complete: () => void; 5 | }; 6 | -------------------------------------------------------------------------------- /src/applyAsyncIterableIteratorToSink.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | makePushPullAsyncIterableIterator, 3 | applyAsyncIterableIteratorToSink 4 | } from "."; 5 | import Observable from "zen-observable"; 6 | 7 | it("'applyAsyncIterableIteratorToSink' exists", () => { 8 | expect(applyAsyncIterableIteratorToSink).toBeDefined(); 9 | }); 10 | 11 | it("can be created", done => { 12 | // In the real-world you would create this iterator inside Observable constructor function. 13 | const { 14 | pushValue, 15 | asyncIterableIterator 16 | } = makePushPullAsyncIterableIterator(); 17 | const observable = new Observable(sink => { 18 | const dispose = applyAsyncIterableIteratorToSink( 19 | asyncIterableIterator, 20 | sink 21 | ); 22 | return dispose; 23 | }); 24 | 25 | const values = [] as unknown[]; 26 | 27 | observable.subscribe({ 28 | next: value => { 29 | values.push(value); 30 | }, 31 | error: err => { 32 | fail("Should not fail. " + err); 33 | }, 34 | complete: () => { 35 | expect(values).toEqual([1, 2, 3]); 36 | done(); 37 | } 38 | }); 39 | 40 | pushValue(1); 41 | pushValue(2); 42 | pushValue(3); 43 | process.nextTick(() => { 44 | asyncIterableIterator.return?.(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/applyAsyncIterableIteratorToSink.ts: -------------------------------------------------------------------------------- 1 | import { Sink } from "./Sink"; 2 | 3 | export function applyAsyncIterableIteratorToSink< 4 | TValue = unknown, 5 | TError = unknown 6 | >( 7 | asyncIterableIterator: AsyncIterableIterator, 8 | sink: Sink 9 | ): () => void { 10 | const run = async () => { 11 | try { 12 | for await (const value of asyncIterableIterator) { 13 | sink.next(value); 14 | } 15 | sink.complete(); 16 | } catch (err) { 17 | sink.error(err as TError); 18 | } 19 | }; 20 | run(); 21 | 22 | return () => { 23 | asyncIterableIterator.return?.(); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./makePushPullAsyncIterableIterator"; 2 | export * from "./Sink"; 3 | export * from "./makeAsyncIterableIteratorFromSink"; 4 | export * from "./applyAsyncIterableIteratorToSink"; 5 | export * from "./isAsyncIterable"; 6 | export * from "./withHandlers"; 7 | export * from "./withHandlersFrom"; 8 | 9 | export * from "./operators/filter"; 10 | export * from "./operators/map"; 11 | -------------------------------------------------------------------------------- /src/isAsyncIterable.ts: -------------------------------------------------------------------------------- 1 | export function isAsyncIterable( 2 | input: unknown 3 | ): input is AsyncIterator | AsyncIterableIterator { 4 | return ( 5 | typeof input === "object" && 6 | input !== null && 7 | // The AsyncGenerator check is for Safari on iOS which currently does not have 8 | // Symbol.asyncIterator implemented 9 | // That means every custom AsyncIterable must be built using a AsyncGeneratorFunction (async function * () {}) 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | ((input as any)[Symbol.toStringTag] === "AsyncGenerator" || 12 | (Symbol.asyncIterator && Symbol.asyncIterator in input)) 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/makeAsyncIterableIteratorFromSink.ts: -------------------------------------------------------------------------------- 1 | import { makePushPullAsyncIterableIterator } from "./makePushPullAsyncIterableIterator"; 2 | import { Sink } from "./Sink"; 3 | 4 | export const makeAsyncIterableIteratorFromSink = < 5 | TValue = unknown, 6 | TError = unknown 7 | >( 8 | make: (sink: Sink) => () => void 9 | ): AsyncIterableIterator => { 10 | const { 11 | pushValue, 12 | asyncIterableIterator 13 | } = makePushPullAsyncIterableIterator(); 14 | const dispose = make({ 15 | next: (value: TValue) => { 16 | pushValue(value); 17 | }, 18 | complete: () => { 19 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 20 | asyncIterableIterator.return!(); 21 | }, 22 | error: (err: TError) => { 23 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 24 | asyncIterableIterator.throw!(err); 25 | } 26 | }); 27 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 28 | const originalReturn = asyncIterableIterator.return!; 29 | let returnValue: ReturnType | undefined = undefined; 30 | asyncIterableIterator.return = () => { 31 | if (returnValue === undefined) { 32 | dispose(); 33 | returnValue = originalReturn(); 34 | } 35 | return returnValue; 36 | }; 37 | return asyncIterableIterator; 38 | }; 39 | -------------------------------------------------------------------------------- /src/makePushPullAsyncIterableIterator.spec.ts: -------------------------------------------------------------------------------- 1 | import { makePushPullAsyncIterableIterator } from "./makePushPullAsyncIterableIterator"; 2 | 3 | it(`"PushPullAsyncIterableIterator" exists`, () => { 4 | expect(makePushPullAsyncIterableIterator).toBeDefined(); 5 | }); 6 | 7 | it("can be created", () => { 8 | makePushPullAsyncIterableIterator(); 9 | }); 10 | 11 | it("can publish some values", async () => { 12 | const { 13 | pushValue, 14 | asyncIterableIterator 15 | } = makePushPullAsyncIterableIterator(); 16 | pushValue(1); 17 | pushValue(2); 18 | pushValue(3); 19 | 20 | let next = await asyncIterableIterator.next(); 21 | expect(next.value).toEqual(1); 22 | next = await asyncIterableIterator.next(); 23 | expect(next.value).toEqual(2); 24 | next = await asyncIterableIterator.next(); 25 | expect(next.value).toEqual(3); 26 | }); 27 | 28 | it("can publish a value for a waiting handler", async () => { 29 | const { 30 | pushValue, 31 | asyncIterableIterator 32 | } = makePushPullAsyncIterableIterator(); 33 | const nextP = asyncIterableIterator.next(); 34 | pushValue(1); 35 | const next = await nextP; 36 | expect(next.value).toEqual(1); 37 | }); 38 | 39 | it("can throw errors", async () => { 40 | const { asyncIterableIterator } = makePushPullAsyncIterableIterator(); 41 | const nextP = asyncIterableIterator.next(); 42 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 43 | asyncIterableIterator.throw!(new Error("Something got thrown.")).catch( 44 | () => undefined 45 | ); 46 | try { 47 | await nextP; 48 | fail("should throw"); 49 | } catch (err) { 50 | expect((err as Error).message).toEqual("Something got thrown."); 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /src/makePushPullAsyncIterableIterator.ts: -------------------------------------------------------------------------------- 1 | import { withHandlers } from "./withHandlers"; 2 | 3 | type Deferred = { 4 | resolve: (value: T) => void; 5 | reject: (value: unknown) => void; 6 | promise: Promise; 7 | }; 8 | 9 | function createDeferred(): Deferred { 10 | const d = {} as Deferred; 11 | d.promise = new Promise((resolve, reject) => { 12 | d.resolve = resolve; 13 | d.reject = reject; 14 | }); 15 | return d; 16 | } 17 | 18 | export type PushPullAsyncIterableIterator = { 19 | /* Push a new value that will be published on the AsyncIterableIterator. */ 20 | pushValue: (value: T) => void; 21 | /* AsyncIterableIterator that publishes the values pushed on the stack with pushValue. */ 22 | asyncIterableIterator: AsyncGenerator; 23 | }; 24 | 25 | const enum StateType { 26 | running = "running", 27 | error = "error", 28 | finished = "finished" 29 | } 30 | 31 | type RunningState = { 32 | type: StateType.running; 33 | }; 34 | 35 | type ErrorState = { 36 | type: StateType.error; 37 | error: unknown; 38 | }; 39 | 40 | type FinishedState = { 41 | type: StateType.finished; 42 | }; 43 | 44 | type State = RunningState | ErrorState | FinishedState; 45 | 46 | /** 47 | * makePushPullAsyncIterableIterator 48 | * 49 | * The iterable will publish values until return or throw is called. 50 | * Afterwards it is in the completed state and cannot be used for publishing any further values. 51 | * It will handle back-pressure and keep pushed values until they are consumed by a source. 52 | */ 53 | export function makePushPullAsyncIterableIterator< 54 | T 55 | >(): PushPullAsyncIterableIterator { 56 | let state = { 57 | type: StateType.running 58 | } as State; 59 | let next = createDeferred(); 60 | const values: Array = []; 61 | 62 | function pushValue(value: T) { 63 | if (state.type !== StateType.running) { 64 | return; 65 | } 66 | 67 | values.push(value); 68 | next.resolve(); 69 | next = createDeferred(); 70 | } 71 | 72 | const source = (async function* PushPullAsyncIterableIterator(): AsyncGenerator< 73 | T, 74 | void 75 | > { 76 | while (true) { 77 | if (values.length > 0) { 78 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 79 | yield values.shift()!; 80 | } else { 81 | if (state.type === StateType.error) { 82 | throw state.error; 83 | } 84 | if (state.type === StateType.finished) { 85 | return; 86 | } 87 | await next.promise; 88 | } 89 | } 90 | })(); 91 | 92 | const stream = withHandlers( 93 | source, 94 | () => { 95 | if (state.type !== StateType.running) { 96 | return; 97 | } 98 | state = { 99 | type: StateType.finished 100 | }; 101 | next.resolve(); 102 | }, 103 | (error: unknown) => { 104 | if (state.type !== StateType.running) { 105 | return; 106 | } 107 | state = { 108 | type: StateType.error, 109 | error 110 | }; 111 | next.resolve(); 112 | } 113 | ); 114 | 115 | return { 116 | pushValue, 117 | asyncIterableIterator: stream 118 | }; 119 | } 120 | -------------------------------------------------------------------------------- /src/operators/filter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Filter the events published by an AsyncIterable. 3 | */ 4 | export function filter( 5 | filter: (input: T) => input is U 6 | ): (asyncIterable: AsyncIterable) => AsyncGenerator; 7 | export function filter( 8 | filter: (input: T) => boolean 9 | ): (asyncIterable: AsyncIterable) => AsyncGenerator; 10 | export function filter(filter: (value: unknown) => boolean) { 11 | return async function* filterGenerator( 12 | asyncIterable: AsyncIterable 13 | ): AsyncGenerator { 14 | for await (const value of asyncIterable) { 15 | if (filter(value)) { 16 | yield value; 17 | } 18 | } 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/operators/map.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Map the events published by an AsyncIterable. 3 | */ 4 | export const map = (map: (input: T) => Promise | O) => 5 | async function* mapGenerator( 6 | asyncIterable: AsyncIterable 7 | ): AsyncGenerator { 8 | for await (const value of asyncIterable) { 9 | yield map(value); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/withHandlers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Attaches a cleanup handler to a AsyncIterable. 3 | * 4 | * @param source The source that should have a return handler attached 5 | * @param onReturn The return handler that should be attached 6 | * @returns 7 | */ 8 | export function withHandlers( 9 | source: AsyncIterable, 10 | onReturn?: () => void, 11 | onThrow?: (err: TError) => void 12 | ): AsyncGenerator { 13 | const stream = (async function* withReturnSource() { 14 | yield* source; 15 | })(); 16 | const originalReturn = stream.return.bind(stream); 17 | 18 | if (onReturn) { 19 | stream.return = (...args) => { 20 | onReturn(); 21 | return originalReturn(...args); 22 | }; 23 | } 24 | 25 | if (onThrow) { 26 | const originalThrow = stream.throw.bind(stream); 27 | stream.throw = (err: TError) => { 28 | onThrow(err); 29 | return originalThrow(err); 30 | }; 31 | } 32 | 33 | return stream; 34 | } 35 | -------------------------------------------------------------------------------- /src/withHandlersFrom.ts: -------------------------------------------------------------------------------- 1 | import { withHandlers } from "."; 2 | 3 | /** 4 | * Attaches a cleanup handler from and AsyncIterable to an AsyncIterable. 5 | * 6 | * @param source 7 | * @param target 8 | */ 9 | export function withHandlersFrom( 10 | /** The source that should be returned with attached handlers. */ 11 | source: AsyncIterable, 12 | /**The target on which the return and throw methods should be called. */ 13 | target: AsyncIterableIterator 14 | ): AsyncGenerator { 15 | return withHandlers( 16 | source, 17 | () => target.return?.(), 18 | err => target.throw?.(err) 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "exclude": ["**/*.spec.ts"], 4 | "compilerOptions": { 5 | "module": "ESNext", 6 | "lib": ["ESNext"], 7 | "target": "ES2018", 8 | "importHelpers": true, 9 | "declaration": true, 10 | "sourceMap": true, 11 | "rootDir": "./src", 12 | "strict": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noPropertyAccessFromIndexSignature": true, 18 | "moduleResolution": "node", 19 | "esModuleInterop": true, 20 | "allowSyntheticDefaultImports": true, 21 | "skipLibCheck": true, 22 | "forceConsistentCasingInFileNames": true, 23 | "downlevelIteration": true, 24 | "outDir": "dist" 25 | } 26 | } 27 | --------------------------------------------------------------------------------