├── .gitignore ├── LICENSE ├── README.md ├── emitter.js ├── example.js ├── index.js ├── package.json └── test └── basic.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | sandbox.js 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 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 | # nanoresource 2 | 3 | Small module that helps you maintain state around resources 4 | 5 | ```sh 6 | npm install nanoresource 7 | ``` 8 | 9 | Allows you to easily implement open/close functionality for a resource 10 | and having a way to mark the resource as active/inactive to avoid it being closed 11 | while it is in middle of something. 12 | 13 | ## Usage 14 | 15 | We can use this module to implement a simple resource that keep a file descriptor 16 | around behind the scene to keep stating the same file. 17 | 18 | ```js 19 | const nanoresource = require('nanoresource') 20 | const fs = require('fs') 21 | 22 | class FileSize extends nanoresource { 23 | constructor (name) { 24 | super() 25 | this.filename = name 26 | this.fd = 0 27 | } 28 | 29 | _open (cb) { 30 | console.log('Now opening file ...') 31 | fs.open(this.filename, 'r', (err, fd) => { 32 | if (err) return cb(err) 33 | this.fd = fd 34 | cb(null) 35 | }) 36 | } 37 | 38 | _close (cb) { 39 | console.log('Now closing file ...') 40 | fs.close(this.fd, cb) 41 | } 42 | 43 | size (cb) { 44 | this.open((err) => { 45 | if (err) return cb(err) 46 | if (!this.active(cb)) return 47 | fs.fstat(this.fd, (err, st) => { 48 | if (err) return this.inactive(cb, err) 49 | this.inactive(cb, null, st.size) 50 | }) 51 | }) 52 | } 53 | } 54 | 55 | const f = new FileSize('index.js') 56 | 57 | f.size((err, size) => { 58 | if (err) throw err 59 | console.log('size is:', size) 60 | }) 61 | 62 | // size a couple of times 63 | f.size((err, size) => { 64 | if (err) throw err 65 | console.log('size is:', size) 66 | }) 67 | 68 | 69 | // after a bit when we are done with the resource we close it ... 70 | setTimeout(() => f.close(), 1000) 71 | ``` 72 | 73 | When running it you should see that the file is only being opened and closed 74 | once. 75 | 76 | ## API 77 | 78 | #### `const r = nanoresource(options)` 79 | 80 | Create a new nanoresource. You can also extend from this prototype if you prefer. 81 | 82 | Options include: 83 | 84 | ```js 85 | { 86 | open: function (cb) { ... }, 87 | close: function (cb) { ... } 88 | } 89 | ``` 90 | 91 | If you specify open or close they are used to populate `r._open` and `r._close` for you. 92 | 93 | The open should open the resource and the close one should close it. 94 | 95 | The close method is guaranteed to run *after* open. If no open has been called and close is called the close method is *not* called. 96 | 97 | #### `r.open(cb)` 98 | 99 | Open the resource. Will call `r._open` behind the scenes once. If multiple calls to `r.open(cb)` the callbacks will be pushed to an internal queue and executed after the one call to `_open` has completed. If the resource was opened in the past the callback will be called on the next tick. 100 | 101 | * Check `r.opened` to see if the resource is fully opened. 102 | * Check `r.opening` to see if the resource is in the process of being opened. 103 | 104 | If the `_open` method fails and calls it callback with an error this error is forwarded to the pending callbacks and if `r.open` is called again `_open` will be re-run. 105 | 106 | #### `r.close(cb)` 107 | 108 | Same semantics as `r.open`, except it only runs `_close` if the resource has been opened. If the resouce is in the middle of opening, `r.close` will wait for the open to finish and then try to close it. 109 | 110 | If the resource is active (see the `r.active()` docs) then close will wait for the the resource to become inactive before closing it. However is a call `r.active()` happens after `r.close()` has been called it will fail immediately. 111 | 112 | * Check `r.closed` to see if the resource is fully closed. 113 | * Check `r.closing` to see if the resource is in the process of being closed. 114 | 115 | Once a resource has been closed it can not be re-opened. 116 | 117 | #### `const valid = r.active()` 118 | 119 | Mark the resource as active. By marking a resource as active you have to call `r.inactive()` once at a later stage to indicate that it is no longer active from your point of view. 120 | 121 | If the resource is not in a valid active state (for example if it is being closed), the `r.active` method will return falls and you should return an error to the caller. 122 | 123 | As a conveinience you can pass in a callback to `r.active(callback)` and the active method will call that callback immediately with an error if it's not in a valid active state in addition to returning false. 124 | 125 | It is same to have multiple methods call this method in parallel. 126 | 127 | #### `r.inactive()` 128 | 129 | The counter-part to `r.active()`. You must call this if you called `r.active` previously. It is a good idea to call this as the last thing you do in your "action" method of your resource. 130 | 131 | As a conveinience you can pass in a callback, error, and value to `r.inactive(callback, error, value)` to call a callback after marking your view of the resource as inactive. 132 | 133 | It is same to have multiple methods call this method in parallel. 134 | 135 | ## EventEmitter 136 | 137 | If you need a nanoresource that is also an EventEmitter do `const Nanoresource = require('./emitter')` which returns an implementation that inherits from Node.js's EventEmitter prototype. 138 | 139 | ## License 140 | 141 | MIT 142 | -------------------------------------------------------------------------------- /emitter.js: -------------------------------------------------------------------------------- 1 | // Copy of index.js that extends from EventEmitter 2 | 3 | const events = require('events') 4 | const inherits = require('inherits') 5 | 6 | const opening = Symbol('opening queue') 7 | const preclosing = Symbol('closing when inactive') 8 | const closing = Symbol('closing queue') 9 | const sync = Symbol('sync') 10 | const fastClose = Symbol('fast close') 11 | 12 | module.exports = Nanoresource 13 | 14 | function Nanoresource (opts) { 15 | if (!(this instanceof Nanoresource)) return new Nanoresource(opts) 16 | events.EventEmitter.call(this) 17 | 18 | if (!opts) opts = {} 19 | if (opts.open) this._open = opts.open 20 | if (opts.close) this._close = opts.close 21 | 22 | this.opening = false 23 | this.opened = false 24 | this.closing = false 25 | this.closed = false 26 | this.actives = 0 27 | 28 | this[opening] = null 29 | this[preclosing] = null 30 | this[closing] = null 31 | this[sync] = false 32 | this[fastClose] = true 33 | } 34 | 35 | inherits(Nanoresource, events.EventEmitter) 36 | 37 | Nanoresource.prototype._open = function (cb) { 38 | cb(null) 39 | } 40 | 41 | Nanoresource.prototype._close = function (cb) { 42 | cb(null) 43 | } 44 | 45 | Nanoresource.prototype.open = function (cb) { 46 | if (!cb) cb = noop 47 | 48 | if (this[closing] || this.closed) return process.nextTick(cb, new Error('Resource is closed')) 49 | if (this.opened) return process.nextTick(cb) 50 | 51 | if (this[opening]) { 52 | this[opening].push(cb) 53 | return 54 | } 55 | 56 | this.opening = true 57 | this[opening] = [cb] 58 | this[sync] = true 59 | this._open(onopen.bind(this)) 60 | this[sync] = false 61 | } 62 | 63 | Nanoresource.prototype.active = function (cb) { 64 | if ((this[fastClose] && this[preclosing]) || this[closing] || this.closed) { 65 | if (cb) process.nextTick(cb, new Error('Resource is closed')) 66 | return false 67 | } 68 | this.actives++ 69 | return true 70 | } 71 | 72 | Nanoresource.prototype.inactive = function (cb, err, val) { 73 | if (!--this.actives) { 74 | const queue = this[preclosing] 75 | if (queue) { 76 | this[preclosing] = null 77 | while (queue.length) this.close(queue.shift()) 78 | } 79 | } 80 | 81 | if (cb) cb(err, val) 82 | } 83 | 84 | Nanoresource.prototype.close = function (allowActive, cb) { 85 | if (typeof allowActive === 'function') return this.close(false, allowActive) 86 | if (!cb) cb = noop 87 | 88 | if (allowActive) this[fastClose] = false 89 | 90 | if (this.closed) return process.nextTick(cb) 91 | 92 | if (this.actives || this[opening]) { 93 | if (!this[preclosing]) this[preclosing] = [] 94 | this[preclosing].push(cb) 95 | return 96 | } 97 | 98 | if (!this.opened) { 99 | this.closed = true 100 | process.nextTick(cb) 101 | return 102 | } 103 | 104 | if (this[closing]) { 105 | this[closing].push(cb) 106 | return 107 | } 108 | 109 | this.closing = true 110 | this[closing] = [cb] 111 | this[sync] = true 112 | this._close(onclose.bind(this)) 113 | this[sync] = false 114 | } 115 | 116 | function onopen (err) { 117 | if (this[sync]) return process.nextTick(onopen.bind(this), err) 118 | 119 | const oqueue = this[opening] 120 | this[opening] = null 121 | this.opening = false 122 | this.opened = !err 123 | 124 | while (oqueue.length) oqueue.shift()(err) 125 | 126 | const cqueue = this[preclosing] 127 | if (cqueue && !this.actives) { 128 | this[preclosing] = null 129 | while (cqueue.length) this.close(cqueue.shift()) 130 | } 131 | } 132 | 133 | function onclose (err) { 134 | if (this[sync]) return process.nextTick(onclose.bind(this), err) 135 | const queue = this[closing] 136 | this.closing = false 137 | this[closing] = null 138 | this.closed = !err 139 | while (queue.length) queue.shift()(err) 140 | } 141 | 142 | function noop () {} 143 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const nanoresource = require('./') 2 | const fs = require('fs') 3 | 4 | class FileSize extends nanoresource { 5 | constructor (name) { 6 | super() 7 | this.filename = name 8 | this.fd = 0 9 | } 10 | 11 | _open (cb) { 12 | console.log('Now opening file ...') 13 | fs.open(this.filename, 'r', (err, fd) => { 14 | if (err) return cb(err) 15 | this.fd = fd 16 | cb(null) 17 | }) 18 | } 19 | 20 | _close (cb) { 21 | console.log('Now closing file ...') 22 | fs.close(this.fd, cb) 23 | } 24 | 25 | size (cb) { 26 | this.open((err) => { 27 | if (err) return cb(err) 28 | if (!this.active(cb)) return 29 | fs.fstat(this.fd, (err, st) => { 30 | if (err) return this.inactive(cb, err) 31 | this.inactive(cb, null, st.size) 32 | }) 33 | }) 34 | } 35 | } 36 | 37 | const f = new FileSize('index.js') 38 | 39 | f.size((err, size) => { 40 | if (err) throw err 41 | console.log('size is:', size) 42 | }) 43 | 44 | // size a couple of times 45 | f.size((err, size) => { 46 | if (err) throw err 47 | console.log('size is:', size) 48 | }) 49 | 50 | // after a bit when we are done with the resource we close it ... 51 | setTimeout(() => f.close(), 1000) 52 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const opening = Symbol('opening queue') 2 | const preclosing = Symbol('closing when inactive') 3 | const closing = Symbol('closing queue') 4 | const sync = Symbol('sync') 5 | const fastClose = Symbol('fast close') 6 | 7 | module.exports = Nanoresource 8 | 9 | function Nanoresource (opts) { 10 | if (!(this instanceof Nanoresource)) return new Nanoresource(opts) 11 | 12 | if (!opts) opts = {} 13 | if (opts.open) this._open = opts.open 14 | if (opts.close) this._close = opts.close 15 | 16 | this.opening = false 17 | this.opened = false 18 | this.closing = false 19 | this.closed = false 20 | this.actives = 0 21 | 22 | this[opening] = null 23 | this[preclosing] = null 24 | this[closing] = null 25 | this[sync] = false 26 | this[fastClose] = true 27 | } 28 | 29 | Nanoresource.prototype._open = function (cb) { 30 | cb(null) 31 | } 32 | 33 | Nanoresource.prototype._close = function (cb) { 34 | cb(null) 35 | } 36 | 37 | Nanoresource.prototype.open = function (cb) { 38 | if (!cb) cb = noop 39 | 40 | if (this[closing] || this.closed) return process.nextTick(cb, new Error('Resource is closed')) 41 | if (this.opened) return process.nextTick(cb) 42 | 43 | if (this[opening]) { 44 | this[opening].push(cb) 45 | return 46 | } 47 | 48 | this.opening = true 49 | this[opening] = [cb] 50 | this[sync] = true 51 | this._open(onopen.bind(this)) 52 | this[sync] = false 53 | } 54 | 55 | Nanoresource.prototype.active = function (cb) { 56 | if ((this[fastClose] && this[preclosing]) || this[closing] || this.closed) { 57 | if (cb) process.nextTick(cb, new Error('Resource is closed')) 58 | return false 59 | } 60 | this.actives++ 61 | return true 62 | } 63 | 64 | Nanoresource.prototype.inactive = function (cb, err, val) { 65 | if (!--this.actives) { 66 | const queue = this[preclosing] 67 | if (queue) { 68 | this[preclosing] = null 69 | while (queue.length) this.close(queue.shift()) 70 | } 71 | } 72 | 73 | if (cb) cb(err, val) 74 | } 75 | 76 | Nanoresource.prototype.close = function (allowActive, cb) { 77 | if (typeof allowActive === 'function') return this.close(false, allowActive) 78 | if (!cb) cb = noop 79 | 80 | if (allowActive) this[fastClose] = false 81 | 82 | if (this.closed) return process.nextTick(cb) 83 | 84 | if (this.actives || this[opening]) { 85 | if (!this[preclosing]) this[preclosing] = [] 86 | this[preclosing].push(cb) 87 | return 88 | } 89 | 90 | if (!this.opened) { 91 | this.closed = true 92 | process.nextTick(cb) 93 | return 94 | } 95 | 96 | if (this[closing]) { 97 | this[closing].push(cb) 98 | return 99 | } 100 | 101 | this.closing = true 102 | this[closing] = [cb] 103 | this[sync] = true 104 | this._close(onclose.bind(this)) 105 | this[sync] = false 106 | } 107 | 108 | function onopen (err) { 109 | if (this[sync]) return process.nextTick(onopen.bind(this), err) 110 | 111 | const oqueue = this[opening] 112 | this[opening] = null 113 | this.opening = false 114 | this.opened = !err 115 | 116 | while (oqueue.length) oqueue.shift()(err) 117 | 118 | const cqueue = this[preclosing] 119 | if (cqueue && !this.actives) { 120 | this[preclosing] = null 121 | while (cqueue.length) this.close(cqueue.shift()) 122 | } 123 | } 124 | 125 | function onclose (err) { 126 | if (this[sync]) return process.nextTick(onclose.bind(this), err) 127 | const queue = this[closing] 128 | this.closing = false 129 | this[closing] = null 130 | this.closed = !err 131 | while (queue.length) queue.shift()(err) 132 | } 133 | 134 | function noop () {} 135 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nanoresource", 3 | "version": "1.3.0", 4 | "description": "Small module that helps you maintain state around resources", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "standard && tape test/*.js" 8 | }, 9 | "author": "Mathias Buus (@mafintosh)", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "standard": "^14.3.1", 13 | "tape": "^4.11.0" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/mafintosh/nanoresource.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/mafintosh/nanoresource/issues" 21 | }, 22 | "homepage": "https://github.com/mafintosh/nanoresource#readme", 23 | "dependencies": { 24 | "inherits": "^2.0.4" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | const tape = require('tape') 2 | const nanoresource = require('../') 3 | 4 | tape('basic usage', function (t) { 5 | t.plan(2 + 2 + 3 * 3 + 3 * 3) 6 | 7 | let opened = false 8 | let closed = false 9 | 10 | const r = nanoresource({ 11 | open (cb) { 12 | t.notOk(closed, 'not closed in open') 13 | t.notOk(opened, 'only open once') 14 | opened = true 15 | cb(null) 16 | }, 17 | close (cb) { 18 | t.ok(opened, 'opened when closing') 19 | t.notOk(closed, 'only close once') 20 | closed = true 21 | cb(null) 22 | } 23 | }) 24 | 25 | r.open(onopen) 26 | r.open(onopen) 27 | r.open(onopen) 28 | 29 | r.close(onclose) 30 | r.close(onclose) 31 | r.close(onclose) 32 | 33 | function onopen (err) { 34 | t.error(err, 'no error') 35 | t.ok(r.opened, 'was opened') 36 | t.ok(opened, 'open ran') 37 | } 38 | 39 | function onclose (err) { 40 | t.error(err, 'no error') 41 | t.ok(r.closed, 'was closed') 42 | t.ok(closed, 'close ran') 43 | } 44 | }) 45 | 46 | tape('open/close is never sync', function (t) { 47 | const r = nanoresource({ 48 | open (cb) { 49 | cb(null) 50 | }, 51 | close (cb) { 52 | cb(null) 53 | } 54 | }) 55 | 56 | let syncOpen = true 57 | r.open(function () { 58 | t.notOk(syncOpen) 59 | let syncClose = true 60 | r.close(function () { 61 | t.notOk(syncClose) 62 | t.end() 63 | }) 64 | syncClose = false 65 | }) 66 | syncOpen = false 67 | }) 68 | 69 | tape('active/inactive', function (t) { 70 | t.plan(2 + 10 * 1) 71 | 72 | let fails = 0 73 | let ran = 0 74 | 75 | const r = nanoresource({ 76 | close (cb) { 77 | t.same(fails, 0) 78 | t.same(ran, 10) 79 | cb(null) 80 | } 81 | }) 82 | 83 | for (let i = 0; i < 10; i++) { 84 | r.open(function () { 85 | t.ok(r.active()) 86 | ran++ 87 | fails++ 88 | setImmediate(function () { 89 | fails-- 90 | r.inactive() 91 | }) 92 | }) 93 | } 94 | 95 | r.open(() => r.close()) 96 | }) 97 | 98 | tape('active after close', function (t) { 99 | t.plan(5) 100 | 101 | const r = nanoresource({ 102 | close (cb) { 103 | t.notOk(r.active(), 'cannot be active in close') 104 | setImmediate(cb) 105 | } 106 | }) 107 | 108 | t.ok(r.active(), 'can be active') 109 | r.open() 110 | t.ok(r.active(), 'can be active') 111 | r.close(() => t.notOk(r.active(), 'still cannot be active')) 112 | t.notOk(r.active(), 'cannot be active cause close was call') 113 | 114 | r.inactive() 115 | r.inactive() 116 | }) 117 | --------------------------------------------------------------------------------