├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── 1-bug.yml │ ├── 2-feature-request.yml │ ├── 3-doc.yml │ ├── 4-other.yml │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── test-pull-request.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── benchmarks ├── .gitignore ├── README.md ├── bun.lockb ├── index.ts ├── package.json └── tsconfig.json ├── bun.lockb ├── docs ├── README.md ├── astro.config.mjs ├── package.json ├── public │ ├── favicon.ico │ └── og.jpg ├── src │ ├── assets │ │ └── readme-banner.png │ ├── content │ │ ├── config.ts │ │ └── docs │ │ │ ├── es │ │ │ ├── getting-started │ │ │ │ └── why.mdx │ │ │ └── index.mdx │ │ │ ├── getting-started │ │ │ ├── installation.mdx │ │ │ ├── usage.mdx │ │ │ └── why.mdx │ │ │ ├── index.mdx │ │ │ └── reference │ │ │ ├── async-await-support.mdx │ │ │ ├── error-handling.mdx │ │ │ ├── methods.mdx │ │ │ └── pipe-function.mdx │ ├── env.d.ts │ └── styles │ │ └── custom.css └── tsconfig.json ├── package.json ├── src ├── api-docs.md ├── bun.lockb ├── fifo-queue.test.ts ├── fifo-queue.ts ├── package.json ├── pipe.bench.ts ├── pipe.test.ts ├── pipe.ts └── tsup.config.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | insert_final_newline = true 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 4 9 | max_line_length = 100 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [trvswgnr] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-bug.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug Report 2 | description: Report a bug to help us improve 3 | title: "[Bug]: " 4 | labels: ["bug", "triage"] 5 | body: 6 | - type: checkboxes 7 | id: no-duplicate-issues 8 | attributes: 9 | label: Is there an existing issue for this? 10 | description: 11 | Please search to see if an issue already exists for the bug you are encountering. 12 | options: 13 | - label: 14 | I have searched the existing 15 | [issues](https://github.com/trvswgnr/sloth-pipe/issues) 16 | required: true 17 | - type: markdown 18 | attributes: 19 | value: | 20 | Thanks for taking the time to fill out this bug report! We are grateful ❤ 21 | - type: textarea 22 | id: current-behavior 23 | attributes: 24 | label: Current Behavior 25 | description: A concise description of what you're experiencing. 26 | validations: 27 | required: true 28 | - type: textarea 29 | id: expected-behavior 30 | attributes: 31 | label: Expected Behavior 32 | description: A concise description of what you expected to happen. 33 | validations: 34 | required: false 35 | - type: textarea 36 | id: steps-to-reproduce 37 | attributes: 38 | label: Steps To Reproduce 39 | description: Steps to reproduce the behavior. 40 | placeholder: | 41 | 1. In this environment... 42 | 2. With this config... 43 | 3. Run '...' 44 | 4. See error... 45 | validations: 46 | required: false 47 | - type: textarea 48 | attributes: 49 | label: Anything else? 50 | description: | 51 | Links? References? Anything that will give us more context about the issue you are encountering! 52 | 53 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 54 | validations: 55 | required: false 56 | - type: input 57 | id: version 58 | attributes: 59 | label: Version 60 | description: What version of our package are you running? 61 | placeholder: ex. 0.0.7 62 | validations: 63 | required: true 64 | - type: input 65 | id: os 66 | attributes: 67 | label: Operating System 68 | description: What OS are you using? 69 | placeholder: ex. Windows 10, Mac 14, Ubuntu 23 [...] 70 | validations: 71 | required: false 72 | - type: dropdown 73 | id: browsers 74 | attributes: 75 | label: Browsers 76 | description: "What browsers are you seeing the problem on?" 77 | multiple: true 78 | options: 79 | - Chrome 80 | - Firefox 81 | - Safari 82 | - Microsoft Edge 83 | - Brave 84 | - Opera 85 | - Other 86 | default: 0 87 | validations: 88 | required: false 89 | - type: checkboxes 90 | id: willing-to-work 91 | attributes: 92 | label: "Are you willing to work on this issue?" 93 | options: 94 | - label: "Yes I am. (leave this unchecked for No)" 95 | required: false 96 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-feature-request.yml: -------------------------------------------------------------------------------- 1 | name: ✨ Feature Request 2 | description: Suggest an idea for improvement or a new feature 3 | title: "[Feature]: " 4 | labels: ["feature"] 5 | body: 6 | - type: checkboxes 7 | id: no-duplicate-issues 8 | attributes: 9 | label: Is there an existing issue for this? 10 | description: 11 | Please check to see whether the issue you want to create has already been reported. 12 | options: 13 | - label: 14 | I have searched the existing 15 | [issues](https://github.com/trvswgnr/sloth-pipe/issues) 16 | required: true 17 | - type: markdown 18 | attributes: 19 | value: | 20 | Thanks for taking the time to suggest a feature for this project! We are grateful ❤ 21 | - type: textarea 22 | id: issue-description 23 | attributes: 24 | label: Description of your feature request 25 | description: A clear and concise description of the feature you are requesting. 26 | validations: 27 | required: true 28 | - type: textarea 29 | id: motivation 30 | attributes: 31 | label: Motivation 32 | description: 33 | Explain why this feature would be beneficial and how it addresses a specific need or 34 | problem. 35 | validations: 36 | required: true 37 | - type: textarea 38 | id: context 39 | attributes: 40 | label: Additional Context 41 | description: Add any additional context or screenshots about the feature request here. 42 | validations: 43 | required: false 44 | - type: input 45 | id: version 46 | attributes: 47 | label: Version 48 | description: What version of our package are you running? 49 | placeholder: ex. 0.0.7 50 | validations: 51 | required: true 52 | - type: input 53 | id: os 54 | attributes: 55 | label: Operating System 56 | description: What OS are you using? 57 | placeholder: ex. Windows 10, Mac 14, Ubuntu 23 [...] 58 | validations: 59 | required: false 60 | - type: dropdown 61 | id: browsers 62 | attributes: 63 | label: Browsers 64 | description: "What browsers are you seeing the problem on?" 65 | multiple: true 66 | options: 67 | - Chrome 68 | - Firefox 69 | - Safari 70 | - Microsoft Edge 71 | - Brave 72 | - Opera 73 | - Other 74 | default: 0 75 | validations: 76 | required: false 77 | - type: checkboxes 78 | id: willing-to-work 79 | attributes: 80 | label: "Are you willing to work on this issue?" 81 | options: 82 | - label: "Yes I am. (leave this unchecked for No)" 83 | required: false 84 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-doc.yml: -------------------------------------------------------------------------------- 1 | name: 📄 Documentation Issue 2 | description: Report an issue with the documentation 3 | title: "[Doc]: " 4 | body: 5 | - type: checkboxes 6 | id: no-duplicate-issues 7 | attributes: 8 | label: Is there an existing issue for this? 9 | description: 10 | Please search to see whether the issue you are encountering has already been reported. 11 | options: 12 | - label: 13 | I have searched the existing 14 | [issues](https://github.com/trvswgnr/sloth-pipe/issues) 15 | required: true 16 | - type: markdown 17 | attributes: 18 | value: | 19 | Thanks for taking the time to report this issue! 20 | - type: textarea 21 | id: description 22 | attributes: 23 | label: Description 24 | description: A clear and concise description of the issue with the documentation. 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: page-section 29 | attributes: 30 | label: Page/Section 31 | description: 32 | Specify the page or section of the documentation where you encountered the problem. 33 | validations: 34 | required: false 35 | - type: textarea 36 | id: current-doc 37 | attributes: 38 | label: Current Documentation 39 | description: 40 | Provide the current content of the documentation related to the issue, if applicable. 41 | validations: 42 | required: false 43 | - type: textarea 44 | id: proposed-improvement 45 | attributes: 46 | label: Proposed Improvement 47 | description: Suggest a clear and concise improvement to the documentation. 48 | validations: 49 | required: false 50 | - type: textarea 51 | id: additional-context 52 | attributes: 53 | label: Additional Context 54 | description: 55 | Add any additional context or information that might help in addressing the 56 | documentation issue. 57 | validations: 58 | required: false 59 | - type: input 60 | id: version 61 | attributes: 62 | label: Version 63 | description: What version of our package are you running? 64 | placeholder: ex. 0.0.7 65 | validations: 66 | required: true 67 | - type: input 68 | id: os 69 | attributes: 70 | label: Operating System 71 | description: What OS are you using? 72 | placeholder: ex. Windows 10, Mac 14, Ubuntu 23 [...] 73 | validations: 74 | required: false 75 | - type: dropdown 76 | id: browsers 77 | attributes: 78 | label: Browsers 79 | description: "What browsers are you seeing the problem on?" 80 | multiple: true 81 | options: 82 | - Chrome 83 | - Firefox 84 | - Safari 85 | - Microsoft Edge 86 | - Brave 87 | - Opera 88 | - Other 89 | default: 0 90 | validations: 91 | required: true 92 | - type: checkboxes 93 | id: willing-to-work 94 | attributes: 95 | label: "Are you willing to work on this issue?" 96 | options: 97 | - label: "Yes I am. (leave this unchecked for No)" 98 | required: false 99 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/4-other.yml: -------------------------------------------------------------------------------- 1 | name: 📂 Other 2 | description: 3 | Report an issue or request that doesn't fall into other categories. Please do NOT create blank 4 | issue 5 | title: "[Other]: " 6 | body: 7 | - type: checkboxes 8 | id: no-duplicate-issues 9 | attributes: 10 | label: Is there an existing issue for this? 11 | description: 12 | Please check to see whether the issue you are encountering has already been reported. 13 | options: 14 | - label: 15 | I have searched the existing 16 | [issues](https://github.com/trvswgnr/sloth-pipe/issues) 17 | required: true 18 | - type: markdown 19 | attributes: 20 | value: | 21 | Thanks for taking the time to create this issue! 22 | - type: textarea 23 | id: issue-description 24 | attributes: 25 | label: Description 26 | description: A clear and concise description of the issue or request. 27 | validations: 28 | required: true 29 | - type: textarea 30 | id: issue-context 31 | attributes: 32 | label: Context 33 | description: Provide any relevant context or details about the issue. 34 | validations: 35 | required: false 36 | - type: textarea 37 | id: issue-reproduce 38 | attributes: 39 | label: Steps to Reproduce (if applicable) 40 | description: Steps to reproduce the behavior. 41 | placeholder: | 42 | 1. Step 1 43 | 2. Step 2 44 | 3. ... 45 | validations: 46 | required: false 47 | - type: textarea 48 | id: current 49 | attributes: 50 | label: Current Behavior 51 | description: Describe the current behavior. 52 | validations: 53 | required: false 54 | - type: textarea 55 | id: expected 56 | attributes: 57 | label: Expected Behavior 58 | description: Describe what you expected to happen. 59 | validations: 60 | required: false 61 | - type: textarea 62 | id: ss 63 | attributes: 64 | label: Screenshots (if applicable) 65 | description: Include screenshots or images if they help in understanding the issue. 66 | validations: 67 | required: false 68 | - type: textarea 69 | id: additional 70 | attributes: 71 | label: Additional Information 72 | description: Add any other information that might be relevant to the issue. 73 | validations: 74 | required: false 75 | - type: input 76 | id: version 77 | attributes: 78 | label: Version 79 | description: What version of our package are you running? 80 | placeholder: ex. 0.0.7 81 | validations: 82 | required: false 83 | - type: input 84 | id: os 85 | attributes: 86 | label: Operating System 87 | description: What OS are you using? 88 | placeholder: ex. Windows 10, Mac 14, Ubuntu 23 [...] 89 | validations: 90 | required: false 91 | - type: dropdown 92 | id: browsers 93 | attributes: 94 | label: "What browsers are you seeing the problem on?" 95 | multiple: true 96 | options: 97 | - Chrome 98 | - Firefox 99 | - Safari 100 | - Microsoft Edge 101 | - Brave 102 | - Opera 103 | - Other 104 | default: 0 105 | validations: 106 | required: false 107 | - type: checkboxes 108 | id: willing-to-work 109 | attributes: 110 | label: "Are you willing to work on this issue?" 111 | options: 112 | - label: "Yes I am. (leave this unchecked for No)" 113 | required: false 114 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Become a SPONSOR ❤ 4 | url: https://github.com/sponsors/trvswgnr 5 | about: Help this project by becoming a sponsor 6 | - name: Read our CONTRIBUTION GUIDE 7 | url: https://github.com/trvswgnr/sloth-pipe?tab=readme-ov-file#contributing 8 | about: This link will take you to our CONTRIBUTION GUIDE doc 9 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | #### Description 4 | 5 | - Closes # 6 | - What does this PR change? Give us a brief description. 7 | - Did you change something visual? A before/after screenshot can be helpful. 8 | 9 | 14 | -------------------------------------------------------------------------------- /.github/workflows/test-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Test Pull Request 2 | 3 | on: 4 | workflow_call: 5 | pull_request: 6 | branches: ["main"] 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | - name: Setup bun 15 | uses: oven-sh/setup-bun@v1 16 | - name: Install dependencies 17 | run: bun install 18 | - name: Fetch main 19 | run: git fetch --no-tags --prune --depth=1 origin main:refs/remotes/origin/main 20 | - name: Run tests 21 | working-directory: ./src 22 | run: bun test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS-specific files 2 | .DS_Store 3 | 4 | # Windows-specific files 5 | Thumbs.db 6 | 7 | # Jetbrains specific files 8 | .idea 9 | 10 | # Environment variables 11 | .env 12 | .env.local 13 | .env.development.local 14 | .env.test.local 15 | .env.production.local 16 | 17 | # Node modules and logs 18 | node_modules/ 19 | npm-debug.log_ 20 | yarn-debug.log* 21 | yarn-error.log* 22 | lerna-debug.log* 23 | .pnpm-debug.log* 24 | .cache 25 | *.tgz 26 | .yarn-integrity 27 | .parcel-cache 28 | .next 29 | .out 30 | .nuxt 31 | .dist 32 | .vuepress/dist 33 | .temp 34 | .docusaurus 35 | .serverless/ 36 | .fusebox/ 37 | .dynamodb/ 38 | .tern-port 39 | .yarn/cache 40 | .yarn/unplugged 41 | .yarn/build-state.yml 42 | .yarn/install-state.gz 43 | .pnp.* 44 | 45 | # Debug logs 46 | _scratch* 47 | logs 48 | _.log 49 | 50 | # Report files 51 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 52 | 53 | # Process IDs 54 | pids 55 | _.pid 56 | _.seed 57 | *.pid.lock 58 | 59 | # Coverage reports 60 | lib-cov 61 | coverage 62 | *.lcov 63 | .nyc_output 64 | 65 | # Grunt intermediate storage 66 | .grunt 67 | 68 | # Bower components 69 | bower_components 70 | 71 | # Node-waf configuration 72 | .lock-wscript 73 | 74 | # Compiled binary addons (https://nodejs.org/api/addons.html) 75 | build/Release 76 | 77 | # JSPM packages 78 | jspm_packages/ 79 | 80 | # Web modules 81 | web_modules/ 82 | 83 | # TypeScript build info 84 | *.tsbuildinfo 85 | 86 | # NPM debug logs 87 | .npm 88 | 89 | # Linter caches 90 | .eslintcache 91 | .stylelintcache 92 | 93 | # Rollup plugin caches 94 | .rpt2_cache/ 95 | .rts2_cache_cjs/ 96 | .rts2_cache_es/ 97 | .rts2_cache_umd/ 98 | 99 | # Node.js REPL history 100 | .node_repl_history 101 | 102 | # Visual Studio Code 103 | .vscode 104 | .vscode-test 105 | 106 | # generated types from Astro 107 | .astro/ 108 | 109 | # Astro's build output 110 | dist/ 111 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "quoteProps": "as-needed", 8 | "jsxSingleQuote": false, 9 | "trailingComma": "all", 10 | "bracketSpacing": true, 11 | "bracketSameLine": false, 12 | "arrowParens": "always", 13 | "requirePragma": false, 14 | "insertPragma": false, 15 | "proseWrap": "always", 16 | "htmlWhitespaceSensitivity": "ignore", 17 | "endOfLine": "lf", 18 | "embeddedLanguageFormatting": "auto", 19 | "singleAttributePerLine": false 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Travis Aaron Wagner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | associated documentation files (the "Software"), to deal in the Software without restriction, 7 | including without limitation the rights to use, copy, modify, merge, publish, distribute, 8 | sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial 12 | portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 15 | NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES 17 | OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![sloth-pipe](/docs/src/assets/readme-banner.png) 2 | 3 | # Sloth Pipe 4 | 5 | 6 | 7 | [![github latest release](https://badgen.net/github/tag/trvswgnr/sloth-pipe?label=latest&cache=600)](https://github.com/trvswgnr/sloth-pipe/releases/latest) 8 | [![npm version](https://badgen.net/npm/v/sloth-pipe?cache=600)](https://www.npmjs.com/package/sloth-pipe) 9 | ![npm weekly downloads](https://img.shields.io/npm/dw/sloth-pipe) 10 | ![dependencies](https://img.shields.io/badge/dependencies-0-orange) 11 | [![license](https://img.shields.io/github/license/trvswgnr/sloth-pipe)](LICENSE) 12 | [![open issues](https://badgen.net/github/open-issues/trvswgnr/sloth-pipe?label=issues)](https://github.com/trvswgnr/sloth-pipe/issues) 13 | [![minzipped size](https://img.shields.io/bundlephobia/minzip/sloth-pipe)](https://bundlephobia.com/result?p=sloth-pipe) 14 | ![follow on xitter](https://img.shields.io/twitter/follow/techsavvytravvy?style=social) 15 | 16 | 17 | 18 | Sloth Pipe is a tiny library for TypeScript and JavaScript that lets you create lazy, chainable, and 19 | reusable pipes for data transformation and processing. Borrowing from functional programming 20 | paradigms, it offers a convenient and powerful way to compose functions and manage data flow in an 21 | application, with an emphasis on lazy evaluation and efficient execution. 22 | 23 | ## Why Sloth Pipe? 24 | 25 | Developers want pipes. They've been one of the 26 | [most requested features](https://2020.stateofjs.com/en-US/opinions/#missing_from_js) in JavaScript 27 | [for years](https://2022.stateofjs.com/en-US/opinions/#top_currently_missing_from_js), and there's 28 | even a [Stage 2 proposal](https://github.com/tc39/proposal-pipeline-operator) for adding them to the 29 | language. Sloth Pipe isn't a direct replacement for the proposed pipeline operator, but it does 30 | offer a similar experience and many of the 31 | [same benefits](https://github.com/tc39/proposal-pipeline-operator#why-a-pipe-operator). 32 | 33 | ## Features 34 | 35 | - **Lazy Evaluation**: Computations are only performed when necessary, optimizing performance and 36 | resource utilization. 37 | - **Chainable API**: Enables the creation of fluent and readable code by chaining multiple 38 | operations. 39 | - **Error Handling**: Built-in support for error handling within the pipe. 40 | - **Async/Await Compatibility**: Seamlessly integrate asynchronous functions into your pipes. 41 | - **Tap Operations**: Allows side-effects without altering the pipe's main data flow. 42 | - **Reusable pipes**: Easily reuse pipes, even after execution. 43 | - **Extensible**: Easily extendable with custom functions and operations. 44 | - **Type-Safe**: Written in TypeScript, with full support for type inference and type safety. 45 | - **Lightweight**: Small and lightweight, with no external dependencies. 46 | - **Well-Tested**: Thoroughly tested with 100% code coverage. 47 | 48 | ## Installation 49 | 50 | To install Sloth Pipe, use the following command: 51 | 52 | ```bash 53 | bun i sloth-pipe 54 | ``` 55 | 56 | or 57 | 58 | ```bash 59 | npm install sloth-pipe 60 | ``` 61 | 62 | ## Usage 63 | 64 | Here's a simple example of how to use Sloth Pipe: 65 | 66 | ```typescript 67 | import { Pipe } from "sloth-pipe"; 68 | 69 | const result = Pipe(5) 70 | .to((x) => x * 2) 71 | .to((x) => x + 3) 72 | .exec(); 73 | 74 | console.log(result); // Outputs: 13 75 | ``` 76 | 77 | ### Async Operations 78 | 79 | Sloth Pipe seamlessly integrates with asynchronous operations: 80 | 81 | ```typescript 82 | const add = async (x: Promise, y: number) => { 83 | const xVal = await x; 84 | return xVal + y; 85 | }; 86 | const asyncResult = await Pipe(5) 87 | .to(async (x) => x * 2) 88 | .to(add, 3) // pass additional arguments to any function 89 | .exec(); 90 | 91 | console.log(asyncResult); // Outputs: 13 92 | ``` 93 | 94 | ### Error Handling 95 | 96 | Handle errors gracefully within the pipe: 97 | 98 | ```typescript 99 | const safeResult = Pipe(5) 100 | .to((x) => { 101 | if (x > 0) throw new Error("Example error"); 102 | return x; 103 | }) 104 | .catch((err) => 0) 105 | .exec(); 106 | 107 | console.log(safeResult); // Outputs: 0 108 | ``` 109 | 110 | ## API Reference 111 | 112 | The API consists of a few key methods: `to`, `tap`, `exec`, and `catch`. For a detailed reference, 113 | please refer to the [API documentation](https://sloth-pipe.vercel.app/). 114 | 115 | ## Contributing 116 | 117 | Any and all contributions are welcome! Open an issue or submit a pull request to contribute. 118 | 119 | This project uses [Bun](https://bun.sh) for development. To get started, clone the repository and 120 | run `bun install` to install dependencies. Then, run `bun test` to run the test suite. 121 | 122 | To build the project, run `bun build`. The output will be in the `dist` directory. 123 | 124 | ## License 125 | 126 | This project is licensed under the [MIT License](LICENSE). 127 | -------------------------------------------------------------------------------- /benchmarks/.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | -------------------------------------------------------------------------------- /benchmarks/README.md: -------------------------------------------------------------------------------- 1 | # benchmarks 2 | 3 | To install dependencies: 4 | 5 | ```bash 6 | bun install 7 | ``` 8 | 9 | To run: 10 | 11 | ```bash 12 | bun run index.ts 13 | ``` 14 | 15 | This project was created using `bun init` in bun v1.0.25. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. 16 | -------------------------------------------------------------------------------- /benchmarks/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trvswgnr/sloth-pipe/4307751915b0632299e59b96430f4a054b01dc33/benchmarks/bun.lockb -------------------------------------------------------------------------------- /benchmarks/index.ts: -------------------------------------------------------------------------------- 1 | import { group, bench, baseline, run } from "mitata"; 2 | import { pipe as EffectPipe } from "effect"; 3 | import { Pipe as SlothPipe } from "../pipe"; 4 | 5 | const exampleFn1 = (x: number) => x + 1; 6 | const exampleFn2 = (x: number) => x + 2; 7 | const spipe = SlothPipe(0).to(exampleFn1).to(exampleFn2); 8 | 9 | group("compare vs libs with sync functions", () => { 10 | baseline("Sloth Pipe", () => { 11 | spipe.exec(); 12 | }); 13 | bench("Effect Pipe", () => { 14 | EffectPipe(0, exampleFn1, exampleFn2); 15 | }); 16 | }); 17 | 18 | const asyncSpipe = SlothPipe(Promise.resolve(0)) 19 | .to(async (x) => (await x) + 1) 20 | .to(async (x) => (await x) + 1); 21 | group("compare to libs with async functions", () => { 22 | baseline("Sloth Pipe", async () => { 23 | await asyncSpipe.exec(); 24 | }); 25 | bench("Effect Pipe", async () => { 26 | await EffectPipe( 27 | Promise.resolve(0), 28 | async (x) => (await x) + 1, 29 | async (x) => (await x) + 1, 30 | ); 31 | }); 32 | bench("Native Promise", async () => { 33 | await Promise.resolve(0) 34 | .then((x) => x + 1) 35 | .then((x) => x + 1); 36 | }); 37 | }); 38 | 39 | await run(); 40 | 41 | // some extra micro benchmarks: 42 | const spipe2 = SlothPipe(0).to(exampleFn1).to(exampleFn2); 43 | printMicroDiff( 44 | microBench("Sloth Pipe", 1000000, () => spipe2.exec()), 45 | microBench("Effect Pipe", 1000000, () => EffectPipe(0, exampleFn1, exampleFn2)), 46 | ); 47 | 48 | const spipe3 = SlothPipe(0).to(exampleFn1).to(exampleFn2); 49 | printMicroDiff( 50 | microBench("Sloth Pipe", 1000000, () => spipe3.exec()), 51 | microBench("Effect Pipe", 1000000, () => EffectPipe(0, exampleFn1, exampleFn2)), 52 | ); 53 | 54 | printTimeDiff( 55 | timeBench("Sloth Pipe", 1, () => spipe.exec()), 56 | timeBench("Effect Pipe", 1, () => EffectPipe(0, exampleFn1, exampleFn2)), 57 | ); 58 | 59 | // run micro benchmark 60 | function microBench(name: string, runs: number, fn: () => void) { 61 | const start = Bun.nanoseconds(); 62 | for (let i = 0; i < runs; i++) { 63 | fn(); 64 | } 65 | const end = Bun.nanoseconds(); 66 | const total = end - start; 67 | const avg = total / runs; 68 | return { name, avg, total, runs }; 69 | } 70 | 71 | type MicroBench = ReturnType; 72 | 73 | function printMicroDiff(a: MicroBench, b: MicroBench) { 74 | console.log(); 75 | console.log(`${a.name}: ${locale(a.avg)}ns`); 76 | console.log(`${b.name}: ${locale(b.avg)}ns`); 77 | if (a.avg < b.avg) { 78 | const times = (b.avg / a.avg).toLocaleString("en-US", { maximumFractionDigits: 2 }); 79 | console.log(`${a.name} is ${times}x faster than ${b.name}`); 80 | } else if (a.avg > b.avg) { 81 | const times = (a.avg / b.avg).toLocaleString("en-US", { maximumFractionDigits: 2 }); 82 | console.log(`${b.name} is ${times}x faster than ${a.name}`); 83 | } else { 84 | console.log(`${a.name} and ${b.name} are the same speed`); 85 | } 86 | } 87 | 88 | // time-based benchmarks (how many runs can be done in n seconds) 89 | function timeBench(name: string, seconds: number, fn: () => void) { 90 | const start = Bun.nanoseconds(); 91 | let runs = 0; 92 | while (Bun.nanoseconds() - start < seconds * 1e9) { 93 | fn(); 94 | runs++; 95 | } 96 | return { name, runs, seconds }; 97 | } 98 | type TimeBench = ReturnType; 99 | 100 | function printTimeDiff(a: TimeBench, b: TimeBench) { 101 | console.log(); 102 | console.log(`${a.name}: ${locale(a.runs)} runs in ${a.seconds} seconds`); 103 | console.log(`${b.name}: ${locale(b.runs)} runs in ${b.seconds} seconds`); 104 | if (a.runs > b.runs) { 105 | const times = (a.runs / b.runs).toLocaleString("en-US", { maximumFractionDigits: 2 }); 106 | console.log(`${a.name} is ${times}x faster than ${b.name}`); 107 | } else if (a.runs < b.runs) { 108 | const times = (b.runs / a.runs).toLocaleString("en-US", { maximumFractionDigits: 2 }); 109 | console.log(`${b.name} is ${times}x faster than ${a.name}`); 110 | } else { 111 | console.log(`${a.name} and ${b.name} are the same speed`); 112 | } 113 | } 114 | 115 | function locale(nanoseconds: number): string { 116 | return nanoseconds.toLocaleString("en-US", { maximumFractionDigits: 2 }); 117 | } 118 | -------------------------------------------------------------------------------- /benchmarks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "benchmarks", 3 | "module": "index.ts", 4 | "type": "module", 5 | "devDependencies": { 6 | "@types/bun": "latest", 7 | "mitata": "^0.1.8" 8 | }, 9 | "peerDependencies": { 10 | "typescript": "^5.0.0" 11 | }, 12 | "dependencies": { 13 | "effect": "^2.2.2" 14 | } 15 | } -------------------------------------------------------------------------------- /benchmarks/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext"], 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleDetection": "force", 7 | "jsx": "react-jsx", 8 | "allowJs": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "verbatimModuleSyntax": true, 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "skipLibCheck": true, 18 | "strict": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "forceConsistentCasingInFileNames": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trvswgnr/sloth-pipe/4307751915b0632299e59b96430f4a054b01dc33/bun.lockb -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Starlight Starter Kit: Basics 2 | 3 | [![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build) 4 | 5 | ``` 6 | npm create astro@latest -- --template starlight 7 | ``` 8 | 9 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics) 10 | [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics) 11 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwithastro%2Fstarlight%2Ftree%2Fmain%2Fexamples%2Fbasics&project-name=my-starlight-docs&repository-name=my-starlight-docs) 12 | 13 | > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! 14 | 15 | ## 🚀 Project Structure 16 | 17 | Inside of your Astro + Starlight project, you'll see the following folders and files: 18 | 19 | ``` 20 | . 21 | ├── public/ 22 | ├── src/ 23 | │ ├── assets/ 24 | │ ├── content/ 25 | │ │ ├── docs/ 26 | │ │ └── config.ts 27 | │ └── env.d.ts 28 | ├── astro.config.mjs 29 | ├── package.json 30 | └── tsconfig.json 31 | ``` 32 | 33 | Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name. 34 | 35 | Images can be added to `src/assets/` and embedded in Markdown with a relative link. 36 | 37 | Static assets, like favicons, can be placed in the `public/` directory. 38 | 39 | ## 🧞 Commands 40 | 41 | All commands are run from the root of the project, from a terminal: 42 | 43 | | Command | Action | 44 | | :------------------------ | :----------------------------------------------- | 45 | | `npm install` | Installs dependencies | 46 | | `npm run dev` | Starts local dev server at `localhost:4321` | 47 | | `npm run build` | Build your production site to `./dist/` | 48 | | `npm run preview` | Preview your build locally, before deploying | 49 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | 50 | | `npm run astro -- --help` | Get help using the Astro CLI | 51 | 52 | ## 👀 Want to learn more? 53 | 54 | Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat). 55 | -------------------------------------------------------------------------------- /docs/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config'; 2 | import starlight from '@astrojs/starlight'; 3 | 4 | 5 | const locales = { 6 | root: { label: 'English', lang: 'en' }, 7 | es: { label: 'Español', lang: 'es' }, 8 | }; 9 | 10 | /* https://vercel.com/docs/projects/environment-variables/system-environment-variables#system-environment-variables */ 11 | const VERCEL_PREVIEW_SITE = 12 | process.env.VERCEL_ENV !== 'production' && 13 | process.env.VERCEL_URL && 14 | `https://${process.env.VERCEL_URL}`; 15 | 16 | const site = VERCEL_PREVIEW_SITE || 'https://sloth-pipe.vercel.app/'; 17 | 18 | // https://astro.build/config 19 | export default defineConfig({ 20 | integrations: [ 21 | starlight({ 22 | title: 'Sloth Pipe', 23 | social: { 24 | github: 'https://github.com/trvswgnr/sloth-pipe', 25 | "x.com": "https://twitter.com/techsavvytravvy", 26 | }, 27 | favicon: '/favicon.ico', 28 | head: [ 29 | { 30 | tag: 'meta', 31 | attrs: { property: 'og:image', content: site + 'og.jpg?v=1' }, 32 | }, 33 | { 34 | tag: 'meta', 35 | attrs: { property: 'twitter:image', content: site + 'og.jpg?v=1' }, 36 | }, 37 | ], 38 | customCss: [ 39 | // Relative path to your custom CSS file 40 | './src/styles/custom.css', 41 | ], 42 | locales, 43 | editLink: { 44 | baseUrl: 45 | "https://github.com/trvswgnr/sloth-pipe/edit/main/docs/", 46 | }, 47 | lastUpdated: true, 48 | sidebar: [ 49 | { 50 | label: 'Getting started', 51 | translations: { 52 | es: "Comienza aqui" 53 | }, 54 | items: [ 55 | { label: 'Why Sloth Pipe?', translations: { es: "¿Por qué Sloth Pipe?" }, link: '/getting-started/why/' }, 56 | { label: 'Installation', translations: { es: "Instalación" }, link: '/getting-started/installation/' }, 57 | { label: 'Usage', translations: { es: "Uso" }, link: '/getting-started/usage/' }, 58 | ], 59 | }, 60 | { 61 | label: 'Reference', 62 | translations: { 63 | es: "Referencia" 64 | }, 65 | items: [ 66 | { label: 'Pipe Function', translations: { es: "Función Pipe" }, link: '/reference/pipe-function/' }, 67 | { label: 'Methods', translations: { es: "Métodos" }, link: '/reference/methods/' }, 68 | { label: 'Error Handling', translations: { es: "Manejo de Errores" }, link: '/reference/error-handling/' }, 69 | { label: 'Async Await Support', translations: { es: "Soporte para Async Await" }, link: '/reference/async-await-support/' }, 70 | ], 71 | }, 72 | ], 73 | }), 74 | ], 75 | }); 76 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro check && astro build", 9 | "preview": "astro preview", 10 | "astro": "astro" 11 | }, 12 | "dependencies": { 13 | "@astrojs/starlight": "^0.17.2", 14 | "astro": "^4.2.1", 15 | "sharp": "^0.32.5", 16 | "@astrojs/check": "^0.4.1", 17 | "typescript": "^5.3.3" 18 | } 19 | } -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trvswgnr/sloth-pipe/4307751915b0632299e59b96430f4a054b01dc33/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/og.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trvswgnr/sloth-pipe/4307751915b0632299e59b96430f4a054b01dc33/docs/public/og.jpg -------------------------------------------------------------------------------- /docs/src/assets/readme-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trvswgnr/sloth-pipe/4307751915b0632299e59b96430f4a054b01dc33/docs/src/assets/readme-banner.png -------------------------------------------------------------------------------- /docs/src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection } from 'astro:content'; 2 | import { docsSchema, i18nSchema } from '@astrojs/starlight/schema'; 3 | 4 | export const collections = { 5 | docs: defineCollection({ schema: docsSchema() }), 6 | i18n: defineCollection({ type: 'data', schema: i18nSchema() }), 7 | }; 8 | -------------------------------------------------------------------------------- /docs/src/content/docs/es/getting-started/why.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: ¿Por qué Sloth Pipe? 3 | description: ¿Por qué deberías usar Sloth Pipe? 4 | --- 5 | 6 | Los desarrolladores quieren pipes. Han sido una de las 7 | [características más solicitadas](https://2022.stateofjs.com/en-US/opinions/#top_currently_missing_from_js) en JavaScript 8 | [por años](https://2022.stateofjs.com/en-US/opinions/#top_currently_missing_from_js), e incluso 9 | hay una [propuesta en etapa 2](https://github.com/tc39/proposal-pipeline-operator) para agregarlas al 10 | lenguaje. Sloth Pipe no es un reemplazo directo del operador de canalización propuesto, pero sí 11 | ofrece una experiencia similar y muchos de los 12 | [beneficios](https://github.com/tc39/proposal-pipeline-operator#why-a-pipe-operator). 13 | 14 | ## Características 15 | 16 | Slot Pipe ofrece una amplia gama de características que lo convierten en una herramienta poderosa y flexible con la cual 17 | trabajar: 18 | 19 | - **Evaluación Perezosa:** Los cálculos solo se realizan cuando es necesario, optimizando el rendimiento y la 20 | utilización de recursos. 21 | - **API encadenable:** Habilita la creación de código fluido y legible encadenando múltiples 22 | operaciones. 23 | - **Manejo de errores:** Soporte integrado para el manejo de errores dentro de la pipe. 24 | - **Compatibilidad Asyn/Await:** Integra sin problemas funciones asíncronas en tus pipes. 25 | - **Operaciones Tap:** Permite efectos secundarios sin alterar el flujo de datos principal de la pipe. 26 | - **Pipes reusables:** Reutiliza fácilmente pipes, incluso después de la ejecución. 27 | - **Extensible:** Fácilmente extensible con funciones y operaciones personalizadas. 28 | - **Seguridad de tipo:** Escrito en TypeScript, con soporte completo para inferencia de tipos y seguridad de tipos. 29 | - **Liviana:** Pequeña y liviana, sin dependencias externas. 30 | - **Bien probada:** Probada a fondo con una cobertura de código del 100%. 31 | -------------------------------------------------------------------------------- /docs/src/content/docs/es/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Bienvenido a Sloth Pipe 3 | description: Comienza a usar sloth-pipe para crear funciones con pipes. 4 | template: splash 5 | hero: 6 | tagline: ¡Una pequeña biblioteca para TypeScript y JavaScript que te permite crear pipes perezosas, encadenables y reutilizables para la transformación y el procesamiento de datos! 7 | image: 8 | file: ../../../assets/readme-banner.png 9 | actions: 10 | - text: Comienza aquí 11 | link: /es/getting-started/why/ 12 | icon: right-arrow 13 | variant: primary 14 | - text: Repositorio de Github 15 | link: https://github.com/trvswgnr/sloth-pipe 16 | icon: github 17 | --- 18 | 19 | -------------------------------------------------------------------------------- /docs/src/content/docs/getting-started/installation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | description: Learn how to install sloth-pipe in your project. 4 | --- 5 | 6 | import { Tabs, TabItem } from '@astrojs/starlight/components'; 7 | 8 | Add `sloth-pipe` to your project using your favorite package manager: 9 | 10 | 11 | 12 | ```bash 13 | npm install sloth-pipe 14 | ``` 15 | 16 | 17 | ```bash 18 | yarn add sloth-pipe 19 | ``` 20 | 21 | 22 | ```bash 23 | pnpm add sloth-pipe 24 | ``` 25 | 26 | 27 | ```bash 28 | bun install sloth-pipe 29 | ``` 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /docs/src/content/docs/getting-started/usage.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Usage 3 | description: Learn how to use sloth-pipe in your projects. 4 | --- 5 | 6 | Sloth Pipe is a functional library designed to streamline the process of applying a sequence of 7 | transformations to a value in TypeScript. It enables chaining multiple operations in a clear and 8 | readable manner. 9 | 10 | ## Importing the Library 11 | 12 | Begin by importing the Pipe function from the sloth-pipe library: 13 | 14 | ```ts 15 | import { Pipe } from "sloth-pipe"; 16 | ``` 17 | 18 | ## Initializing the Pipe 19 | 20 | Create a new pipe instance by calling Pipe() with the initial value. This value is the starting 21 | point for subsequent transformations: 22 | 23 | ```ts 24 | const pipeInstance = Pipe(5); 25 | ``` 26 | 27 | ## Applying Transformations 28 | 29 | Use the .to() method to define transformations. Each .to() takes a function as an argument. This 30 | function describes how the current value should be transformed: 31 | 32 | The first transformation multiplies the input by 2: 33 | 34 | ```ts 35 | .to((x) => x * 2) 36 | ``` 37 | 38 | The second transformation adds 3 to the result of the first transformation: 39 | 40 | ```ts 41 | .to((x) => x + 3) 42 | ``` 43 | 44 | ## Executing the Pipe 45 | 46 | Complete the chain of transformations with the .exec() method. This method executes the 47 | transformations in sequence and returns the final result: 48 | 49 | ```ts 50 | const result = pipeInstance.exec(); 51 | ``` 52 | 53 | ## Output: 54 | 55 | The final result can be used as needed. In this example, it's logged to the console: 56 | 57 | ```ts 58 | console.log(result); // Outputs: 13 59 | ``` 60 | -------------------------------------------------------------------------------- /docs/src/content/docs/getting-started/why.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why Sloth Pipe? 3 | description: Why should you use Sloth Pipe? 4 | --- 5 | 6 | Developers want pipes. They've been one of the 7 | [most requested features](https://2020.stateofjs.com/en-US/opinions/#missing_from_js) in JavaScript 8 | [for years](https://2022.stateofjs.com/en-US/opinions/#top_currently_missing_from_js), and there's 9 | even a [Stage 2 proposal](https://github.com/tc39/proposal-pipeline-operator) for adding them to the 10 | language. Sloth Pipe isn't a direct replacement for the proposed pipeline operator, but it does 11 | offer a similar experience and many of the 12 | [same benefits](https://github.com/tc39/proposal-pipeline-operator#why-a-pipe-operator). 13 | 14 | ## Features 15 | 16 | Sloth Pipe offers a wide range of features that make it a powerful and flexible tool for working 17 | with: 18 | 19 | - **Lazy Evaluation:** Computations are only performed when necessary, optimizing performance and 20 | resource utilization. 21 | - **Chainable API:** Enables the creation of fluent and readable code by chaining multiple 22 | operations. 23 | - **Error Handling:** Built-in support for error handling within the pipe. 24 | - **Async/Await Compatibility:** Seamlessly integrate asynchronous functions into your pipes. 25 | - **Tap Operations:** Allows side-effects without altering the pipe's main data flow. 26 | - **Reusable pipes:** Easily reuse pipes, even after execution. 27 | - **Extensible:** Easily extendable with custom functions and operations. 28 | - **Type-Safe:** Written in TypeScript, with full support for type inference and type safety. 29 | - **Lightweight:** Small and lightweight, with no external dependencies. 30 | - **Well-Tested:** Thoroughly tested with 100% code coverage. 31 | -------------------------------------------------------------------------------- /docs/src/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Welcome to Sloth Pipe 3 | description: Get started piping functions with sloth-pipe. 4 | template: splash 5 | hero: 6 | tagline: A tiny library for TypeScript and JavaScript that lets you create lazy, chainable, and reusable pipes for data transformation and processing! 7 | image: 8 | file: ../../assets/readme-banner.png 9 | actions: 10 | - text: Get started 11 | link: /getting-started/why/ 12 | icon: right-arrow 13 | variant: primary 14 | - text: Github repository 15 | link: https://github.com/trvswgnr/sloth-pipe 16 | icon: github 17 | --- 18 | 19 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/async-await-support.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Async Await Support 3 | description: sloth-pipe supports asynchronous operations within the pipeline. 4 | --- 5 | 6 | `sloth-pipe` supports asynchronous operations within the pipeline. Functions passed to `to` or `tap` 7 | can be asynchronous, and `exec` will return a Promise if any function in the pipeline is 8 | asynchronous. 9 | 10 | **Async Example**: 11 | 12 | ```typescript 13 | const asyncResult = await Pipe(5) 14 | .to(async (x) => x * 2) 15 | .to(async (x) => x + 3) 16 | .exec(); 17 | 18 | console.log(asyncResult); // Outputs: 13 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/error-handling.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Error Handling 3 | description: How to handle errors with sloth-pipe 4 | --- 5 | 6 | `sloth-pipe` allows handling errors that may occur during function execution in the pipeline. When an 7 | error is thrown in a `to` or `tap` method, the `catch` method is invoked if it is defined. This does 8 | not stop the pipeline from executing, but it does allow for graceful error handling. 9 | 10 | **Example**: 11 | 12 | ```typescript 13 | const result = Pipe(5) 14 | .to((x) => { 15 | throw new Error("Example Error"); 16 | }) 17 | .catch((err) => "Error occurred") 18 | .exec(); 19 | 20 | console.log(result); // Outputs: "Error occurred" 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/methods.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Methods 3 | description: Exploring the methods of sloth-pipe. 4 | --- 5 | 6 | ## `to` 7 | 8 | ### pipe.to(fn: Function, ...args: any[]): Pipeable 9 | 10 | Adds a transformation function to the pipe. 11 | 12 | **Parameters**: 13 | 14 | - `fn`: Function to be applied to the current value. 15 | - `args`: Additional arguments to pass to the function. 16 | 17 | **Returns**: The modified `Pipeable` object for chaining. 18 | 19 | **Example**: 20 | 21 | ```typescript 22 | pipe.to((x) => x * 2); 23 | ``` 24 | 25 | ## `tap` 26 | 27 | ### pipe.tap(fn: Function, ...args: any[]): Pipeable 28 | 29 | Adds a side-effect function to the pipe without altering the value. 30 | 31 | **Parameters**: 32 | 33 | - `fn`: Side-effect function to be applied. 34 | - `args`: Additional arguments to pass to the function. 35 | 36 | **Returns**: The modified `Pipeable` object for chaining. 37 | 38 | **Example**: 39 | 40 | ```typescript 41 | pipe.tap((x) => console.log(x)); 42 | ``` 43 | 44 | ## `exec` 45 | 46 | ### pipe.exec(): any 47 | 48 | Executes the pipe and returns the final value. 49 | 50 | **Returns**: The final value after applying all functions in the pipe. 51 | 52 | **Example**: 53 | 54 | ```typescript 55 | const result = pipe.exec(); 56 | ``` 57 | 58 | ## `catch` 59 | 60 | ### pipe.catch(fn: Function): Pipeable 61 | 62 | Adds an error handling function to the pipe. 63 | 64 | **Parameters**: 65 | 66 | - `fn`: Error handling function. 67 | 68 | **Returns**: The modified `Pipeable` object for chaining. 69 | 70 | **Example**: 71 | 72 | ```typescript 73 | pipe.catch((error) => console.error(error)); 74 | ``` 75 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/pipe-function.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Pipe Function 3 | description: Overview of the Pipe function. 4 | --- 5 | 6 | The Pipe function is the cornerstone of the Sloth Pipe library. It serves as the entry point for 7 | creating a new pipe, setting the stage for a series of transformations or operations to be performed 8 | on an initial value. By accepting an initial value as its parameter, it instantiates a Pipeable 9 | object. This object then becomes the conduit through which data flows and transforms, leveraging the 10 | library's functional programming capabilities. 11 | 12 | ### Pipe\(initialValue: T): Pipeable\ 13 | 14 | Creates a new pipe with an initial value. 15 | 16 | **Parameters**: 17 | 18 | - `initialValue`: The starting value for the pipe. 19 | 20 | **Returns**: A `Pipeable` object representing the pipe. 21 | 22 | **Example**: 23 | 24 | ```typescript 25 | const pipe = Pipe(10); 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /docs/src/styles/custom.css: -------------------------------------------------------------------------------- 1 | /* Dark mode colors. */ 2 | :root { 3 | --sl-color-accent-low: #072d00; 4 | --sl-color-accent: #247f00; 5 | --sl-color-accent-high: #aad7a0; 6 | --sl-color-white: #ffffff; 7 | --sl-color-gray-1: #eaf0e8; 8 | --sl-color-gray-2: #bdc4bb; 9 | --sl-color-gray-3: #82907f; 10 | --sl-color-gray-4: #4f5c4d; 11 | --sl-color-gray-5: #303c2d; 12 | --sl-color-gray-6: #1f2a1c; 13 | --sl-color-black: #151a13; 14 | } 15 | /* Light mode colors. */ 16 | :root[data-theme="light"] { 17 | --sl-color-accent-low: #c0e2b8; 18 | --sl-color-accent: #258100; 19 | --sl-color-accent-high: #0d3e00; 20 | --sl-color-white: #151a13; 21 | --sl-color-gray-1: #1f2a1c; 22 | --sl-color-gray-2: #303c2d; 23 | --sl-color-gray-3: #4f5c4d; 24 | --sl-color-gray-4: #82907f; 25 | --sl-color-gray-5: #bdc4bb; 26 | --sl-color-gray-6: #eaf0e8; 27 | --sl-color-gray-7: #f4f7f3; 28 | --sl-color-black: #ffffff; 29 | } 30 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict" 3 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sloth-pipe-root", 3 | "private": true, 4 | "description": "", 5 | "version": "0.0.6", 6 | "scripts": { 7 | "docs": "cd docs && bun dev" 8 | }, 9 | "workspaces": [ 10 | "benchmarks", 11 | "docs", 12 | "src" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/api-docs.md: -------------------------------------------------------------------------------- 1 | # Sloth Pipe API Documentation 2 | 3 | Sloth Pipe is a TS/JS library for building lazy, chainable, and efficient data processing pipes. This 4 | document provides detailed information about the API, including method signatures, return types, and 5 | usage examples. 6 | 7 | ## Table of Contents 8 | 9 | - [Pipe Function](#pipe-function) 10 | - [Methods](#methods) 11 | - [to](#to) 12 | - [tap](#tap) 13 | - [exec](#exec) 14 | - [catch](#catch) 15 | - [Type Definitions](#type-definitions) 16 | - [Error Handling](#error-handling) 17 | - [Async/Await Support](#asyncawait-support) 18 | 19 | ## Pipe Function 20 | 21 | ### Pipe\(initialValue: T): Pipeable\ 22 | 23 | Creates a new pipe with an initial value. 24 | 25 | **Parameters**: 26 | 27 | - `initialValue`: The starting value for the pipe. 28 | 29 | **Returns**: A `Pipeable` object representing the pipe. 30 | 31 | **Example**: 32 | 33 | ```typescript 34 | const pipe = Pipe(10); 35 | ``` 36 | 37 | ## Methods 38 | 39 | ### to 40 | 41 | #### pipe.to(fn: Function, ...args: any[]): Pipeable 42 | 43 | Adds a transformation function to the pipe. 44 | 45 | **Parameters**: 46 | 47 | - `fn`: Function to be applied to the current value. 48 | - `args`: Additional arguments to pass to the function. 49 | 50 | **Returns**: The modified `Pipeable` object for chaining. 51 | 52 | **Example**: 53 | 54 | ```typescript 55 | pipe.to((x) => x * 2); 56 | ``` 57 | 58 | ### tap 59 | 60 | #### pipe.tap(fn: Function, ...args: any[]): Pipeable 61 | 62 | Adds a side-effect function to the pipe without altering the value. 63 | 64 | **Parameters**: 65 | 66 | - `fn`: Side-effect function to be applied. 67 | - `args`: Additional arguments to pass to the function. 68 | 69 | **Returns**: The modified `Pipeable` object for chaining. 70 | 71 | **Example**: 72 | 73 | ```typescript 74 | pipe.tap((x) => console.log(x)); 75 | ``` 76 | 77 | ### exec 78 | 79 | #### pipe.exec(): any 80 | 81 | Executes the pipe and returns the final value. 82 | 83 | **Returns**: The final value after applying all functions in the pipe. 84 | 85 | **Example**: 86 | 87 | ```typescript 88 | const result = pipe.exec(); 89 | ``` 90 | 91 | ### catch 92 | 93 | #### pipe.catch(fn: Function): Pipeable 94 | 95 | Adds an error handling function to the pipe. 96 | 97 | **Parameters**: 98 | 99 | - `fn`: Error handling function. 100 | 101 | **Returns**: The modified `Pipeable` object for chaining. 102 | 103 | **Example**: 104 | 105 | ```typescript 106 | pipe.catch((error) => console.error(error)); 107 | ``` 108 | 109 | ## Type Definitions 110 | 111 | ### Pipeable\ 112 | 113 | The `Pipeable` type is the core of Sloth Pipe's chainable interface. It allows the chaining of 114 | `to`, `tap`, `catch`, and `exec` methods. 115 | 116 | ### Function Signatures 117 | 118 | #### to 119 | 120 | - **Signature**: `(fn: (x: T) => U): Pipe` 121 | - **Description**: Transforms the current value in the pipe using the provided function. 122 | - **Returns**: The same `Pipeable` object for further chaining. 123 | 124 | #### tap 125 | 126 | - **Signature**: `(fn: (x: T, ...args: any[]) => void): Pipe` 127 | - **Description**: Executes a side-effect function using the current value in the pipe without 128 | modifying it. 129 | - **Returns**: The same `Pipeable` object for further chaining. 130 | 131 | #### catch 132 | 133 | - **Signature**: `(fn: (err: unknown) => V) => Pipe` 134 | - **Description**: Adds error handling to the pipeline. The function is called if an error occurs 135 | in any preceding `to` or `tap` function. 136 | - **Returns**: The same `Pipeable` object for further chaining. 137 | 138 | #### exec 139 | 140 | - **Signature**: `(): T` 141 | - **Description**: Executes the pipeline and returns the final value. 142 | - **Returns**: The final value after applying all functions in the pipeline and handling any 143 | errors. 144 | 145 | ## Error Handling 146 | 147 | Sloth Pipe allows handling errors that may occur during function execution in the pipeline. When an 148 | error is thrown in a `to` or `tap` method, the `catch` method is invoked if it is defined. This does 149 | not stop the pipeline from executing, but it does allow for graceful error handling. 150 | 151 | **Example**: 152 | 153 | ```typescript 154 | const result = Pipe(5) 155 | .to((x) => { 156 | throw new Error("Example Error"); 157 | }) 158 | .catch((err) => "Error occurred") 159 | .exec(); 160 | 161 | console.log(result); // Outputs: "Error occurred" 162 | ``` 163 | 164 | ## Async/Await Support 165 | 166 | Sloth Pipe supports asynchronous operations within the pipeline. Functions passed to `to` or `tap` 167 | can be asynchronous, and `exec` will return a Promise if any function in the pipeline is 168 | asynchronous. 169 | 170 | **Async Example**: 171 | 172 | ```typescript 173 | const asyncResult = await Pipe(5) 174 | .to(async (x) => x * 2) 175 | .to(async (x) => x + 3) 176 | .exec(); 177 | 178 | console.log(asyncResult); // Outputs: 13 179 | ``` 180 | 181 | **Note**: When using asynchronous functions, the `catch` method will be invoked just like with 182 | synchronous functions. However, the `catch` method must also be asynchronous in order to handle 183 | errors properly. 184 | -------------------------------------------------------------------------------- /src/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trvswgnr/sloth-pipe/4307751915b0632299e59b96430f4a054b01dc33/src/bun.lockb -------------------------------------------------------------------------------- /src/fifo-queue.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test, beforeEach } from "bun:test"; 2 | import Queue from "./fifo-queue"; 3 | 4 | describe("FIFO Queue", () => { 5 | let queue: Queue; 6 | 7 | beforeEach(() => { 8 | queue = new Queue(); 9 | }); 10 | 11 | test("should initialize an empty queue", () => { 12 | expect(queue.size()).toBe(0); 13 | expect(queue.empty()).toBe(true); 14 | }); 15 | 16 | test("should enqueue items to the queue", () => { 17 | queue.enqueue(1); 18 | expect(queue.size()).toBe(1); 19 | expect(queue.empty()).toBe(false); 20 | 21 | queue.enqueue(2); 22 | expect(queue.size()).toBe(2); 23 | }); 24 | 25 | test("should dequeue items from the queue", () => { 26 | queue.enqueue(1); 27 | queue.enqueue(2); 28 | 29 | expect(queue.dequeue()).toBe(1); 30 | expect(queue.size()).toBe(1); 31 | 32 | expect(queue.dequeue()).toBe(2); 33 | expect(queue.size()).toBe(0); 34 | expect(queue.empty()).toBe(true); 35 | }); 36 | 37 | test("should return undefined when dequeueing an empty queue", () => { 38 | expect(queue.dequeue()).toBeUndefined(); 39 | }); 40 | 41 | test("should peek the front item in the queue", () => { 42 | const queue1 = new Queue(); 43 | queue1.enqueue(1); 44 | queue1.enqueue(2); 45 | 46 | expect(queue1.peekFront()).toBe(1); 47 | expect(queue1.size()).toBe(2); 48 | 49 | const queue2 = new Queue(); 50 | queue2.enqueue(1); 51 | expect(queue2.peekFront()).toBe(1); 52 | 53 | const queue3 = new Queue(); 54 | expect(queue3.peekFront()).toBeUndefined(); 55 | }); 56 | 57 | test("should peek the back item in the queue", () => { 58 | const queue = new Queue(); 59 | queue.enqueue(1); 60 | queue.enqueue(2); 61 | queue.enqueue(3); 62 | 63 | expect(queue.peekBack()).toBe(3); 64 | expect(queue.size()).toBe(3); 65 | 66 | const queue2 = new Queue(); 67 | queue2.enqueue(1); 68 | expect(queue2.peekBack()).toBe(1); 69 | 70 | const queue3 = new Queue(); 71 | expect(queue3.peekBack()).toBeUndefined(); 72 | }); 73 | 74 | test("should peek front and back of the queue", () => { 75 | const queue = new Queue(); 76 | queue.enqueue(1); 77 | queue.enqueue(2); 78 | queue.enqueue(3); 79 | 80 | expect(queue.peekFront()).toBe(1); 81 | expect(queue.peekBack()).toBe(3); 82 | expect(queue.size()).toBe(3); 83 | 84 | const queue2 = new Queue(); 85 | queue2.enqueue(1); 86 | expect(queue2.peekFront()).toBe(1); 87 | expect(queue2.peekBack()).toBe(1); 88 | 89 | const queue3 = new Queue(); 90 | expect(queue3.peekFront()).toBeUndefined(); 91 | expect(queue3.peekBack()).toBeUndefined(); 92 | }); 93 | 94 | test("should handle large data sets", () => { 95 | const num = 100_000; 96 | for (let i = 0; i < num; i++) { 97 | queue.enqueue(i); 98 | } 99 | 100 | expect(queue.size()).toBe(num); 101 | 102 | for (let i = 0; i < num; i++) { 103 | expect(queue.dequeue()).toBe(i); 104 | } 105 | 106 | expect(queue.empty()).toBe(true); 107 | }); 108 | 109 | test("should handle different types of data", () => { 110 | queue = new Queue(); 111 | const obj = { a: 1, b: 2 }; 112 | const str = "test"; 113 | const undef = undefined; 114 | const nil = null; 115 | 116 | queue.enqueue(obj); 117 | queue.enqueue(str); 118 | queue.enqueue(undef); 119 | queue.enqueue(nil); 120 | 121 | expect(queue.dequeue()).toBe(obj); 122 | expect(queue.dequeue()).toBe(str); 123 | expect(queue.dequeue()).toBe(undef); 124 | expect(queue.dequeue()).toBe(nil); 125 | expect(queue.dequeue()).toBe(undefined); 126 | }); 127 | 128 | test("should correctly iterate over the queue using for...of", () => { 129 | queue.enqueue(1); 130 | queue.enqueue(2); 131 | queue.enqueue(3); 132 | 133 | let i = 1; 134 | for (const item of queue) { 135 | expect(item).toBe(i++); 136 | } 137 | 138 | // After iterating, the queue should still be the same 139 | expect(queue.size()).toBe(3); 140 | expect(queue.empty()).toBe(false); 141 | 142 | const queue2 = new Queue(); 143 | queue2.enqueue(1); 144 | queue2.enqueue(2); 145 | queue2.enqueue(3); 146 | 147 | let i2 = 0; 148 | for (const item of queue2.drain()) { 149 | expect(item).toBe(++i2); 150 | } 151 | 152 | // After iterating, the queue should be empty 153 | expect(queue2.size()).toBe(0); 154 | expect(queue2.empty()).toBe(true); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /src/fifo-queue.ts: -------------------------------------------------------------------------------- 1 | export default class FifoQueue implements Queue { 2 | constructor(private queue: (T | undefined)[] = [], private front: number = 0) {} 3 | 4 | enqueue(item: T): void { 5 | this.queue.push(item); 6 | } 7 | 8 | dequeue(): T | undefined { 9 | if (this.empty()) return undefined; 10 | const item = this.queue[this.front]; 11 | this.queue[this.front] = undefined; // allow garbage collection 12 | this.front++; 13 | return item; 14 | } 15 | 16 | empty(): boolean { 17 | return this.front >= this.queue.length; 18 | } 19 | 20 | size(): number { 21 | return this.queue.length - this.front; 22 | } 23 | 24 | get length(): number { 25 | return this.size(); 26 | } 27 | 28 | peekFront(): T | undefined { 29 | return this.queue[this.front]; 30 | } 31 | 32 | peekBack(): T | undefined { 33 | return this.queue[this.queue.length - 1]; 34 | } 35 | 36 | *drain(): IterableIterator { 37 | while (!this.empty()) { 38 | yield this.dequeue()!; 39 | } 40 | } 41 | 42 | *[Symbol.iterator](): Iterator { 43 | for (let i = this.front; i < this.queue.length; i++) { 44 | yield this.queue[i]!; 45 | } 46 | } 47 | } 48 | 49 | interface Queue { 50 | /** adds an item to the queue */ 51 | enqueue(item: T): void; 52 | /** removes an item from the queue */ 53 | dequeue(): T | undefined; 54 | /** returns the size of the queue */ 55 | size(): number; 56 | /** returns the size of the queue */ 57 | length: number; 58 | /** checks if the queue is empty */ 59 | empty(): boolean; 60 | /** gets the last item in the queue, without removing it */ 61 | peekBack(): T | undefined; 62 | /** gets the first item in the queue, without removing it */ 63 | peekFront(): T | undefined; 64 | /** returns an iterator over the queue, consuming the queue with each iteration */ 65 | drain(): IterableIterator; 66 | /** returns an iterator over the queue without consuming it */ 67 | [Symbol.iterator](): Iterator; 68 | } 69 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sloth-pipe", 3 | "description": "A pipe utility that lazily evaluates only when needed, and only once.", 4 | "version": "0.0.6", 5 | "type": "module", 6 | "main": "dist/index.js", 7 | "exports": { 8 | ".": { 9 | "import": "./dist/index.js", 10 | "require": "./dist/index.cjs" 11 | }, 12 | "./fifo-queue": { 13 | "import": "./dist/fifo-queue.js", 14 | "require": "./dist/fifo-queue.cjs" 15 | } 16 | }, 17 | "scripts": { 18 | "build": "tsup", 19 | "lint": "tsc", 20 | "bench": "bun ./pipe.bench.ts" 21 | }, 22 | "devDependencies": { 23 | "bun-types": "1.0.20", 24 | "mitata": "^0.1.6", 25 | "tsup": "^8.0.1", 26 | "typescript": "5.3.3" 27 | }, 28 | "files": [ 29 | "dist" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/pipe.bench.ts: -------------------------------------------------------------------------------- 1 | import { run, bench, group, baseline, type Report } from "mitata"; 2 | import { Pipe } from "./pipe"; 3 | import { unlink } from "fs/promises"; 4 | import { Worker, isMainThread, parentPort } from "worker_threads"; 5 | 6 | const oldPipePath = "./_old-pipe.ts"; 7 | const __filename = new URL(import.meta.url).pathname; 8 | 9 | if (isMainThread) { 10 | const worker = new Worker(__filename); 11 | 12 | worker.on("message", (exitCode: number) => { 13 | cleanup().then(() => { 14 | console.log("Goodbye!"); 15 | process.exit(exitCode); 16 | }); 17 | }); 18 | 19 | worker.on("error", (error: Error) => { 20 | console.error("Worker error:", error); 21 | cleanup().then(() => { 22 | console.log("Goodbye!"); 23 | process.exit(1); 24 | }); 25 | }); 26 | 27 | worker.on("exit", (code: number) => { 28 | if (code !== 0) { 29 | console.error(`Worker stopped with exit code ${code}`); 30 | cleanup().then(() => { 31 | console.log("Goodbye!"); 32 | process.exit(code); 33 | }); 34 | } 35 | }); 36 | 37 | process.on("SIGINT", async () => { 38 | console.log("\nSIGINT received"); 39 | await cleanup().then(() => { 40 | console.log("Goodbye!"); 41 | process.exit(130); 42 | }); 43 | }); 44 | 45 | process.on("SIGTERM", async () => { 46 | console.log("\nSIGTERM received"); 47 | await cleanup().then(() => { 48 | console.log("Goodbye!"); 49 | process.exit(143); 50 | }); 51 | }); 52 | } else { 53 | const result = await main(); 54 | parentPort?.postMessage(result); 55 | } 56 | 57 | async function main() { 58 | const OldPipe = await getOldPipe(oldPipePath); 59 | 60 | const exampleFn1 = (x: number) => x + 1; 61 | const exampleFn2 = (x: number) => x + 2; 62 | 63 | group("compare old and new Pipe with sync functions", () => { 64 | baseline("new Pipe", () => { 65 | Pipe(0).to(exampleFn1).to(exampleFn2).exec(); 66 | }); 67 | bench("old Pipe", () => { 68 | OldPipe(0).to(exampleFn1).to(exampleFn2).exec(); 69 | }); 70 | }); 71 | 72 | group("compare to native promise with async functions", () => { 73 | baseline("new Pipe", async () => { 74 | await Pipe(Promise.resolve(0)) 75 | .to(async (x) => (await x) + 1) 76 | .to(async (x) => (await x) + 1) 77 | .exec(); 78 | }); 79 | bench("old Pipe", async () => { 80 | await OldPipe(Promise.resolve(0)) 81 | .to(async (x) => (await x) + 1) 82 | .to(async (x) => (await x) + 1) 83 | .exec(); 84 | }); 85 | bench("Native Promise", async () => { 86 | await Promise.resolve(0) 87 | .then((x) => x + 1) 88 | .then((x) => x + 1); 89 | }); 90 | }); 91 | 92 | if (process.argv.includes("--micro")) { 93 | // some extra micro benchmarks: 94 | printMicroDiff( 95 | microBench("New Pipe", 10, () => Pipe(0).to(exampleFn1).to(exampleFn2)), 96 | microBench("Old Pipe", 10, () => OldPipe(0).to(exampleFn1).to(exampleFn2).exec()), 97 | ); 98 | 99 | printMicroDiff( 100 | microBench("New Pipe", 1000000, () => Pipe(0).to(exampleFn1).to(exampleFn2)), 101 | microBench("Old Pipe", 1000000, () => OldPipe(0).to(exampleFn1).to(exampleFn2).exec()), 102 | ); 103 | 104 | printTimeDiff( 105 | timeBench("New Pipe", 1, () => Pipe(0).to(exampleFn1).to(exampleFn2)), 106 | timeBench("Old Pipe", 1, () => OldPipe(0).to(exampleFn1).to(exampleFn2).exec()), 107 | ); 108 | 109 | printTimeDiff( 110 | timeBench("New Pipe", 5, () => Pipe(0).to(exampleFn1).to(exampleFn2)), 111 | timeBench("Old Pipe", 5, () => OldPipe(0).to(exampleFn1).to(exampleFn2).exec()), 112 | ); 113 | 114 | console.log(); 115 | } 116 | 117 | const options = {}; 118 | 119 | return await run(options) 120 | .then(checkIsSlower) 121 | .catch((error) => { 122 | console.error(error); 123 | return 1; 124 | }); 125 | } 126 | 127 | function checkIsSlower(report: Report, marginOfError = 0.2) { 128 | console.log("\nChecking if new Pipe is slower than old Pipe..."); 129 | const groups: any = {}; 130 | report.benchmarks.forEach((b) => { 131 | const key = b.group; 132 | if (!key) return; 133 | groups[key] ??= []; 134 | if (b.name === "Native Promise") return; 135 | groups[key].push({ name: b.name, avg: b.stats?.avg }); 136 | }); 137 | 138 | let isSlower = false; 139 | Object.entries(groups).forEach(([group, _benchmarks]) => { 140 | const benchmarks = _benchmarks as { name: string; avg: number }[]; 141 | const newPipeBenchmark = benchmarks.find((b) => b.name === "new Pipe"); 142 | const oldPipeBenchmark = benchmarks.find((b) => b.name === "old Pipe"); 143 | 144 | if (!newPipeBenchmark || !oldPipeBenchmark) { 145 | console.warn(`Missing benchmarks for comparison in group ${group}`); 146 | return; 147 | } 148 | 149 | const isSignificantlySlower = 150 | newPipeBenchmark.avg > oldPipeBenchmark.avg * (1 + marginOfError); 151 | if (isSignificantlySlower) { 152 | process.stderr.write( 153 | `❌ new Pipe is more than ${ 154 | marginOfError * 100 155 | }% slower than old Pipe in "${group}"\n`, 156 | ); 157 | isSlower = true; 158 | } 159 | }); 160 | 161 | if (!isSlower) { 162 | console.log("✅ new Pipe is faster or not significantly slower than old Pipe"); 163 | } 164 | 165 | return Number(isSlower); 166 | } 167 | 168 | async function getOldPipe(filepath: string) { 169 | const child = Bun.spawn(["git", "show", "origin/main:src/pipe.ts"], { stdout: "pipe" }); 170 | const code = await child.exited; 171 | if (code !== 0) throw new Error("could not get old pipe"); 172 | const content: string = await Bun.readableStreamToText(child.stdout); 173 | await Bun.write(filepath, content); 174 | const { Pipe: OldPipe } = (await import(filepath)) as { Pipe: typeof Pipe }; 175 | return OldPipe; 176 | } 177 | 178 | async function cleanup() { 179 | console.log("\nCleaning up..."); 180 | await unlink(oldPipePath).catch(() => {}); 181 | } 182 | 183 | // run micro benchmark 184 | function microBench(name: string, runs: number, fn: () => void) { 185 | const start = Bun.nanoseconds(); 186 | for (let i = 0; i < runs; i++) { 187 | fn(); 188 | } 189 | const end = Bun.nanoseconds(); 190 | const total = end - start; 191 | const avg = total / runs; 192 | return { name, avg, total, runs }; 193 | } 194 | 195 | type MicroBench = ReturnType; 196 | 197 | function printMicroDiff(a: MicroBench, b: MicroBench) { 198 | console.log(); 199 | console.log( 200 | `${a.name}: ${yellow(convertNanos(a.avg))}/iter over ${yellow(locale(a.runs))} runs`, 201 | ); 202 | console.log( 203 | `${b.name}: ${yellow(convertNanos(b.avg))}/iter over ${yellow(locale(b.runs))} runs`, 204 | ); 205 | if (a.avg < b.avg) { 206 | const times = (b.avg / a.avg).toLocaleString("en-US", { maximumFractionDigits: 2 }); 207 | console.log( 208 | `${cyan(a.name)} is ${green(times)}x faster than ${cyan(b.name)} over ${yellow( 209 | locale(a.runs), 210 | )} runs`, 211 | ); 212 | } else if (a.avg > b.avg) { 213 | const times = (a.avg / b.avg).toLocaleString("en-US", { maximumFractionDigits: 2 }); 214 | console.log( 215 | `${cyan(a.name)} is ${red(times)}x slower than ${cyan(a.name)} over ${yellow( 216 | locale(a.runs), 217 | )} runs`, 218 | ); 219 | } else { 220 | console.log( 221 | `${cyan(a.name)} and ${cyan(b.name)} are the same speed over ${yellow( 222 | locale(a.runs), 223 | )} runs`, 224 | ); 225 | } 226 | } 227 | 228 | // time-based benchmarks (how many runs can be done in n seconds) 229 | function timeBench(name: string, seconds: number, fn: () => void) { 230 | const start = Bun.nanoseconds(); 231 | let runs = 0; 232 | while (Bun.nanoseconds() - start < seconds * 1e9) { 233 | fn(); 234 | runs++; 235 | } 236 | return { name, runs, seconds }; 237 | } 238 | type TimeBench = ReturnType; 239 | 240 | function printTimeDiff(a: TimeBench, b: TimeBench) { 241 | console.log(); 242 | console.log( 243 | `${a.name}: ${yellow(locale(a.runs))} runs in ${yellow(locale(a.seconds))} second${ 244 | a.seconds === 1 ? "" : "s" 245 | }`, 246 | ); 247 | console.log( 248 | `${b.name}: ${yellow(locale(b.runs))} runs in ${yellow(locale(b.seconds))} second${ 249 | b.seconds === 1 ? "" : "s" 250 | }`, 251 | ); 252 | if (a.runs > b.runs) { 253 | const times = locale(a.runs / b.runs); 254 | console.log( 255 | `${cyan(a.name)} is ${green(times)}x faster than ${cyan(b.name)} running for ${yellow( 256 | locale(a.seconds), 257 | )} second${a.seconds === 1 ? "" : "s"}`, 258 | ); 259 | } else if (a.runs < b.runs) { 260 | const times = locale(b.runs / a.runs); 261 | console.log( 262 | `${cyan(a.name)} is ${red(times)}x slower than ${cyan(b.name)} running for ${yellow( 263 | locale(a.seconds), 264 | )} second${a.seconds === 1 ? "" : "s"}`, 265 | ); 266 | } else { 267 | console.log( 268 | `${cyan(a.name)} and ${cyan(b.name)} are the same speed running for ${yellow( 269 | locale(a.seconds), 270 | )} second${a.seconds === 1 ? "" : "s"}`, 271 | ); 272 | } 273 | } 274 | 275 | function locale(num: number) { 276 | return num.toLocaleString("en-US", { maximumFractionDigits: 2 }); 277 | } 278 | 279 | /** converts nanoseconds to the largest possible unit */ 280 | function convertNanos(nanos: number): string { 281 | const units = [ 282 | { unit: "μs", factor: 1000 }, 283 | { unit: "ms", factor: 1000000 }, 284 | { unit: "s", factor: 1000000000 }, 285 | ]; 286 | let _unit = "ns"; 287 | let _value = nanos; 288 | for (const { unit, factor } of units) { 289 | if (nanos < factor) break; 290 | _unit = unit; 291 | _value = nanos / factor; 292 | } 293 | return `${_value.toLocaleString("en-US", { maximumFractionDigits: 2 })} ${_unit}`; 294 | } 295 | 296 | function green(str: string) { 297 | return `\x1b[32m${str}\x1b[0m`; 298 | } 299 | 300 | function red(str: string) { 301 | return `\x1b[31m${str}\x1b[0m`; 302 | } 303 | 304 | function cyan(str: string) { 305 | return `\x1b[36m${str}\x1b[0m`; 306 | } 307 | 308 | function yellow(str: string) { 309 | return `\x1b[33m${str}\x1b[0m`; 310 | } 311 | -------------------------------------------------------------------------------- /src/pipe.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, mock } from "bun:test"; 2 | import { Pipe } from "./pipe"; 3 | 4 | describe("Pipe", () => { 5 | it("should create a new Pipe with initial value", () => { 6 | const pipe = Pipe(5); 7 | expect(pipe.exec()).toEqual(5); 8 | }); 9 | 10 | it("should lazily evaluate values", () => { 11 | const fn = mock((x) => x); 12 | const pipe = Pipe(5) 13 | .to(fn) 14 | .to((x) => x + 3); 15 | expect(fn).not.toHaveBeenCalled(); 16 | expect(pipe.exec()).toEqual(8); 17 | expect(fn).toHaveBeenCalled(); 18 | }); 19 | 20 | it("should lazily evaluate values with tap", () => { 21 | const fn = mock((x: number) => x); 22 | const fn2 = mock((x: number) => x); 23 | const pipe = Pipe(5) 24 | .to(fn) 25 | .tap(fn2) 26 | .to((x) => x + 3); 27 | expect(fn).not.toHaveBeenCalled(); 28 | expect(fn2).not.toHaveBeenCalled(); 29 | expect(pipe.exec()).toEqual(8); 30 | expect(fn).toHaveBeenCalled(); 31 | expect(fn2).toHaveBeenCalled(); 32 | }); 33 | 34 | it("should only evaluate values once", () => { 35 | const fn = mock((x) => x + 1); 36 | const fn2 = mock((x) => x + 1); 37 | const pipe = Pipe(5) 38 | .to(fn) 39 | .to((x) => x + 3); 40 | expect(fn).not.toHaveBeenCalled(); 41 | expect(pipe.exec()).toEqual(9); 42 | expect(fn).toHaveBeenCalledTimes(1); 43 | expect(pipe.exec()).toEqual(9); 44 | expect(fn).toHaveBeenCalledTimes(1); 45 | pipe.to(fn2); 46 | expect(pipe.exec()).toEqual(10); 47 | expect(fn).toHaveBeenCalledTimes(1); 48 | expect(fn2).toHaveBeenCalledTimes(1); 49 | }); 50 | 51 | describe("to", () => { 52 | it("should add a function to the pipe and change the value", () => { 53 | const pipe = Pipe(5).to((x) => x * 2); 54 | expect(pipe.exec()).toBe(10); 55 | }); 56 | 57 | it("should chain multiple functions", () => { 58 | const pipe = Pipe(5) 59 | .to((x) => x * 2) 60 | .to((x) => x + 3); 61 | expect(pipe.exec()).toBe(13); 62 | }); 63 | }); 64 | 65 | describe("tap", () => { 66 | it("should add a function to the pipe without changing the value", () => { 67 | const pipe = Pipe(5).tap((x) => x * 2); 68 | expect(pipe.exec()).toBe(5); 69 | }); 70 | 71 | it("should chain with to function", () => { 72 | const pipe = Pipe(5) 73 | .to((x) => x * 2) 74 | .tap((x) => x + 3); 75 | expect(pipe.exec()).toBe(10); 76 | }); 77 | 78 | it("should create side effects without changing the pipe value", () => { 79 | let order: Number[] = []; 80 | const pipe = Pipe(5) 81 | .tap((x) => { 82 | order.push(1); 83 | return x - 1; 84 | }) 85 | .tap((x) => { 86 | order.push(2); 87 | return x - 1; 88 | }); 89 | expect(pipe.exec()).toBe(5); 90 | expect(order).toEqual([1, 2]); 91 | }); 92 | 93 | it("should return the correct pipe value after catching an error in tap", () => { 94 | const pipe = Pipe(5) 95 | .to((x) => x * 2) 96 | .tap((x) => { 97 | if (x === 10) throw new Error("Error"); 98 | }) 99 | .catch(() => 1); 100 | expect(pipe.exec()).toBe(10); 101 | }); 102 | }); 103 | 104 | describe("exec", () => { 105 | it("should execute all functions in the pipe and return the final value", () => { 106 | const pipe = Pipe(5) 107 | .to((x) => x * 2) 108 | .tap((x) => x + 3) 109 | .to((x) => x - 1); 110 | expect(pipe.exec()).toBe(9); 111 | }); 112 | }); 113 | 114 | describe("catch", () => { 115 | it("should catch errors in the pipe and return the final value", () => { 116 | let tapped = false; 117 | const pipe = Pipe(5) 118 | .to((x) => x * 2) 119 | .tap((x) => { 120 | tapped = true; 121 | }) 122 | .to((x) => x - 1) 123 | .exec(); 124 | expect(tapped).toBe(true); 125 | expect(pipe).toBe(9); 126 | }); 127 | 128 | it("should catch errors in the pipe and return the catch value", () => { 129 | let bad = false; 130 | const pipe = Pipe(5) 131 | .to((x) => x * 2) 132 | .catch(() => 0) 133 | .to((x) => { 134 | if (x === 10) throw new Error("simulated error"); 135 | bad = true; 136 | return x - 1; 137 | }) 138 | .catch(() => 1); 139 | expect(bad).toBe(false); 140 | expect(pipe.exec()).toBe(1); 141 | }); 142 | 143 | it("should throw an error if catch is not used", () => { 144 | const pipe = Pipe(5) 145 | .to((x) => x * 2) 146 | .tap((x) => { 147 | if (x === 10) throw new Error("simulated error"); 148 | }); 149 | expect(() => pipe.exec()).toThrow("simulated error"); 150 | }); 151 | }); 152 | 153 | describe("built-in methods", () => { 154 | it("should correctly convert to JSON", () => { 155 | const pipe = Pipe(5).to((x) => x * 2); 156 | expect(JSON.stringify(pipe)).toBe("10"); 157 | const pipe2 = Pipe({ a: 1 }).to((x) => x); 158 | expect(JSON.stringify(pipe2)).toBe('{"a":1}'); 159 | }); 160 | it("should correctly convert to number", () => { 161 | const pipe = Pipe(5).to((x) => x * 2); 162 | expect(0 + pipe).toBe(10); 163 | const pipe2 = Pipe({ a: 1 }).to((x) => x); 164 | expect(+pipe2).toBeNaN(); 165 | }); 166 | it("should correctly convert to string", () => { 167 | const pipe = Pipe(5).to((x) => x * 2); 168 | expect(`${pipe}`).toBe("10"); 169 | const pipe2 = Pipe({ a: 1 }).to((x) => x); 170 | expect(`${pipe2}`).toBe("[object Object]"); 171 | }); 172 | it("should correctly convert to boolean", () => { 173 | const pipe = Pipe(5).to((x) => x * 2); 174 | expect(!pipe).toBe(false); 175 | const pipe2 = Pipe({ a: 1 }).to((x) => x); 176 | expect(!!pipe2).toBe(true); 177 | }); 178 | it("should correctly convert to bigint", () => { 179 | const pipe = Pipe(5).to((x) => x * 2); 180 | expect(BigInt(pipe)).toBe(BigInt(10)); 181 | const pipe2 = Pipe({ a: 1 }).to((x) => x); 182 | // @ts-expect-error 183 | expect(() => BigInt(pipe2)).toThrow(); 184 | }); 185 | it("should correctly convert to primitive", () => { 186 | const pipe = Pipe(5).to((x) => x * 2); 187 | expect(+pipe).toBe(10); 188 | expect(`${pipe}`).toBe("10"); 189 | const pipe2 = Pipe({ a: 1 }).to((x) => x); 190 | expect(`${pipe2}`).toBe("[object Object]"); 191 | }); 192 | it("should correctly convert to object", () => { 193 | const pipe = Pipe(5).to((x) => x * 2); 194 | // @ts-expect-error - "Spread types may only be created from object types" 195 | expect({ ...pipe }).toMatchObject({ value: 10 }); 196 | const pipe2 = Pipe({ a: 1 }).to((x) => x); 197 | expect({ ...pipe2 }).toMatchObject({ value: { a: 1 } }); 198 | const pipe3 = Pipe([1]).to((x) => x); 199 | expect([...pipe3, 2]).toEqual([1, 2]); 200 | }); 201 | it("should allow inspecting in node-like environments", () => { 202 | const pipe = Pipe(5).to((x) => x * 2); 203 | expect((pipe as any)[Symbol.for("nodejs.util.inspect.custom")]()).toBe("Pipe(10)"); 204 | }); 205 | }); 206 | 207 | describe("async", () => { 208 | it("should correctly handle async functions", async () => { 209 | const pipe = Pipe(5) 210 | .to(async (x) => x * 2) 211 | .to(async (x) => (await x) + 3); 212 | expect(await pipe.exec()).toBe(13); 213 | }); 214 | 215 | it("should correctly handle async functions with tap", async () => { 216 | const fn1 = mock((x) => x); 217 | const fn2 = mock(async (x) => (await x) - 1); 218 | const val = await Pipe(5) 219 | .to(async (x) => x * 2) 220 | .tap(async (x) => { 221 | fn1((await x) + 3); 222 | }) 223 | .to(fn2) 224 | .exec(); 225 | expect(fn1).toHaveBeenCalledWith(13); 226 | expect(fn2).toHaveBeenCalledWith(Promise.resolve(10)); 227 | expect(val).toBe(9); 228 | }); 229 | 230 | it("should correctly handle async functions with catch", async () => { 231 | let bad = false; 232 | const val = await Pipe(5) 233 | .to(async (x) => x * 2) 234 | .to(async (x) => { 235 | if ((await x) === 10) throw new Error("simulated error"); 236 | bad = true; 237 | return (await x) - 1; 238 | }) 239 | .catch(async () => 70) 240 | .to(async (x) => (await x) - 1) 241 | .exec(); 242 | expect(val).toBe(69); 243 | }); 244 | 245 | it("should correctly handle async functions with catch and tap", async () => { 246 | const bad = mock(async (x) => x); 247 | const fn1 = mock(async (x) => x); 248 | const fn2 = mock(async (x) => (await x) - 1); 249 | const val = await Pipe(5) 250 | .to(async (x) => x * 2) 251 | .tap(async (x) => { 252 | if ((await x) === 10) throw new Error("simulated error"); 253 | bad(true); 254 | }) 255 | .catch(fn1) 256 | .to(fn2) 257 | .exec(); 258 | expect(bad).not.toHaveBeenCalled(); 259 | expect(fn1).toHaveBeenCalledWith(new Error("simulated error")); 260 | expect(fn2).toHaveBeenCalledWith(expect.resolvesTo.closeTo(10, 0)); 261 | expect(val).toBe(9); 262 | }); 263 | }); 264 | 265 | it("should be reusable", () => { 266 | const pipe = Pipe(5) 267 | .to((x) => x * 2) 268 | .to((x) => x + 3); 269 | expect(pipe.exec()).toBe(13); 270 | expect(pipe.exec()).toBe(13); 271 | expect(pipe.exec()).toBe(13); 272 | pipe.to((x) => x - 1); 273 | expect(pipe.exec()).toBe(12); 274 | expect(pipe.exec()).toBe(12); 275 | const fn1 = mock((x) => x); 276 | const fn2 = mock((x) => x); 277 | const fn3 = mock((x) => x); 278 | pipe.to(fn1).to(fn2).to(fn3); 279 | expect(fn1).not.toHaveBeenCalled(); 280 | expect(fn2).not.toHaveBeenCalled(); 281 | expect(fn3).not.toHaveBeenCalled(); 282 | expect(pipe.exec()).toBe(12); 283 | expect(pipe.exec()).toBe(12); 284 | expect(pipe.exec()).toBe(12); 285 | expect(fn1).toHaveBeenCalledTimes(1); 286 | expect(fn2).toHaveBeenCalledTimes(1); 287 | expect(fn3).toHaveBeenCalledTimes(1); 288 | expect(pipe.exec()).toBe(12); 289 | expect(fn1).toHaveBeenCalledWith(12); 290 | expect(fn2).toHaveBeenCalledWith(12); 291 | expect(fn3).toHaveBeenCalledWith(12); 292 | }); 293 | 294 | it("should be practical", () => { 295 | const multipleArgs = (x: string, y: number) => x.repeat(y); 296 | const multipleArgs2 = (x: string, y: number, z: string) => x.repeat(y) + z; 297 | const pipe1 = Pipe("hello") 298 | .to((x) => x.toUpperCase()) 299 | .to((x) => x.split("")) 300 | .to((x) => x.reverse()) 301 | .to((x) => x.join("")) 302 | .to((x) => x + "!") 303 | .to((x) => x.repeat(3)) 304 | .to((x) => x.split("!")) 305 | .to((x) => x.join(" ")) 306 | .to((x) => x.trim()) 307 | .to(multipleArgs, 2) 308 | .to(multipleArgs2, 2, "!") 309 | .exec(); 310 | expect(pipe1).toBe("OLLEH OLLEH OLLEHOLLEH OLLEH OLLEHOLLEH OLLEH OLLEHOLLEH OLLEH OLLEH!"); 311 | 312 | const fn1 = mock((x) => x); 313 | const fn2 = mock((x) => x); 314 | const fn3 = mock((x) => x); 315 | const pipe2 = Pipe(69) 316 | .to(String) 317 | .to((x) => x.split("")) 318 | .tap(fn1) 319 | .to((x) => x.map((y) => +y)) 320 | .to((x) => x.reduce((a, b) => a + b)) 321 | .tap(fn2) 322 | .to((x) => x ** 2) 323 | .tap(fn3) 324 | .to((x) => x.toString(16)) 325 | .to((x) => x.toUpperCase()) 326 | .to((x) => x.padStart(4, "0")) 327 | .exec(); 328 | expect(fn1).toHaveBeenCalledWith(["6", "9"]); 329 | expect(fn2).toHaveBeenCalledWith(15); 330 | expect(fn3).toHaveBeenCalledWith(225); 331 | expect(pipe2).toBe("00E1"); 332 | }); 333 | 334 | it("should not regress performance", async () => { 335 | const { exited } = Bun.spawn(["bun", "./pipe.bench.ts"], { stdout: "pipe" }); 336 | const code = await exited; 337 | expect(code).toBe(0); 338 | }, 20000); 339 | }); 340 | -------------------------------------------------------------------------------- /src/pipe.ts: -------------------------------------------------------------------------------- 1 | import Queue from "./fifo-queue"; 2 | 3 | const NODE_INSPECT = Symbol.for("nodejs.util.inspect.custom"); 4 | 5 | export const Pipe = (value: T): Pipeable => { 6 | let hasTap = false; 7 | let hasCatch = false; 8 | const queue = new Queue>(); 9 | const exec = (): unknown => { 10 | if (queue.empty()) return value; 11 | if (!hasTap && !hasCatch) { 12 | switch (queue.length) { 13 | case 0: 14 | return value; 15 | case 1: { 16 | const item = queue.dequeue()!; 17 | value = item.fn(value, ...item.args); 18 | return value; 19 | } 20 | case 2: { 21 | const item1 = queue.dequeue()!; 22 | const item2 = queue.dequeue()!; 23 | value = item2.fn(item1.fn(value, ...item1.args), ...item2.args); 24 | return value; 25 | } 26 | case 3: { 27 | const item1 = queue.dequeue()!; 28 | const item2 = queue.dequeue()!; 29 | const item3 = queue.dequeue()!; 30 | // prettier-ignore 31 | value = item3.fn(item2.fn(item1.fn(value, ...item1.args), ...item2.args), ...item3.args); 32 | return value; 33 | } 34 | default: 35 | break; 36 | } 37 | } 38 | for (const { fn, args, tap, catchFn } of queue.drain()) { 39 | if (tap) { 40 | if (catchFn) { 41 | tryCatch(fn, value, args, catchFn); 42 | continue; 43 | } 44 | fn(value, ...args); 45 | continue; 46 | } 47 | if (catchFn) { 48 | value = tryCatch(fn, value, args, catchFn); 49 | continue; 50 | } 51 | value = fn(value, ...args); 52 | } 53 | return value; 54 | }; 55 | const ret = { 56 | get value() { 57 | return exec(); 58 | }, 59 | to: (fn: (x?: any, ...args: any[]) => any, ...args: any[]) => { 60 | queue.enqueue({ fn, args, tap: false, catchFn: undefined }); 61 | return ret; 62 | }, 63 | tap: (fn: (x?: any, ...args: any[]) => any, ...args: any[]) => { 64 | hasTap = true; 65 | queue.enqueue({ fn, args, tap: true, catchFn: undefined }); 66 | return ret; 67 | }, 68 | catch: (fn: (err: unknown) => any) => { 69 | hasCatch = true; 70 | const item = queue.peekBack(); 71 | if (!item) throw new Error("cannot catch without a previous function"); 72 | item.catchFn = fn as any; 73 | return ret; 74 | }, 75 | exec, 76 | valueOf: exec, 77 | toJSON: exec, 78 | toString: () => String(exec()), 79 | [Symbol.toStringTag]: "Pipe", 80 | [Symbol.toPrimitive]: (hint: unknown) => { 81 | return hint === "string" ? String(exec()) : Number(exec()); 82 | }, 83 | [Symbol.iterator]: (): Iterator => { 84 | const val = exec(); 85 | if (typeof val === "object" && val !== null && Symbol.iterator in val) { 86 | const iter = val[Symbol.iterator]; 87 | if (typeof iter === "function") { 88 | return iter.call(val); 89 | } 90 | } 91 | return { 92 | next() { 93 | return { done: true, value: val }; 94 | }, 95 | }; 96 | }, 97 | [NODE_INSPECT]: () => `Pipe(${exec()})`, 98 | }; 99 | return ret as unknown as Pipeable; 100 | }; 101 | 102 | function tryCatch( 103 | fn: (...args: any[]) => any, 104 | value: any, 105 | args: any[], 106 | catchFn: (err: unknown) => any, 107 | ) { 108 | try { 109 | if (fn.constructor.name === "AsyncFunction") { 110 | return fn(value, ...args).catch(catchFn); 111 | } 112 | return fn(value, ...args); 113 | } catch (err) { 114 | return catchFn(err as any); 115 | } 116 | } 117 | 118 | type QueueItem = { 119 | fn: (x: T, ...args: any[]) => any; 120 | args: any[]; 121 | tap: boolean; 122 | catchFn?: (err: unknown) => Pipeable & Catchable & T; 123 | }; 124 | 125 | export type Pipe> = U extends Promise 126 | ? PipeMethodReturn 127 | : PipeMethodReturn & T; 128 | 129 | type Catchable = { catch: (fn: (err: unknown) => V) => Pipe }; 130 | type CatchableTap = { catch: (fn: (err: unknown) => V) => Pipe }; 131 | type PipeMethodReturn = M extends "to" 132 | ? Pipeable & Catchable 133 | : M extends "tap" 134 | ? Pipeable & CatchableTap 135 | : M extends "catch" 136 | ? Pipeable 137 | : never; 138 | 139 | type PipedFnTo = { 140 | /** 141 | * takes a function and arguments and adds it to the pipe. 142 | * when the pipe is executed, the function will be called with the value, setting the value to the result. 143 | * @param fn the function to be called with the value 144 | * @param args additional arguments to be passed to the function, if any 145 | * @returns the pipe for further chaining 146 | */ 147 | (fn: (x: T, ...args: A) => U, ...args: A): Pipe; 148 | (fn: (x: T) => U): Pipe; 149 | (fn: () => U): Pipe; 150 | }; 151 | 152 | type PipedFnTap = { 153 | /** 154 | * takes a function and arguments and adds it to the pipe. 155 | * when the pipe is executed, the function will be called with the value, but the value will not be changed. 156 | * @param fn the function to be called with the value 157 | * @param args additional arguments to be passed to the function, if any 158 | * @returns the pipe for further chaining 159 | */ 160 | (fn: (x: T, ...args: A) => U, ...args: A): Pipe; 161 | (fn: (x: T) => U): Pipe; 162 | (fn: () => U): Pipe; 163 | }; 164 | 165 | export interface Pipeable { 166 | /** 167 | * the result of the pipe after it's been executed. 168 | * if the pipe has not been executed, it wil execute it and return the result 169 | */ 170 | value: T; 171 | to: PipedFnTo; 172 | tap: PipedFnTap; 173 | /** executes the pipe and returns the result */ 174 | exec: () => T; 175 | // internals 176 | [NODE_INSPECT]: () => string; 177 | [Symbol.toStringTag]: "Pipe"; 178 | [Symbol.toPrimitive]: (hint: "string" | "number" | "default") => string | number; 179 | [Symbol.iterator]: () => Iterator; 180 | toString: Pipeable["exec"]; 181 | valueOf: Pipeable["exec"]; 182 | toJSON: Pipeable["exec"]; 183 | } 184 | -------------------------------------------------------------------------------- /src/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | format: ["esm", "cjs"], 5 | entry: { 6 | index: "pipe.ts", 7 | "fifo-queue": "fifo-queue.ts", 8 | }, 9 | outDir: "dist", 10 | dts: true, 11 | sourcemap: true, 12 | clean: true, 13 | }); 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext"], 4 | "module": "esnext", 5 | "target": "esnext", 6 | "moduleResolution": "bundler", 7 | "moduleDetection": "force", 8 | "allowImportingTsExtensions": true, 9 | "noEmit": true, 10 | "composite": true, 11 | "strict": true, 12 | "downlevelIteration": true, 13 | "skipLibCheck": false, 14 | "jsx": "react-jsx", 15 | "allowSyntheticDefaultImports": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "allowJs": true, 18 | "noUncheckedIndexedAccess": true, 19 | "types": ["bun-types"] 20 | } 21 | } 22 | --------------------------------------------------------------------------------