├── .editorconfig ├── .gitattributes ├── .github ├── security.md └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── index.d.ts ├── index.js ├── index.test-d.ts ├── license ├── package.json ├── readme.md └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/security.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 22 14 | - 20 15 | - 18 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - run: npm install 22 | - run: npm test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Create a lazy promise that defers execution until it's awaited or when `.then()`, `.catch()`, or `.finally()` is called. 3 | */ 4 | export default class PLazy extends Promise { // eslint-disable-line @typescript-eslint/naming-convention 5 | /** 6 | Create a `PLazy` promise from a promise-returning or async function. 7 | 8 | @example 9 | ``` 10 | import PLazy from 'p-lazy'; 11 | 12 | const lazyPromise = new PLazy(resolve => { 13 | someHeavyOperation(resolve); 14 | }); 15 | 16 | // `someHeavyOperation` is not yet called 17 | 18 | await doSomethingFun; 19 | 20 | // `someHeavyOperation` is called 21 | console.log(await lazyPromise); 22 | ``` 23 | */ 24 | static from(function_: () => ValueType | PromiseLike): PLazy; 25 | } 26 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export default class PLazy extends Promise { 2 | #executor; 3 | #promise; 4 | 5 | constructor(executor) { 6 | super(resolve => { 7 | resolve(); 8 | }); 9 | 10 | this.#executor = executor; 11 | } 12 | 13 | static from(function_) { 14 | return new PLazy(resolve => { 15 | resolve(function_()); 16 | }); 17 | } 18 | 19 | static resolve(value) { 20 | return new PLazy(resolve => { 21 | resolve(value); 22 | }); 23 | } 24 | 25 | static reject(error) { 26 | return new PLazy((resolve, reject) => { 27 | reject(error); 28 | }); 29 | } 30 | 31 | then(onFulfilled, onRejected) { 32 | this.#promise ??= new Promise(this.#executor); 33 | return this.#promise.then(onFulfilled, onRejected); 34 | } 35 | 36 | catch(onRejected) { 37 | this.#promise ??= new Promise(this.#executor); 38 | return this.#promise.catch(onRejected); 39 | } 40 | 41 | finally(onFinally) { 42 | this.#promise ??= new Promise(this.#executor); 43 | return this.#promise.finally(onFinally); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {expectType} from 'tsd'; 2 | import PLazy from './index.js'; 3 | 4 | const lazyPromise = new PLazy((resolve, reject) => { 5 | if (true) { // eslint-disable-line no-constant-condition 6 | reject(new Error('fixture')); 7 | } else { 8 | resolve('foo'); 9 | } 10 | }); 11 | 12 | expectType>(lazyPromise); 13 | expectType>(PLazy.from(async () => 1)); 14 | expectType>(PLazy.from(() => 1)); 15 | expectType>(PLazy.resolve(1)); 16 | expectType>(PLazy.reject(new Error('fixture'))); 17 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "p-lazy", 3 | "version": "5.0.0", 4 | "description": "Create a lazy promise that defers execution until it's awaited or when `.then()`, `.catch()`, or `.finally()` is called", 5 | "license": "MIT", 6 | "repository": "sindresorhus/p-lazy", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "type": "module", 14 | "exports": { 15 | "types": "./index.d.ts", 16 | "default": "./index.js" 17 | }, 18 | "sideEffects": false, 19 | "engines": { 20 | "node": ">=18" 21 | }, 22 | "scripts": { 23 | "test": "xo && ava && tsd" 24 | }, 25 | "files": [ 26 | "index.js", 27 | "index.d.ts" 28 | ], 29 | "keywords": [ 30 | "promise", 31 | "lazy", 32 | "defer", 33 | "deferred", 34 | "then", 35 | "catch", 36 | "fulfilled", 37 | "async", 38 | "function", 39 | "await", 40 | "promises", 41 | "bluebird" 42 | ], 43 | "devDependencies": { 44 | "ava": "^6.2.0", 45 | "tsd": "^0.31.2", 46 | "xo": "^0.59.3" 47 | }, 48 | "xo": { 49 | "rules": { 50 | "promise/prefer-await-to-then": "off", 51 | "unicorn/no-thenable": "off" 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # p-lazy 2 | 3 | > Create a lazy promise that defers execution until it's awaited or when `.then()`, or `.catch()`, or `.finally()` is called 4 | 5 | Useful if you're doing some heavy operations and would like to only do it when the promise is actually used. 6 | 7 | ## Install 8 | 9 | ```sh 10 | npm install p-lazy 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```js 16 | import PLazy from 'p-lazy'; 17 | 18 | const lazyPromise = new PLazy(resolve => { 19 | someHeavyOperation(resolve); 20 | }); 21 | 22 | // `someHeavyOperation` is not yet called 23 | 24 | await doSomethingFun; 25 | 26 | // `someHeavyOperation` is called 27 | console.log(await lazyPromise); 28 | ``` 29 | 30 | ## API 31 | 32 | ### new PLazy(executor) 33 | 34 | Same as the [`Promise` constructor](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise). `PLazy` is a subclass of `Promise`. 35 | 36 | ### PLazy.from(fn) 37 | 38 | Create a `PLazy` promise from a promise-returning or async function. 39 | 40 | ### PLazy.resolve(value) 41 | 42 | Create a `PLazy` promise that is resolved with the given `value`, or the promise passed as `value`. 43 | 44 | ### PLazy.reject(reason) 45 | 46 | Create a `PLazy` promise that is rejected with the given `reason`. 47 | 48 | ## Related 49 | 50 | - [p-cancelable](https://github.com/sindresorhus/p-cancelable) - Create a promise that can be canceled 51 | - [p-defer](https://github.com/sindresorhus/p-defer) - Create a deferred promise 52 | - [lazy-value](https://github.com/sindresorhus/lazy-value) - Create a lazily evaluated value 53 | - [define-lazy-prop](https://github.com/sindresorhus/define-lazy-prop) - Define a lazily evaluated property on an object 54 | - [More…](https://github.com/sindresorhus/promise-fun) 55 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import {setTimeout as delay} from 'node:timers/promises'; 2 | import test from 'ava'; 3 | import PLazy from './index.js'; 4 | 5 | const fixture = Symbol('fixture'); 6 | 7 | test('executor resolves', async t => { 8 | const steps = []; 9 | 10 | const lazyPromise = new PLazy(resolve => { 11 | steps.push('executor called'); 12 | resolve(fixture); 13 | }); 14 | 15 | steps.push('promise created'); 16 | 17 | await delay(50); 18 | 19 | steps.push('then called'); 20 | 21 | await lazyPromise.then(value => { 22 | t.is(value, fixture); 23 | steps.push('then-handler called'); 24 | }); 25 | 26 | t.deepEqual(steps, [ 27 | 'promise created', 28 | 'then called', 29 | 'executor called', 30 | 'then-handler called', 31 | ]); 32 | }); 33 | 34 | test('executor rejects', async t => { 35 | const fixtureError = new Error('fixture'); 36 | const steps = []; 37 | 38 | const lazyPromise = new PLazy((resolve, reject) => { 39 | steps.push('executor called'); 40 | reject(fixtureError); 41 | }); 42 | 43 | steps.push('promise created'); 44 | 45 | await delay(50); 46 | 47 | steps.push('catch called'); 48 | 49 | await lazyPromise.catch(error => { 50 | t.is(error, fixtureError); 51 | steps.push('catch-handler called'); 52 | }); 53 | 54 | t.deepEqual(steps, [ 55 | 'promise created', 56 | 'catch called', 57 | 'executor called', 58 | 'catch-handler called', 59 | ]); 60 | }); 61 | 62 | test('executor is never called if no `then`', async t => { 63 | t.plan(1); 64 | new PLazy(resolve => { // eslint-disable-line no-new 65 | t.fail('executor should not be called'); 66 | resolve(); 67 | }); 68 | 69 | await delay(50); 70 | t.pass(); 71 | }); 72 | 73 | test('executor is called with only catch handler', async t => { 74 | const steps = []; 75 | 76 | const lazyPromise = new PLazy(resolve => { 77 | steps.push('executor called'); 78 | resolve(); 79 | }); 80 | 81 | steps.push('promise created'); 82 | 83 | await delay(50); 84 | 85 | steps.push('catch called'); 86 | 87 | await lazyPromise.catch(() => {}); 88 | 89 | t.deepEqual(steps, [ 90 | 'promise created', 91 | 'catch called', 92 | 'executor called', 93 | ]); 94 | }); 95 | 96 | test('convert promise-returning function to lazy promise', async t => { 97 | let called = false; 98 | 99 | const lazyPromise = PLazy.from(async () => { 100 | called = true; 101 | return fixture; 102 | }); 103 | 104 | t.true(lazyPromise instanceof PLazy); 105 | t.true(lazyPromise instanceof Promise); 106 | t.false(called); 107 | 108 | t.is(await lazyPromise, fixture); 109 | t.true(called); 110 | }); 111 | 112 | test('should have static method `reject` that returns a lazy rejected promise', async t => { 113 | const fixtureError = new Error('fixture'); 114 | const steps = []; 115 | 116 | const lazyPromise = PLazy.reject(fixtureError); 117 | 118 | steps.push('promise created'); 119 | 120 | await delay(50); 121 | 122 | steps.push('catch called'); 123 | 124 | await lazyPromise.catch(error => { 125 | t.is(error, fixtureError); 126 | steps.push('catch-handler called'); 127 | }); 128 | 129 | t.deepEqual(steps, [ 130 | 'promise created', 131 | 'catch called', 132 | 'catch-handler called', 133 | ]); 134 | }); 135 | 136 | test('should have static method `resolve` that returns a lazy resolved promise', async t => { 137 | const lazyPromise = PLazy.resolve(fixture); 138 | 139 | t.true(lazyPromise instanceof PLazy); 140 | t.true(lazyPromise instanceof Promise); 141 | 142 | t.is(await lazyPromise, fixture); 143 | }); 144 | --------------------------------------------------------------------------------