├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | tmp/ 4 | npm-debug.log* 5 | .DS_Store 6 | .nyc_output/ 7 | yarn.lock 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | node_js: 2 | - "4" 3 | - "5" 4 | - "6" 5 | - "7" 6 | sudo: false 7 | language: node_js 8 | script: "npm run test" 9 | after_success: "npm i -g codecov && npm run coverage && codecov" 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Yoshua Wuyts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![deprecated](http://badges.github.io/stability-badges/dist/deprecated.svg)](https://dat-ecosystem.org/) 2 | 3 | More info on active projects and modules at [dat-ecosystem.org](https://dat-ecosystem.org/) 4 | 5 | --- 6 | 7 | # multidrive [![stability][0]][1] 8 | [![npm version][2]][3] [![build status][4]][5] [![Test coverage][6]][7] 9 | [![downloads][8]][9] [![js-standard-style][10]][11] 10 | 11 | Manage multiple hyperdrive archives located anywhere on the filesystem. 12 | 13 | ## Usage 14 | ```js 15 | var hyperdrive = require('hyperdrive') 16 | var multidrive = require('multidrive') 17 | var toilet = require('toiletdb') 18 | 19 | var store = toilet('./data.json') 20 | multidrive(store, createArchive, closeArchive, function (err, drive) { 21 | if (err) throw err 22 | 23 | var data = { key: '<64-bit-hex>' } 24 | drive.create(data, function (err, archive) { 25 | if (err) throw err 26 | 27 | var archives = drive.list() 28 | console.log(archives) 29 | 30 | drive.close(archive.key, function (err) { 31 | if (err) throw err 32 | console.log('archive deleted') 33 | }) 34 | }) 35 | }) 36 | 37 | function createArchive (data, done) { 38 | var db = level('/tmp/' + 'multidrive-' + data.key) 39 | var drive = hyperdrive(db) 40 | var archive = drive.createArchive(data.key) 41 | done(null, archive) 42 | } 43 | 44 | function closeArchive (archive, done) { 45 | archive.close() 46 | done() 47 | } 48 | ``` 49 | 50 | ## Error handling 51 | If there is an error initializing a drive, instead of the whole process failing, an error object with attached `.data` property will be pushed into the list of archives instead. That means when consuming multidrive.list(), you should check for errors: 52 | 53 | ```js 54 | var archives = multidrive.list() 55 | archives.forEach(function (archive) { 56 | if (archive instanceof Error) { 57 | var err = archive 58 | console.log('failed to initialize archive with %j: %s', err.data, err.message) 59 | } 60 | }) 61 | ``` 62 | 63 | This way you can decide for yourself whether an individual initialization failure should cause the whole process to fail or not. 64 | 65 | ## API 66 | ### multidrive(store, createArchive, closeArchive, callback(err, drive)) 67 | Create a new multidrive instance. `db` should be a valid `toiletdb` instance. 68 | `createArchive` is the function used to create new Hyperdrive archives. 69 | `callback` is called after initialization. `closeArchive` is called when 70 | `drive.remove()` is called. 71 | 72 | `createArchive` has an api of `createArchive(data, done)` where `data` is passed in 73 | by `drive.create()` and `done(err, archive)` expects a valid archive. 74 | 75 | `closeArchive` has an api of `closeArchive(archive, done)` where `archive` was 76 | created by `createArchive` and `done(err)` is expected to be called when the 77 | archive has been properly closed. `closeArchive` is called when a specific 78 | archive is closed through `.close` or when through `.disconnect` all archives get 79 | disconnected. 80 | 81 | ### archives = drive.list() 82 | List all `archives` in the `multidrive`. 83 | 84 | ### drive.create(data, callback(err, drive[, duplicate])) 85 | Create a new Hyperdrive archive. `data` is passed into `createArchive`. 86 | If an archive with the same key already exists, returns that instead and sets 87 | `duplicate` to `true`. 88 | 89 | ### drive.close(key, callback(err)) 90 | Remove an archive by its public key. Calls `closeArchive()` 91 | 92 | ### drive.disconnect(callback(err)) 93 | Disconnects the drive from the store and closes all archives (without removing them). 94 | 95 | ## Installation 96 | ```sh 97 | $ npm install multidrive 98 | ``` 99 | 100 | ## See Also 101 | - https://github.com/karissa/hyperdiscovery 102 | - https://github.com/mafintosh/hyperdrive 103 | - https://github.com/Level/level 104 | - https://github.com/maxogden/toiletdb 105 | 106 | ## License 107 | [MIT](https://tldrlegal.com/license/mit-license) 108 | 109 | [0]: https://img.shields.io/badge/stability-experimental-orange.svg?style=flat-square 110 | [1]: https://nodejs.org/api/documentation.html#documentation_stability_index 111 | [2]: https://img.shields.io/npm/v/multidrive.svg?style=flat-square 112 | [3]: https://npmjs.org/package/multidrive 113 | [4]: https://img.shields.io/travis/datproject/multidrive/master.svg?style=flat-square 114 | [5]: https://travis-ci.org/datproject/multidrive 115 | [6]: https://img.shields.io/codecov/c/github/datproject/multidrive/master.svg?style=flat-square 116 | [7]: https://codecov.io/github/datproject/multidrive 117 | [8]: http://img.shields.io/npm/dm/multidrive.svg?style=flat-square 118 | [9]: https://npmjs.org/package/multidrive 119 | [10]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square 120 | [11]: https://github.com/feross/standard 121 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var mapLimit = require('map-limit') 2 | var assert = require('assert') 3 | var debug = require('debug')('multidrive') 4 | 5 | module.exports = multidrive 6 | 7 | function multidrive (store, createArchive, closeArchive, cb) { 8 | assert.equal(typeof store, 'object', 'multidrive: store should be type object') 9 | assert.equal(typeof createArchive, 'function', 'multidrive: createArchive should be type function') 10 | assert.equal(typeof closeArchive, 'function', 'multidrive: closeArchive should be type function') 11 | assert.equal(typeof cb, 'function', 'multidrive: cb should be type function') 12 | 13 | var archives = [] 14 | var _disconnected = false 15 | var drive = { 16 | list: list, 17 | create: create, 18 | close: close, 19 | disconnect: disconnect 20 | } 21 | 22 | debug('initialize') 23 | store.read(sink) 24 | 25 | function sink (err, data) { 26 | if (err) return cb(err) 27 | var values = Object.keys(data).map(function (key) { 28 | var value = JSON.parse(data[key]) 29 | value.key = key 30 | return value 31 | }) 32 | debug('found %s dats', values.length) 33 | 34 | function createWithoutError (data, cb) { 35 | try { 36 | createArchive(data, function (err, dat) { 37 | if (err) { 38 | err.data = data 39 | dat = err 40 | err = null 41 | } 42 | cb(err, dat) 43 | }) 44 | } catch (err) { 45 | err.data = data 46 | cb(null, err) 47 | } 48 | } 49 | 50 | mapLimit(values, 1, createWithoutError, function (err, _archives) { 51 | if (err) return cb(err) 52 | archives = _archives 53 | debug('initialized') 54 | cb(null, drive) 55 | }) 56 | } 57 | 58 | function list () { 59 | return archives 60 | } 61 | 62 | function create (data, cb) { 63 | if (_disconnected) return setImmediate(cb.bind(null, new Error('disconnected'))) 64 | debug('create archive data=%j', data) 65 | createArchive(data, function (err, archive) { 66 | if (err) return cb(err) 67 | var key = archive.key 68 | var hexKey = key.toString('hex') 69 | debug('archive created key=%s', hexKey) 70 | 71 | var duplicates = archives.filter(function (_archive) { 72 | if (_archive instanceof Error) return false 73 | var a = Buffer(_archive.key) 74 | var b = Buffer(key) 75 | return a.equals(b) 76 | }) 77 | var duplicate = duplicates[0] 78 | if (duplicate) { 79 | debug('archive duplicate key=%s', hexKey) 80 | return cb(null, duplicate, Boolean(duplicate)) 81 | } 82 | 83 | var _data 84 | if (data) _data = JSON.stringify(data) 85 | store.write(key, _data, function (err) { 86 | if (err) return cb(err) 87 | debug('archive stored key=%s', hexKey) 88 | archives.push(archive) 89 | cb(null, archive) 90 | }) 91 | }) 92 | } 93 | 94 | function disconnect (cb) { 95 | if (_disconnected) return setImmediate(cb.bind(null, new Error('disconnected'))) 96 | _disconnected = true 97 | store = null 98 | if (archives.length === 0) return setImmediate(cb) 99 | var _archives = archives 100 | var count = _archives.length 101 | var _err 102 | _archives.forEach(function (archive) { 103 | closeArchive(archive, next) 104 | }) 105 | archives = [] 106 | 107 | function next (err) { 108 | count-- 109 | if (err && !_err) { 110 | _err = err 111 | } 112 | if (count === 0) { 113 | cb(_err) 114 | } 115 | } 116 | } 117 | 118 | function close (key, cb) { 119 | if (_disconnected) return setImmediate(cb.bind(null, new Error('disconnected'))) 120 | if (Buffer.isBuffer(key)) key = key.toString('hex') 121 | debug('close archive key=%s', key) 122 | var i = 0 123 | var archive = archives.find(function (archive, j) { 124 | var _key = (archive.key || archive.data.key).toString('hex') 125 | if (_key !== key) return 126 | i = j 127 | return true 128 | }) 129 | if (!archive) return setImmediate(cb.bind(null, new Error('could not find archive ' + key))) 130 | if (archive instanceof Error) next() 131 | else closeArchive(archive, next) 132 | 133 | function next (err) { 134 | if (err) return cb(err) 135 | debug('archive closed key=%s', key) 136 | store.delete(key, function (err) { 137 | if (err) return cb(err) 138 | debug('archive deleted key=%s', key) 139 | archives.splice(i, 1) 140 | cb(null, archive) 141 | }) 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multidrive", 3 | "version": "5.2.1", 4 | "description": "Manage multiple hyperdrive instances", 5 | "main": "index.js", 6 | "scripts": { 7 | "deps": "dependency-check . && dependency-check . --extra --no-dev", 8 | "test": "standard && npm run deps && nyc node test.js", 9 | "coverage": "nyc report --reporter=text-lcov > coverage.lcov" 10 | }, 11 | "repository": "datproject/multidrive", 12 | "keywords": [ 13 | "hyperdrive", 14 | "hypercore", 15 | "dat" 16 | ], 17 | "license": "MIT", 18 | "dependencies": { 19 | "debug": "^2.6.6", 20 | "map-limit": "0.0.1" 21 | }, 22 | "devDependencies": { 23 | "dependency-check": "^2.6.0", 24 | "hyperdrive": "^7.13.1", 25 | "istanbul": "^0.4.5", 26 | "memdb": "^1.3.1", 27 | "noop2": "^2.0.0", 28 | "nyc": "^10.0.0", 29 | "random-access-file": "^1.3.1", 30 | "random-access-memory": "^2.2.0", 31 | "rimraf": "^2.5.4", 32 | "standard": "^8.5.0", 33 | "tape": "^4.6.2", 34 | "toiletdb": "^1.0.0", 35 | "uuid": "^3.0.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var hyperdrive = require('hyperdrive') 2 | var toilet = require('toiletdb') 3 | var memdb = require('memdb') 4 | var test = require('tape') 5 | var fs = require('fs') 6 | 7 | function flushToilet () { 8 | try { 9 | fs.unlinkSync('state.json') 10 | } catch (e) {} 11 | } 12 | 13 | flushToilet() 14 | 15 | var noop = function () {} 16 | var noopCb = function () { 17 | setImmediate(arguments[arguments.length - 1]) 18 | } 19 | var multidrive = require('./') 20 | 21 | test('drive = multidrive', function (t) { 22 | t.test('should assert input types', function (t) { 23 | t.plan(3) 24 | t.throws(multidrive.bind(null), /object/) 25 | t.throws(multidrive.bind(null, {}), /function/) 26 | t.throws(multidrive.bind(null, {}, noop), /function/) 27 | }) 28 | t.end() 29 | }) 30 | 31 | test('drive.create', function (t) { 32 | t.test('should create an archive', function (t) { 33 | t.plan(6) 34 | 35 | var store = toilet('state.json') 36 | multidrive(store, createArchive, noopCb, function (err, drive) { 37 | t.ifError(err, 'no err') 38 | t.equal(typeof drive, 'object', 'drive was returned') 39 | drive.create(null, function (err, archive) { 40 | t.ifError(err, 'no err') 41 | t.equal(typeof archive, 'object', 'archive was created') 42 | t.ok(Buffer.isBuffer(archive.metadata.key), 'archive has a key') 43 | drive.disconnect(function (err) { 44 | t.error(err) 45 | }) 46 | }) 47 | }) 48 | 49 | function createArchive (data, done) { 50 | var db = memdb() 51 | var drive = hyperdrive(db) 52 | var archive = drive.createArchive() 53 | done(null, archive) 54 | } 55 | }) 56 | 57 | t.test('should recreate archives', function (t) { 58 | t.plan(6) 59 | flushToilet() 60 | var store = toilet('state.json') 61 | multidrive(store, createArchive, noopCb, function (err, drive) { 62 | t.ifError(err, 'no err') 63 | 64 | drive.create({ hello: 'world' }, function (err, archive) { 65 | t.ifError(err, 'no err') 66 | 67 | drive.disconnect(function (err) { 68 | t.error(err) 69 | var newStore = toilet('state.json') 70 | multidrive(newStore, createArchive, noopCb, function (err, drive) { 71 | t.ifError(err, 'no err') 72 | var drives = drive.list() 73 | t.equal(drives.length, 1, 'one drive on init') 74 | drive.disconnect(function (err) { 75 | t.error(err) 76 | }) 77 | }) 78 | }) 79 | }) 80 | }) 81 | 82 | function createArchive (data, done) { 83 | var db = memdb() 84 | var drive = hyperdrive(db) 85 | var archive = drive.createArchive() 86 | done(null, archive) 87 | } 88 | }) 89 | 90 | t.test('should noop on duplicates', function (t) { 91 | t.plan(6) 92 | flushToilet() 93 | var store = toilet('state.json') 94 | var db = memdb() 95 | var drive = hyperdrive(db) 96 | multidrive(store, createArchive, noopCb, function (err, drive) { 97 | t.ifError(err, 'no err') 98 | 99 | drive.create({ hello: 'world' }, function (err, archive) { 100 | t.ifError(err, 'no err') 101 | 102 | drive.create({ key: archive.key }, function (err, _archive, duplicate) { 103 | t.ifError(err, 'no err') 104 | t.equal(_archive, archive) 105 | t.equal(duplicate, true) 106 | }) 107 | drive.disconnect(function (err) { 108 | t.error(err) 109 | }) 110 | }) 111 | }) 112 | 113 | function createArchive (data, done) { 114 | var archive = drive.createArchive({ key: data.key }) 115 | done(null, archive) 116 | } 117 | }) 118 | 119 | t.test('should properly compare different key types', function (t) { 120 | t.plan(6) 121 | flushToilet() 122 | var store = toilet('state.json') 123 | var db = memdb() 124 | var drive = hyperdrive(db) 125 | multidrive(store, createArchive, noopCb, function (err, drive) { 126 | t.ifError(err, 'no err') 127 | 128 | drive.create({ hello: 'world' }, function (err, archive) { 129 | t.ifError(err, 'no err') 130 | 131 | drive.create({ key: archive.key }, function (err, _archive, duplicate) { 132 | t.ifError(err, 'no err') 133 | t.equal(_archive, archive) 134 | t.equal(duplicate, true) 135 | drive.disconnect(function (err) { 136 | t.error(err) 137 | }) 138 | }) 139 | }) 140 | }) 141 | 142 | function createArchive (data, done) { 143 | var archive = drive.createArchive({ key: data.key }) 144 | if (data.key) archive.key = Buffer(archive.key) 145 | done(null, archive) 146 | } 147 | }) 148 | t.end() 149 | }) 150 | 151 | test('drive.list', function (t) { 152 | t.test('should list archives', function (t) { 153 | t.plan(4) 154 | flushToilet() 155 | 156 | var store = toilet('state.json') 157 | multidrive(store, createArchive, noopCb, function (err, drive) { 158 | t.ifError(err, 'no err') 159 | drive.create(null, function (err, archive) { 160 | t.ifError(err, 'no err') 161 | var drives = drive.list() 162 | t.equal(drives.length, 1, 'one drive') 163 | drive.disconnect(function (err) { 164 | t.error(err) 165 | }) 166 | }) 167 | }) 168 | 169 | function createArchive (data, done) { 170 | var db = memdb() 171 | var drive = hyperdrive(db) 172 | var archive = drive.createArchive() 173 | done(null, archive) 174 | } 175 | }) 176 | 177 | t.test('should not fail on initial archive creation errors', function (t) { 178 | t.plan(9) 179 | flushToilet() 180 | 181 | var store = toilet('state.json') 182 | var createArchive = function (data, done) { 183 | var db = memdb() 184 | var drive = hyperdrive(db) 185 | var archive = drive.createArchive() 186 | done(null, archive) 187 | } 188 | multidrive(store, createArchive, noopCb, function (err, drive) { 189 | t.ifError(err, 'no err') 190 | drive.create({ some: 'data' }, function (err, archive) { 191 | t.ifError(err, 'no err') 192 | var createArchive = function (data, done) { 193 | done(Error('not today')) 194 | } 195 | drive.disconnect(function (err) { 196 | t.error(err) 197 | multidrive(store, createArchive, noopCb, function (err, drive) { 198 | t.ifError(err, 'no err') 199 | var drives = drive.list() 200 | t.equal(drives.length, 1, 'one drive') 201 | t.ok(drives[0] instanceof Error) 202 | t.equal(drives[0].data.some, 'data') 203 | t.equal(drives[0].data.key, archive.key.toString('hex')) 204 | drive.disconnect(function (err) { 205 | t.error(err) 206 | }) 207 | }) 208 | }) 209 | }) 210 | }) 211 | }) 212 | t.end() 213 | }) 214 | 215 | test('drive.close', function (t) { 216 | t.test('close an archive', function (t) { 217 | t.plan(5) 218 | flushToilet() 219 | 220 | var store = toilet('state.json') 221 | multidrive(store, createArchive, closeArchive, function (err, drive) { 222 | t.ifError(err, 'no err') 223 | drive.create(null, function (err, archive) { 224 | t.ifError(err, 'no err') 225 | drive.close(archive.key, function (err) { 226 | t.ifError(err, 'no err') 227 | var drives = drive.list() 228 | t.equal(drives.length, 0, 'no drives left') 229 | drive.disconnect(function (err) { 230 | t.error(err) 231 | }) 232 | }) 233 | }) 234 | }) 235 | 236 | function createArchive (data, done) { 237 | var db = memdb() 238 | var drive = hyperdrive(db) 239 | var archive = drive.createArchive() 240 | done(null, archive) 241 | } 242 | 243 | function closeArchive (archive, done) { 244 | archive.close() 245 | done() 246 | } 247 | }) 248 | 249 | t.test('close an archive instanceof Error', function (t) { 250 | t.plan(7) 251 | flushToilet() 252 | 253 | var store = toilet('state.json') 254 | multidrive(store, createArchive, closeArchive, function (err, drive) { 255 | t.ifError(err, 'no err') 256 | drive.create({}, function (err, archive) { 257 | t.ifError(err, 'no err') 258 | var createArchive = function (data, done) { 259 | done(Error('not today')) 260 | } 261 | drive.disconnect(function (err) { 262 | t.error(err) 263 | multidrive(store, createArchive, noopCb, function (err, drive) { 264 | t.ifError(err, 'no err') 265 | var errDat = drive.list()[0] 266 | drive.close(errDat.data.key, function (err) { 267 | t.ifError(err, 'no err') 268 | var drives = drive.list() 269 | t.equal(drives.length, 0, 'no drives left') 270 | drive.disconnect(function (err) { 271 | t.error(err) 272 | }) 273 | }) 274 | }) 275 | }) 276 | }) 277 | }) 278 | 279 | function createArchive (data, done) { 280 | var db = memdb() 281 | var drive = hyperdrive(db) 282 | var archive = drive.createArchive() 283 | done(null, archive) 284 | } 285 | 286 | function closeArchive (archive, done) { 287 | archive.close() 288 | done() 289 | } 290 | }) 291 | t.end() 292 | }) 293 | 294 | test('drive.disconnect', function (t) { 295 | t.test('create and disconnect without archives created', function (t) { 296 | t.plan(2) 297 | flushToilet() 298 | var store = toilet('state.json') 299 | multidrive(store, noop, noop, function (err, drive) { 300 | t.error(err) 301 | drive.disconnect(function (err) { 302 | t.error(err) 303 | }) 304 | }) 305 | }) 306 | t.test('errors after disconnection', function (t) { 307 | t.plan(6) 308 | flushToilet() 309 | var store = toilet('state.json') 310 | multidrive(store, noop, noop, function (err, drive) { 311 | t.error(err) 312 | drive.disconnect(function (err) { 313 | t.error(err) 314 | t.equals(drive.list().length, 0) 315 | drive.create(null, function (err) { 316 | t.equals(err.message, 'disconnected') 317 | drive.close(null, function (err) { 318 | t.equals(err.message, 'disconnected') 319 | drive.disconnect(function (err) { 320 | t.equals(err.message, 'disconnected') 321 | }) 322 | }) 323 | }) 324 | }) 325 | }) 326 | }) 327 | t.test('disconnecting closes archives', function (t) { 328 | t.plan(4) 329 | flushToilet() 330 | var store = toilet('state.json') 331 | multidrive(store, createArchive, closeArchive, function (err, drive) { 332 | t.error(err) 333 | drive.create(null, function (err, archive) { 334 | t.error(err) 335 | drive.disconnect(function (err) { 336 | t.error(err) 337 | t.ok(archive._closed) 338 | }) 339 | }) 340 | }) 341 | 342 | function createArchive (data, done) { 343 | var db = memdb() 344 | var drive = hyperdrive(db) 345 | var archive = drive.createArchive() 346 | done(null, archive) 347 | } 348 | 349 | function closeArchive (archive, done) { 350 | archive.close() 351 | done() 352 | } 353 | }) 354 | t.test('disconnecting closes archives and passes through errors', function (t) { 355 | t.plan(3) 356 | flushToilet() 357 | var store = toilet('state.json') 358 | multidrive(store, createArchive, closeArchive, function (err, drive) { 359 | t.error(err) 360 | drive.create({key: 'x'}, function (err, archive) { 361 | t.error(err) 362 | drive.disconnect(function (err) { 363 | t.equals(err.message, 'test-error') 364 | }) 365 | }) 366 | }) 367 | 368 | function createArchive (data, done) { 369 | done(null, data) 370 | } 371 | 372 | function closeArchive (archive, done) { 373 | done(new Error('test-error')) 374 | } 375 | }) 376 | t.end() 377 | }) 378 | 379 | test('cleanup toilet', function (t) { 380 | flushToilet() 381 | t.ok(true, 'flushed toilet') 382 | process.nextTick(t.end) 383 | }) 384 | --------------------------------------------------------------------------------