├── .github └── workflows │ └── test-node.yml ├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package.json └── test.js /.github/workflows/test-node.yml: -------------------------------------------------------------------------------- 1 | name: Build Status 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | node-version: [lts/*] 14 | os: [ubuntu-latest, macos-latest, windows-latest] 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - run: npm install 23 | - run: npm test 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | sandbox.js 3 | sandbox 4 | coverage 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Mathias Buus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ready-resource 2 | 3 | Modern single resource management 4 | 5 | ``` 6 | npm install ready-resource 7 | ``` 8 | 9 | ## Usage 10 | 11 | ``` js 12 | const ReadyResource = require('ready-resource') 13 | 14 | class Thing extends ReadyResource { 15 | constructor () { 16 | super() 17 | } 18 | 19 | async _open () { 20 | // open the resource 21 | } 22 | 23 | async _close () { 24 | // close the resource 25 | } 26 | } 27 | 28 | const r = new Thing() 29 | 30 | await r.ready() // calls _open once 31 | await r.ready() // noop 32 | 33 | await r.close() // calls _close after _open has finished 34 | await r.close() // noop 35 | ``` 36 | 37 | ## License 38 | 39 | MIT 40 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events') 2 | 3 | module.exports = class ReadyResource extends EventEmitter { 4 | constructor () { 5 | super() 6 | 7 | this.opening = null 8 | this.closing = null 9 | 10 | this.opened = false 11 | this.closed = false 12 | } 13 | 14 | ready () { 15 | if (this.opening !== null) return this.opening 16 | this.opening = open(this) 17 | return this.opening 18 | } 19 | 20 | close () { 21 | if (this.closing !== null) return this.closing 22 | this.closing = close(this) 23 | return this.closing 24 | } 25 | 26 | async _open () { 27 | // add impl here 28 | } 29 | 30 | async _close () { 31 | // add impl here 32 | } 33 | } 34 | 35 | async function open (self) { 36 | // open after close 37 | if (self.closing !== null) return 38 | 39 | try { 40 | await self._open() 41 | } catch (err) { 42 | self.close() // safe to run in bg 43 | throw err 44 | } 45 | 46 | self.opened = true 47 | self.emit('ready') 48 | } 49 | 50 | async function close (self) { 51 | try { 52 | if (self.opened === false && self.opening !== null) await self.opening 53 | } catch { 54 | // ignore errors on closing 55 | } 56 | if (self.opened === true || self.opening === null) await self._close() 57 | self.closed = true 58 | self.emit('close') 59 | } 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ready-resource", 3 | "version": "1.1.2", 4 | "description": "Modern single resource management", 5 | "main": "index.js", 6 | "imports": { 7 | "events": { 8 | "bare": "bare-events", 9 | "default": "events" 10 | } 11 | }, 12 | "files": [ 13 | "index.js" 14 | ], 15 | "scripts": { 16 | "test": "standard && brittle test.js" 17 | }, 18 | "devDependencies": { 19 | "brittle": "^3.1.0", 20 | "standard": "^17.0.0" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/holepunchto/ready-resource.git" 25 | }, 26 | "author": "Mathias Buus (@mafintosh)", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/holepunchto/ready-resource/issues" 30 | }, 31 | "homepage": "https://github.com/holepunchto/ready-resource", 32 | "dependencies": { 33 | "bare-events": "^2.2.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const test = require('brittle') 2 | const Resource = require('./') 3 | 4 | test('basic', async function (t) { 5 | const r = new Resource() 6 | 7 | let opened = false 8 | let closed = false 9 | 10 | r._open = async function () { 11 | opened = true 12 | } 13 | 14 | r._close = async function () { 15 | closed = true 16 | } 17 | 18 | await r.ready() 19 | 20 | t.is(opened, true) 21 | t.is(closed, false) 22 | 23 | await r.close() 24 | 25 | t.is(opened, true) 26 | t.is(closed, true) 27 | }) 28 | 29 | test('basic - close when open throws', async function (t) { 30 | const r = new Resource() 31 | 32 | let triggered = false 33 | 34 | r._open = async function () { 35 | throw new Error('Synthetic') 36 | } 37 | 38 | r._close = async function () { 39 | triggered = true 40 | } 41 | 42 | await t.exception(r.ready(), /Synthetic/) 43 | 44 | t.is(r.opened, false) 45 | t.is(triggered, false) 46 | t.is(r.closed, true) // autocloses 47 | 48 | await t.execution(r.close()) 49 | 50 | t.is(r.opened, false) 51 | t.is(triggered, false) 52 | t.is(r.closed, true) 53 | }) 54 | 55 | test('ready rejecting emits close', async t => { 56 | t.plan(4) 57 | 58 | const r = new Resource() 59 | r._open = () => Promise.reject(new Error('bad open')) 60 | 61 | r.on('close', () => { 62 | t.pass('emitted close') 63 | t.ok(r.closed) 64 | t.ok(r.closing) 65 | }) 66 | 67 | await t.exception(r.ready()) 68 | }) 69 | --------------------------------------------------------------------------------