├── .github └── FUNDING.yml ├── .gitignore ├── .eslintignore ├── asyncconcat ├── index.js ├── README.md ├── package.json └── test │ └── test.js ├── asyncproxy ├── package.json ├── index.js ├── test │ └── test.js └── README.md ├── LICENSE ├── nocha ├── test │ └── index.js ├── package.json ├── lib │ ├── Runnable.js │ ├── Test.js │ └── Suite.js ├── README.md └── cli.js ├── package.json ├── persistentmap ├── package.json ├── README.md ├── test.js └── index.js ├── README.md ├── jsondiff ├── package.json ├── test │ ├── diff.js │ ├── patch.js │ └── merge.js ├── index.js ├── src │ └── README_js.md └── README.md └── .eslintrc.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [broofa] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw* 2 | .idea/** 3 | .DS_Store 4 | node_modules 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/bootstrap 2 | **/bundle.js 3 | **/dist 4 | **/node_modules 5 | **/vendor 6 | **/swagger-ui 7 | **/viz-lite.js 8 | **/markerclusterer 9 | **/aws-build 10 | -------------------------------------------------------------------------------- /asyncconcat/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function(stream, encoding) { 2 | return new Promise((resolve, reject) => { 3 | const chunks = []; 4 | stream.on('error', err => reject(err)); 5 | stream.on('data', d => chunks.push(d)); 6 | stream.on('end', () => { 7 | const buffer = Buffer.concat(chunks); 8 | resolve(encoding ? buffer.toString(encoding) : buffer); 9 | }); 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /asyncconcat/README.md: -------------------------------------------------------------------------------- 1 | # asyncConcat 2 | 3 | Async method to concatenate a stream into a buffer 4 | 5 | ## Installation 6 | 7 | `npm install @broofa/asyncconcat` 8 | 9 | ## Usage 10 | 11 | ``` 12 | const asyncConcat = require('@broofa/asyncconcat'); 13 | 14 | // Concatenate stream as a String 15 | const asString = await asyncConcat(someStream, 'utf8'); 16 | 17 | // Concatenate stream as a Buffer 18 | const asBuffer = await asyncConcat(someStream); 19 | ``` 20 | -------------------------------------------------------------------------------- /asyncproxy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@broofa/asyncproxy", 3 | "version": "1.0.6", 4 | "description": "A simple, intuitive solution for converting node-style APIs to Promises.", 5 | "main": "index.js", 6 | "funding": ["https://github.com/sponsors/broofa"], 7 | "scripts": { 8 | "prepare": "npm test", 9 | "test": "mocha test/test.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/broofa/BroofaJS/tree/master/asyncproxy" 14 | }, 15 | "keywords": [ 16 | "promisify", 17 | "promisifyAll", 18 | "promiseproxy", 19 | "bluebird" 20 | ], 21 | "author": "", 22 | "license": "ISC", 23 | "devDependencies": { 24 | "mocha": "^5.2.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Robert Kieffer 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /nocha/test/index.js: -------------------------------------------------------------------------------- 1 | it('Scrobble', () => {}); 2 | 3 | describe('All the things', () => { 4 | it('Frabjous day', () => {}); 5 | 6 | it('Lorum Ipsum', () => { 7 | throw Error('Should throw'); 8 | }); 9 | 10 | it('Dolor wat', () => { 11 | throw Error('No, really... just throw'); 12 | }); 13 | 14 | describe('Some of the things', () => { 15 | it('Turkey shoot', () => {}); 16 | }); 17 | 18 | for (let i = 0; i < 5; i++) { 19 | it(`Plunko ${i}`, () => {}); 20 | } 21 | }); 22 | 23 | describe('None of it', () => { 24 | it('Arcturux', () => {}); 25 | }); 26 | 27 | it('Sloooooooow', () => { 28 | return new Promise(resolve => setTimeout(resolve, 4000)); 29 | }); 30 | 31 | it('Netherworld', () => {}); 32 | -------------------------------------------------------------------------------- /asyncconcat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@broofa/asyncconcat", 3 | "version": "1.0.3", 4 | "description": "Async method to concatenate a stream into a buffer", 5 | "main": "index.js", 6 | "funding": ["https://github.com/sponsors/broofa"], 7 | "scripts": { 8 | "test": "mocha test/test.js", 9 | "prepare": "npm test" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/broofa/BroofaJS/tree/master/asyncconcat" 14 | }, 15 | "keywords": [ 16 | "async", 17 | "asynchronous", 18 | "buffer", 19 | "concat", 20 | "promise", 21 | "stream" 22 | ], 23 | "author": "Robert Kieffer", 24 | "license": "ISC", 25 | "devDependencies": { 26 | "mocha": "^5.2.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@broofa/core", 3 | "private": true, 4 | "description": "Misc. JS utilities that may be of broader interest. See subdirectories for documentation", 5 | "version": "1.1.0", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "prepare": "./build/readme.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/broofa/BroofaJS.git" 13 | }, 14 | "author": "Robert Kieffer ", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/broofa/BroofaJS/issues" 18 | }, 19 | "homepage": "https://github.com/broofa/BroofaJS#readme", 20 | "funding": [ 21 | "https://github.com/sponsors/broofa" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /persistentmap/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "persistentmap", 3 | "version": "1.1.5", 4 | "description": "An ES6 Map with Redis-inspired persistence and durability.", 5 | "funding": ["https://github.com/sponsors/broofa"], 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "nocha test.js" 9 | }, 10 | "author": "Robert Kieffer ", 11 | "license": "ISC", 12 | "dependencies": {}, 13 | "devDependencies": { 14 | "nocha": "1.1.1" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://www.github.com/broofa/BroofaJS/tree/master/persistentmap" 19 | }, 20 | "keywords": [ 21 | "ES6", 22 | "Map", 23 | "Redis", 24 | "append", 25 | "only", 26 | "data", 27 | "store" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # BroofaJS 3 | 4 | Misc. JS modules that may be of broader interest. Each module is independent of the others, and 5 | often free from dependencies on other modules. License is typically ISC, but see the respective`package.json` files for detailed info. 6 | 7 | | module | description | 8 | | --- | --- | 9 | | [@broofa/asyncconcat](asyncconcat) | Async method to concatenate a stream into a buffer | 10 | | [@broofa/asyncproxy](asyncproxy) | A simple, intuitive solution for converting node-style APIs to Promises. | 11 | | [@broofa/jsondiff](jsondiff) | Pragmatic, intuitive diffing and patching of JSON objects | 12 | | [nocha](nocha) | A mocha-inspired test utility | 13 | | [persistentmap](persistentmap) | An ES6 Map with Redis-inspired persistence and durability. | 14 | -------------------------------------------------------------------------------- /asyncconcat/test/test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var asyncConcat = require('..'); 3 | var {Readable} = require('stream'); 4 | 5 | describe(__filename, () => { 6 | it('concats', async () => { 7 | const input = 'now is the time for all good men'.split(/ /g, ''); 8 | 9 | // Create a stream that has > 1 chunk of data 10 | const src = new Readable(); 11 | src._read = function(size) { 12 | for (const word of input) { 13 | this.push(word, 'utf8'); 14 | } 15 | this.push(null); 16 | }; 17 | 18 | // Concat the stream as both string and Buffer 19 | const output = await Promise.all([ 20 | asyncConcat(src, 'utf8'), 21 | asyncConcat(src) 22 | ]); 23 | 24 | assert.equal(input.join(''), output[0]); 25 | assert(Buffer.from(input.join('')).equals(output[1])); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /nocha/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nocha", 3 | "version": "1.1.3", 4 | "description": "A mocha-inspired test utility", 5 | "funding": ["https://github.com/sponsors/broofa"], 6 | "main": "lib/index.js", 7 | "exports": { 8 | ".": "./index.cjs" 9 | }, 10 | "scripts": { 11 | "test": "./cli.js test/index.js" 12 | }, 13 | "bin": { 14 | "nocha": "./cli.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/broofa/BroofaJS.git" 19 | }, 20 | "keywords": [ 21 | "async", 22 | "es6", 23 | "testing", 24 | "tests", 25 | "unit" 26 | ], 27 | "author": "", 28 | "license": "ISC", 29 | "bugs": { 30 | "url": "https://github.com/broofa/BroofaJS/issues" 31 | }, 32 | "homepage": "https://github.com/broofa/BroofaJS#readme", 33 | "dependencies": { 34 | "chalk": "3.0.0", 35 | "yargs": "15.1.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /jsondiff/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@broofa/jsondiff", 3 | "version": "1.3.6", 4 | "description": "Pragmatic, intuitive diffing and patching of JSON objects", 5 | "funding": ["https://github.com/sponsors/broofa"], 6 | "main": "index.js", 7 | "dependencies": {}, 8 | "devDependencies": { 9 | "mocha": "^5.2.0", 10 | "runmd": "1.2.1" 11 | }, 12 | "scripts": { 13 | "test": "mocha test/*.js", 14 | "md": "runmd --watch --output=README.md src/README_js.md", 15 | "prepare": "npm test && runmd --output=README.md src/README_js.md" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/broofa/BroofaJS/tree/master/jsondiff" 20 | }, 21 | "keywords": [ 22 | "RFC 6902", 23 | "assign", 24 | "clone", 25 | "deep", 26 | "diff", 27 | "json", 28 | "object", 29 | "patch", 30 | "recursive" 31 | ], 32 | "author": "Robert Kieffer", 33 | "license": "ISC" 34 | } 35 | -------------------------------------------------------------------------------- /nocha/lib/Runnable.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const {argv} = require('yargs'); 3 | 4 | /** 5 | * A "runnable" thing. Can be a Suite or a Test 6 | */ 7 | module.exports = class Test { 8 | constructor(parent, name) { 9 | this.parent = parent; 10 | this.name = name; 11 | } 12 | 13 | shouldSkip() { 14 | const skip = this.id && argv.only && 15 | this.id != argv.only && 16 | !this.id.startsWith(`${argv.only}.`) && 17 | !String(argv.only).startsWith(`${this.id}.`); 18 | return skip; 19 | } 20 | 21 | shouldBreak() { 22 | return argv.break && argv.break == this.id; 23 | } 24 | 25 | get id() { 26 | if (!this.parent) return null; 27 | const parentId = this.parent.id; 28 | const id = this.parent.runnables.indexOf(this) + 1; 29 | return parentId == null ? `${id}` : `${parentId}.${id}`; 30 | } 31 | 32 | get title() { 33 | return this.parent ? `${chalk.dim(this.id)} ${this.name}` : '(top)'; 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /nocha/lib/Test.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const Runnable = require('./Runnable'); 3 | 4 | module.exports = class Test extends Runnable { 5 | constructor(parent, name, testFunc) { 6 | super(parent, name); 7 | this.testFunc = testFunc; 8 | } 9 | 10 | async run(suite) { 11 | if (this.shouldSkip()) return; 12 | 13 | console.log(this.title); 14 | 15 | try { 16 | await this.parent.runHook('beforeEach'); 17 | 18 | await new Promise((resolve, reject) => { 19 | if (this.shouldBreak()) debugger; 20 | // --break users: You can step into your test function by stepping into 21 | // `testFunc` here ... 22 | const p = this.testFunc(); 23 | 24 | if (!p || !p.then) return resolve(p); 25 | 26 | const timer = setTimeout(() => reject(Error('timed out')), 3000); 27 | p 28 | .then(val => { 29 | clearTimeout(timer); 30 | resolve(val); 31 | }) 32 | .catch(err => { 33 | clearTimeout(timer); 34 | reject(err); 35 | }); 36 | }); 37 | 38 | await this.parent.runHook('afterEach'); 39 | } catch (err) { 40 | this.error = err; 41 | console.log( 42 | this.id.replace(/./g, ' '), 43 | chalk.red(err) 44 | ); 45 | } 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /asyncproxy/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function(_target, options) { 2 | const {methodRegex} = {methodRegex: /Async$/, ...options}; 3 | 4 | const memo = new Map(); 5 | 6 | return new Proxy(_target, { 7 | get: function(target, k) { 8 | // Non-string properties (i.e. Symbols) pass thru 9 | if (typeof(k) != 'string') return target[k]; 10 | 11 | // If property isn't in the memo cache ... 12 | if (!memo.has(k)) { 13 | // Extract base method name 14 | const originalMethod = k.replace(methodRegex, ''); 15 | 16 | // If it's different (i.e. matches regex), then promisify 17 | if (k !== originalMethod) { 18 | if (typeof(target[originalMethod]) != 'function') { 19 | throw Error(`${k} can't promisify ${originalMethod} because it's not a function`); 20 | } 21 | 22 | const promisified = function(...args) { 23 | return new Promise(function(resolve, reject) { 24 | target[originalMethod](...args, function(err, ...results) { 25 | if (err) { 26 | reject(err); 27 | } else { 28 | resolve(results.length <= 1 ? results[0] : results); 29 | } 30 | }); 31 | }); 32 | }; 33 | 34 | memo.set(k, promisified); 35 | } else { 36 | memo.set(k, null); 37 | } 38 | } 39 | 40 | return memo.get(k) || target[k]; 41 | } 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /asyncproxy/test/test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var asyncProxy = require('..'); 3 | 4 | const api = { 5 | foo: 'foo', 6 | 7 | passthru(...args) { 8 | const cb = args.pop(); 9 | cb(null, ...args); 10 | }, 11 | 12 | diethru(...args) { 13 | const cb = args.pop(); 14 | cb(Error('I slip thus mortal coil')); 15 | } 16 | }; 17 | 18 | describe(__filename, () => { 19 | it('wraps API', () => { 20 | const papi = asyncProxy(api); 21 | assert.deepStrictEqual(papi, api); 22 | }); 23 | 24 | it('basic promise', async () => { 25 | const papi = asyncProxy(api); 26 | assert.equal(1, await papi.passthruAsync(1)); 27 | }); 28 | 29 | it('complains on non-functions', async () => { 30 | const papi = asyncProxy(api); 31 | try { 32 | await papi.noSuchMethodAsync(); 33 | } catch (err) { 34 | console.error(err); 35 | return; 36 | } 37 | 38 | throw Error('Failed to throw'); 39 | }); 40 | 41 | it('throws', async () => { 42 | const papi = asyncProxy(api); 43 | try { 44 | await papi.diethruAsync(); 45 | } catch (err) { 46 | return; 47 | } 48 | 49 | throw Error('Failed to throw'); 50 | }); 51 | 52 | it('custom regex', async () => { 53 | const papi = asyncProxy(api, {methodRegex: /^a_/}); 54 | assert.deepStrictEqual(1, await papi.a_passthru(1)); 55 | }); 56 | 57 | it('multi-args', async () => { 58 | const papi = asyncProxy(api); 59 | assert.deepStrictEqual([1, 2], await papi.passthruAsync(1,2)); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /nocha/README.md: -------------------------------------------------------------------------------- 1 | # nocha 2 | 3 | An drop-in replacement for [parts of] `mocha` that makes debugging unit tests easier. 4 | 5 | * `--only` option for limiting to a specific test 6 | * `--break` to break immediately before entering a test 7 | * ES6 support (experimental) 8 | 9 | ## Usage - Normal run 10 | ``` 11 | $ nocha test/index.js 12 | test/index.js 13 | 1 Scrobble 14 | 2 All the things 15 | 2.1 Frabjous day 16 | 2.2 Some of the things 17 | 18 | All tests passed! 🎉 19 | ``` 20 | 21 | ## Usage - Run a specific test only 22 | ``` 23 | $ nocha --only=2.1 test/ 24 | test/index.js 25 | 2 All the things 26 | 2.1 Frabjous day 27 | ``` 28 | 29 | ## Usage - Pause debugger at a specified test 30 | ``` 31 | $ nocha --break=2.1 test/ 32 | Debugger listening on ws://127.0.0.1:9229/ab07086f-8e63-401f-8079-d4a58803fcc8 33 | For help, see: https://nodejs.org/en/docs/inspector 34 | 35 | 36 | 37 | Debugger attached. 38 | test/index.js 39 | 1 Scrobble 40 | 2 All the things 41 | 2.1 Frabjous day 42 | 43 | 44 | 45 | 2.2 Some of the things 46 | 47 | All tests passed! 🎉 48 | ``` 49 | 50 | ## Flags 51 | 52 | The flags below refer to step #'s, which precede each task and suite title in 53 | console output. E.g. "1.3 **Validates Email**"'s step number is `1.3`. 54 | 55 | ### --only={step number} 56 | 57 | Run the named step, and only the named step. 58 | 59 | ### --break={step number} 60 | 61 | Waits for debugger to connect then breaks immediately prior to running the 62 | designated step. 63 | 64 | ## ES6 module support (experimental) 65 | 66 | Tests with the `.mjs` extension will be loaded as ES6 modules. These tests must 67 | explicitely import mocha functions as follows: 68 | 69 | ``` 70 | import {describe, it, before, beforeEach, after, afterEach} from 'nocha'; 71 | ``` 72 | -------------------------------------------------------------------------------- /jsondiff/test/diff.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var {diff, DROP, KEEP} = require('..'); 3 | 4 | describe(__filename, async () => { 5 | it('Undefined', async () => { 6 | assert.equal(123, diff(undefined, 123)); 7 | assert.equal(DROP, diff(123, undefined)); 8 | }); 9 | 10 | it('Null', async () => { 11 | assert.equal(KEEP, diff(null, null)); 12 | assert.equal(123, diff(null, 123)); 13 | assert.equal(null, diff(123, null)); 14 | }); 15 | 16 | it('Boolean', async () => { 17 | assert.equal(KEEP, diff(true, true)); 18 | assert.equal(KEEP, diff(false, false)); 19 | assert.equal(false, diff(true, false)); 20 | assert.equal(true, diff(false, true)); 21 | }); 22 | 23 | it('Number', async () => { 24 | assert.equal(KEEP, diff(111, 111)); 25 | assert.equal(222, diff(111, 222)); 26 | }); 27 | 28 | it('String', async () => { 29 | assert.equal(KEEP, diff('aaa', 'aaa')); 30 | assert.equal('bbb', diff('aaa', 'bbb')); 31 | }); 32 | 33 | it('Date', async () => { 34 | const a = new Date(100), b = new Date(100), c = new Date(200); 35 | assert.equal(KEEP, diff(a, b)); 36 | assert.equal(c, diff(a, c)); 37 | }); 38 | 39 | it('Array', async () => { 40 | const a = [1, 'abc', [1,2]]; 41 | const b = [1, 'abc', [1,2]]; 42 | const c = [1, 'abc', true]; 43 | 44 | assert.equal(KEEP, diff(a, b)); 45 | assert.deepEqual([KEEP, KEEP, true], diff(a, c)); 46 | assert.deepEqual([KEEP], diff(a, [1])); 47 | assert.deepEqual([KEEP, KEEP, 3, 4, 5], diff(a, [1,'abc',3,4,5])); 48 | }); 49 | 50 | it('Object', async () => { 51 | const a = {bar: 222, a: 'aaa'}; 52 | const b = {bar: 222, a: 'aaa'}; 53 | const c = {bar: 222, a: 'bbb'}; 54 | 55 | assert.equal(KEEP, diff(a, b)); 56 | assert.deepEqual({a: 'bbb'}, diff(a, c)); 57 | }); 58 | 59 | it('Compound', async () => { 60 | const a = {bar: 111, a: {x: 111}, b: [1, {x:222}, {x: 222}]}; 61 | const b = {bar: 222, b: [4, {x:333}, {x: 222}],}; 62 | 63 | assert.deepEqual({bar: 222, a: DROP, b: [4, {x:333}, KEEP]}, diff(a, b)); 64 | }); 65 | 66 | it('Undefined properties', async () => { 67 | assert.deepEqual(diff({}, {a: undefined}), KEEP); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /asyncproxy/README.md: -------------------------------------------------------------------------------- 1 | # asyncProxy 2 | 3 | A simple, intuitive solution for converting node-style APIs to Promises. 4 | 5 | This module is similar to utilities such as `bluebird.promisifyAll` and 6 | `promiseproxy`. The main difference is that it's behavior is driven by calling 7 | code rather than magical introspection or ad-hoc configuration options. 8 | 9 | Key features ... 10 | 11 | * **Tiny**. No dependencies, ~0.3KB when minified/compressed. 12 | * **Zero API footprint**. Does not modify the original API. 13 | * **Drop-in replacement**. asyncProxy objects are literal proxies for the original API. Treat them exactly as you would the original API. 14 | * **Simple promisification logic**. Just add "Async" to method name. `api.fooAsync()` == `api.foo()`-promisified. 15 | * **Simple multi-args logic**. API callback takes more than one argument? Array destructuring, FTW! 16 | 17 | ## Installation 18 | 19 | You know the drill ... 20 | 21 | ``` 22 | npm i @broofa/asyncproxy 23 | ``` 24 | 25 | ``` javascript 26 | const asyncProxy = require('@broofa/asyncproxy'); 27 | ``` 28 | 29 | ## Example: Promisify `fs.readFile` (basic usage) 30 | 31 | ```javascript 32 | // Wrap api in asyncProxy() 33 | const fs = asyncProxy(require('fs')); 34 | 35 | // An asyncProxy-ified apis is *identical* to the original api 36 | console.log(fs === require('fs')); // ==> true 37 | 38 | // ... but promisifies any method invoked as `${methodName}Async` 39 | const fileContents = await fs.readFileAsync('README.md', 'utf8'); 40 | ``` 41 | 42 | ## Example: Promisify `child.exec` (multiple callback arguments) 43 | 44 | Anytime 2+ arguments are passed to a callback, the Promise resolves to an 45 | argument Array: 46 | 47 | ``` 48 | // Promisified `exec` method 49 | const child_process = asyncProxy(require('child_process')); 50 | 51 | // Use array destructuring to extract result values 52 | let [stdout, stderr] = await execAsync('ls'); 53 | ``` 54 | 55 | ## Example: Custom method name pattern 56 | 57 | Appending `Async` to indicate a promise-returning method is the general 58 | convention, but this may not always be desirable. The `methodRegex` option 59 | is used to detect which methods should be promisified. 60 | 61 | E.g. To use an "async\_"-prefix: 62 | 63 | ``` 64 | const fs = asyncProxy(require('fs'), {methodRegex: /^async_/}); 65 | 66 | const fileContents = await fs.async_readFile('README.md', 'utf8'); 67 | ``` 68 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "commonjs": true, 6 | "es6": true, 7 | "node": true, 8 | "mocha": true 9 | }, 10 | "parserOptions": {"ecmaVersion": 8, "sourceType": "module"}, 11 | "extends": ["eslint:recommended"], 12 | "rules": { 13 | "array-bracket-spacing": ["warn", "never"], 14 | "arrow-body-style": ["warn", "as-needed"], 15 | "arrow-parens": ["warn", "as-needed"], 16 | "arrow-spacing": "warn", 17 | "brace-style": "off", 18 | "camelcase": "warn", 19 | "comma-spacing": ["warn", {"after": true}], 20 | "comma-dangle": ["warn", "only-multiline"], 21 | "dot-notation": "warn", 22 | "func-call-spacing": ["warn", "never"], 23 | "indent": ["warn", 2, { 24 | "SwitchCase": 1, 25 | "FunctionDeclaration": {"parameters": 1}, 26 | "MemberExpression": 1, 27 | "CallExpression": {"arguments": 1} 28 | }], 29 | "key-spacing": ["warn", {"beforeColon": false, "afterColon": true, "mode": "minimum"}], 30 | "keyword-spacing": "warn", 31 | "linebreak-style": ["warn", "unix"], 32 | "lines-around-directive": ["warn", { "before": "never", "after": "always"}], 33 | "max-len": ["off", {"code": 120, "tabWidth": 2, "ignoreTrailingComments": true, "ignoreTemplateLiterals": true}], 34 | "no-console": "off", 35 | "no-constant-condition": "off", 36 | "no-else-return": "warn", 37 | "no-empty": "off", 38 | "no-extra-bind": "warn", 39 | "no-floating-decimal": "warn", 40 | "no-multi-spaces": ["warn", {"ignoreEOLComments": true}], 41 | "no-multiple-empty-lines": ["warn", {"max": 2, "maxBOF": 0, "maxEOF": 0}], 42 | "no-redeclare": "off", 43 | "no-restricted-globals": ["warn"], 44 | "no-trailing-spaces": "warn", 45 | "no-undef": "error", 46 | "no-unused-vars": ["warn", {"args": "none"}], 47 | "no-useless-return": "error", 48 | "no-var": "warn", 49 | "no-whitespace-before-property": "warn", 50 | "object-curly-spacing": ["warn", "never"], 51 | "prefer-const": "warn", 52 | "quotes": ["warn", "single"], 53 | "quote-props": ["warn", "as-needed"], 54 | "require-await": "warn", 55 | "semi": ["warn", "always"], 56 | "semi-spacing": "warn", 57 | "space-before-blocks": ["warn", "always"], 58 | "space-before-function-paren": ["warn", { 59 | "anonymous": "never", 60 | "named": "never", 61 | "asyncArrow": "always" 62 | }], 63 | 64 | "space-in-parens": ["warn", "never"], 65 | "strict": "warn" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /nocha/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {Suite} = require('./lib/Suite'); 4 | const chalk = require('chalk'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const {argv} = require('yargs'); 8 | const {spawn} = require('child_process'); 9 | const inspector = require('inspector'); 10 | 11 | let breakBeforeTest = false; 12 | 13 | async function runFile(filename) { 14 | const suiteName = filename.replace(process.cwd() + '/', ''); 15 | console.log(chalk.bold.underline(suiteName)); 16 | 17 | suite = new Suite(null, suiteName); 18 | const deactivate = suite.activate(); 19 | 20 | // Purge require() cache 21 | for (const k in require.cache) delete require.cache[k]; 22 | 23 | try { 24 | require(filename); 25 | await suite.run(); 26 | } finally { 27 | deactivate(); 28 | } 29 | 30 | return suite; 31 | } 32 | 33 | /** 34 | * Recursive directory search 35 | */ 36 | function flattenPaths(name, accum = []) { 37 | const stat = fs.lstatSync(name); 38 | 39 | if (stat.isFile()) { 40 | // Run the file 41 | accum.push(name); 42 | } else if (stat.isDirectory()) { 43 | // Run each file in the directory 44 | const entries = fs.readdirSync(name) 45 | .filter(e => /\.m?js$/.test(e)) 46 | .map(e => path.join(name, e)); 47 | 48 | for (const entry of entries) { 49 | const estat = fs.lstatSync(entry); 50 | if (estat.isDirectory()) continue; 51 | if (!estat.isFile()) continue; 52 | 53 | accum.push(entry); 54 | } 55 | } 56 | 57 | return accum; 58 | } 59 | 60 | async function main() { 61 | const paths = argv._.reduce((acc, p) => flattenPaths(path.resolve(process.cwd(), p), acc), []); 62 | 63 | const allSuites = new Suite(); 64 | 65 | for (const filepath of paths) { 66 | allSuites.add(await runFile(filepath)); 67 | } 68 | 69 | const errors = allSuites.getErrors(); 70 | 71 | if (!errors.length) { 72 | console.log(chalk.green('All tests passed! \u{1f389}')); 73 | } else { 74 | console.log(chalk.underline('Error Details')); 75 | for (const test of errors) { 76 | console.log(chalk.red.bold(test.title)); 77 | console.error(test.error); 78 | console.log() 79 | } 80 | 81 | console.log(chalk.red.inverse(`${errors.length} failures\u{1f61e}`)); 82 | 83 | process.exit(1); 84 | } 85 | } 86 | 87 | // If a breakpoint is requested, wait for debugger to connect 88 | if (argv.break) inspector.open(undefined, undefined, true); 89 | 90 | // Looks good. Go ahead and run the tests 91 | main(); 92 | -------------------------------------------------------------------------------- /jsondiff/test/patch.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var {diff, patch, value, DROP, KEEP} = require('..'); 3 | 4 | describe(__filename, async () => { 5 | it('value', async () => { 6 | assert.equal(undefined, value(DROP)); 7 | assert.equal(null, value(null)); 8 | assert.equal(123, value(123)); 9 | }); 10 | 11 | it('Undefined', async () => { 12 | assert.equal(123, patch(undefined, 123)); 13 | assert.equal(undefined, patch(123, undefined)); 14 | }); 15 | 16 | it('Null', async () => { 17 | assert.equal(123, patch(null, 123)); 18 | assert.equal(null, patch(123, null)); 19 | }); 20 | 21 | it('Boolean', async () => { 22 | assert.equal(true, patch(true, true)); 23 | assert.equal(false, patch(true, false)); 24 | }); 25 | 26 | it('Number', async () => { 27 | assert.equal(111, patch(111, 111)); 28 | assert.equal(222, patch(111, 222)); 29 | }); 30 | 31 | it('String', async () => { 32 | assert.equal('aaa', patch('aaa', 'aaa')); 33 | assert.equal('bbb', patch('aaa', 'bbb')); 34 | }); 35 | 36 | it('Date', async () => { 37 | const a = new Date(100), b = new Date(100), c = new Date(200); 38 | assert.equal(a, patch(a, b)); 39 | assert.equal(c, patch(a, c)); 40 | }); 41 | 42 | it('Array', async () => { 43 | const a = [1, 'abc', [1,2]]; 44 | const b = [1, 'abc', [1,2]]; 45 | const c = [1, 'abc', true]; 46 | 47 | assert.deepEqual(a, patch(a, b)); 48 | assert.deepEqual([undefined, 1], patch(123, [KEEP,1])); 49 | }); 50 | 51 | it('Object', async () => { 52 | const a = {bar: 222, a: 'aaa'}; 53 | const b = {bar: 222, a: 'aaa'}; 54 | const c = {bar: 222, a: 'bbb'}; 55 | 56 | assert.equal(a, patch(a, b)); 57 | assert.notEqual(c, patch(a, c)); // Has entries from both, so is new object 58 | assert.deepEqual(c, patch(a, c)); 59 | assert.deepEqual({a: 1}, patch({a: 1, b: 2}, {a: 1, b: DROP})); 60 | }); 61 | 62 | it('Compound', async () => { 63 | const a = {bar: 111, a: {x: 111}, b: [1, {x:222}, {x: 222}]}; 64 | const b = {bar: 222, b: [4, {x:333}, {x: 222}],}; 65 | 66 | assert.deepEqual( 67 | patch(a, b), 68 | {bar: 222, a: {x: 111}, b: [4, {x:333}, {x: 222}]} 69 | ); 70 | }); 71 | 72 | it('diff-patch', async () => { 73 | const a = { 74 | bar: 111, 75 | a: {x: 111}, 76 | b: [1, {x:222}, {x: 222}], 77 | c: 333, 78 | f: [1,2,5,6], 79 | ff: [1,2,6], 80 | }; 81 | const b = { 82 | bar: 222, 83 | b: [4, {x:333}, {x: 222}], 84 | d: 444, 85 | f: [1,2,6], 86 | ff: [1,2,5,6], 87 | }; 88 | 89 | const d = JSON.parse(JSON.stringify(diff(a, b))); 90 | assert.deepEqual(patch(a, diff(a, b)), b); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /jsondiff/test/merge.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var {merge} = require('..'); 3 | 4 | describe(__filename, async () => { 5 | it('Undefined', async () => { 6 | //assert.equal(123, merge(undefined, 123)); 7 | assert.equal(undefined, merge(123, undefined)); 8 | }); 9 | 10 | it('Null', async () => { 11 | assert.equal(123, merge(null, 123)); 12 | assert.equal(null, merge(123, null)); 13 | }); 14 | 15 | it('Boolean', async () => { 16 | assert.equal(true, merge(true, true)); 17 | assert.equal(false, merge(true, false)); 18 | }); 19 | 20 | it('Number', async () => { 21 | assert.equal(111, merge(111, 111)); 22 | assert.equal(222, merge(111, 222)); 23 | }); 24 | 25 | it('String', async () => { 26 | assert.equal('aaa', merge('aaa', 'aaa')); 27 | assert.equal('bbb', merge('aaa', 'bbb')); 28 | }); 29 | 30 | it('Date', async () => { 31 | const a = new Date(100), b = new Date(100), c = new Date(200); 32 | assert.equal(a, merge(a, b)); 33 | assert.equal(c, merge(a, c)); 34 | }); 35 | 36 | it('Array', async () => { 37 | const a = [1, 'abc', [1,2]]; 38 | const b = [1, 'abc', [1,2]]; 39 | const c = [1, 'abc', true]; 40 | 41 | assert.equal(a, merge(a, b)); // Identical, should return before 42 | assert.notEqual(c, merge(a, c)); // Has entries from both, so is new array 43 | assert.deepEqual(c, merge(a, c)); 44 | }); 45 | 46 | it('Object', async () => { 47 | const a = {bar: 222, a: 'aaa'}; 48 | const b = {bar: 222, a: 'aaa'}; 49 | const c = {bar: 222, a: 'bbb'}; 50 | 51 | assert.equal(a, merge(a, b)); 52 | assert.notEqual(c, merge(a, c)); // Has entries from both, so is new object 53 | assert.deepEqual(c, merge(a, c)); 54 | }); 55 | 56 | it('Compound', async () => { 57 | const a = {bar: 111, a: {x: 111}, b: [1, {x:222}, {x: 222}]}; 58 | const b = {bar: 222, b: [4, {x:333}, {x: 222}],}; 59 | 60 | assert.equal(a.b[2], merge(a, b).b[2]); 61 | assert.deepEqual(b, merge(a, b)); 62 | }); 63 | 64 | it('README', async () => { 65 | const before = { 66 | a: 'hello', 67 | b: 123, 68 | c: {ca: ['zig'], cb: [{a:1}, {b:2}]}, 69 | }; 70 | 71 | const after = { 72 | a: 'world', 73 | b: 123, 74 | c: {ca: ['zig'], cb: [{a:99}, {b:2}]}, 75 | }; 76 | 77 | const state = merge(before, after); 78 | 79 | // Where state HAS changed 80 | assert.notEqual(state, before); 81 | assert.notEqual(state.c, before.c); 82 | assert.notEqual(state.c.cb, before.c.cb); 83 | assert.notEqual(state.c.cb[0], before.c.cb[0]); 84 | 85 | // Where state HAS NOT changed 86 | assert.equal(state.c.ca, before.c.ca); 87 | assert.equal(state.c.cb[1], before.c.cb[1]); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /persistentmap/README.md: -------------------------------------------------------------------------------- 1 | # PersistentMap 2 | 3 | An ES6 Map with Redis-inspired persistence and durability 4 | 5 | Features: 6 | * Compatible - Drop-in replacement for ES6 Map 7 | * Performant - 100K's writes/second sustained 8 | * Reliable - Append-only transaction file, atomic file operations 9 | * Sweet and simple - Zero dependencies, pure JS, < 1KB of code 10 | 11 | **What's this for?** PersitentMap is a lightweight, in-process data store that 12 | uses time-tested principles for insuring data-integrity. In short, if you want 13 | to retain metrics and analytics counters, event history, app preferences ... 14 | whatever, but don't want the hassle of setting up MySQL or Redis, this might be 15 | what you're after. 16 | 17 | ## Quick Start 18 | 19 | Install: 20 | 21 | ``` 22 | npm install persistentmap 23 | ``` 24 | 25 | Use: 26 | ``` 27 | // Create a map (and tell it where to save state on disk) 28 | const pm = new PersistentMap('~/datastore.json'); 29 | 30 | // Load any prior state 31 | await pm.load(); 32 | 33 | // Treat it like a Map 34 | pm.set('foo', 123); 35 | pm.get('foo'); // -> 123 36 | 37 | // If you want to verify state has been saved before proceeding 38 | // await the result 39 | await pm.set('foo', 345); 40 | 41 | // 'foo' is now saved to disk. If/when the process dies, restarting it 42 | // will restore 'foo' when the map is load()'ed, above 43 | ``` 44 | 45 | ## Performance 46 | 47 | Unless you're doing 10K's or 100Ks of `set()` or `delete()` calls per second, PersistentMap performance should not be an issue. Methods that change the map state(`set()`, `delete()`, and `clear()`), if await'ed, will be fast, but "file system fast", not "in process memory" fast, and depend to some extent on how much data you're storing. In these cases, the [Big O](https://en.wikipedia.org/wiki/Big_O_notation) performance will be: 48 | 49 | * `set(key, val)`: O(N) , where N = `JSON.stringify(val).length` 50 | * `delete(key)`: O(1) 51 | * `clear()`: O(1) 52 | 53 | For all other methods, performance should be indistuingishable from a native 54 | Map. 55 | 56 | ***Note***: the `set()` and `delete()` operations may occasionally trigger a 57 | full-state rewrite (this occurs when filesize > `options.maxFileSize`), in which case 58 | performance will be O(N), where N = `JSON.stringify(map).length` 59 | 60 | ## API 61 | 62 | PersistentMap extends the ES6 Map class. It provides the full [ES6 Map API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map), with the following changes: 63 | 64 | * New constructor signature, documented below 65 | * Existing `clear()`, `delete()`, and `set()` methods enhanced to return a 66 | `Promise` that resolves when state has been successfully saved to file (or 67 | rejects on error). 68 | * New `compact()`, `flush()`, and `load()` methods, documented below 69 | 70 | ### constructor(filepath, options) 71 | 72 | * `filepath` - Location of transaction file. This will be created if it does not 73 | exist. Note: PersistentMap may occasionally create a temporary file at `${filepath}.tmp`, as well. 74 | * `options.maxFileSize` - Size (bytes) at which to compact the transaction file. 75 | Default = 1,000,000. 76 | 77 | ### `async` compact()` 78 | Saves current state of map to the transaction file. 79 | 80 | ### `async` flush() 81 | Wait for all pending writes to complete before resolving. 82 | 83 | ### `async` load() 84 | Load map state from transaction file. 85 | -------------------------------------------------------------------------------- /nocha/lib/Suite.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const Runnable = require('./Runnable'); 3 | const Test = require('./Test'); 4 | const util = require('util'); 5 | const {argv} = require('yargs'); 6 | 7 | let currentSuite; 8 | 9 | class Suite extends Runnable { 10 | _hooks = {}; 11 | runnables = []; 12 | 13 | constructor(parent = null, name = null) { 14 | super(parent, name); 15 | } 16 | 17 | set before(v) {this._hooks.before = v;} 18 | set beforeEach(v) {this._hooks.beforeEach = v;} 19 | set after(v) {this._hooks.after = v;} 20 | set afterEach(v) {this._hooks.afterEach = v;} 21 | 22 | async runHook(hookName) { 23 | if (!(hookName in this._hooks)) return; 24 | try { 25 | return await this._hooks[hookName].apply(null); 26 | } catch (err) { 27 | // Coerce to error so we can extend with hook name 28 | if (!(err instanceof Error)) { 29 | err = new Error(err); 30 | delete err.stack; 31 | } 32 | 33 | err.message = `In ${hookName}(), "${err.message}"`; 34 | 35 | throw err; 36 | } 37 | } 38 | 39 | add(runnable) { 40 | this.runnables.push(runnable); 41 | } 42 | 43 | async run() { 44 | if (this.shouldSkip()) return; 45 | 46 | if (this.parent) console.log(`${chalk.bold(this.title)}`); 47 | 48 | const deactivate = this.activate(); 49 | try { 50 | await this.runHook('before'); 51 | 52 | if (this.shouldBreak()) debugger; 53 | // --break users: You can step through this suite's tests in this 54 | // loop ... 55 | for (const runnable of this.runnables) { 56 | await runnable.run(this); 57 | } 58 | 59 | console.log(); 60 | 61 | await this.runHook('after'); 62 | } finally { 63 | deactivate(); 64 | } 65 | } 66 | 67 | getErrors(errors = []) { 68 | for (const runnable of this.runnables) { 69 | if (runnable instanceof Test) { 70 | if (runnable.error) errors.push(runnable); 71 | } else { 72 | runnable.getErrors(errors); 73 | } 74 | } 75 | 76 | return errors; 77 | } 78 | 79 | activate() { 80 | const self = this; 81 | 82 | const before = {...global}; 83 | 84 | const GLOBALS = { 85 | before(cb) {self.before = cb;}, 86 | beforeEach(cb) {self.beforeEach = cb;}, 87 | 88 | after(cb) {self.after = cb;}, 89 | afterEach(cb) {self.afterEach = cb;}, 90 | 91 | async describe(name, suiteFunc) { 92 | const suite = new Suite(self, name); 93 | 94 | self.add(suite); 95 | 96 | const deactivate = suite.activate(); 97 | 98 | try { 99 | // Invoke suite setup function 100 | const p = suiteFunc(); 101 | if (p && p.then) await(p); 102 | } catch (err) { 103 | console.error(err); 104 | process.exit(1); 105 | } finally { 106 | deactivate(); 107 | } 108 | }, 109 | 110 | async it(...args) { 111 | self.add(new Test(self, ...args)); 112 | }, 113 | 114 | log(...args) { 115 | const out = util.inspect(...args).replace(/^/mg, ' '); 116 | console.log(chalk.dim(out)); 117 | } 118 | }; 119 | 120 | // Suite-module globals 121 | Object.assign(global, GLOBALS); 122 | 123 | return () => { 124 | for (const k of Object.keys(global)) { 125 | if (k in before) { 126 | global[k] = before[k]; 127 | } else { 128 | delete global[k]; 129 | } 130 | } 131 | }; 132 | } 133 | }; 134 | 135 | exports.Suite = Suite; 136 | -------------------------------------------------------------------------------- /persistentmap/test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const {promises: fs} = require('fs'); 3 | const PersistentMap = require('./index.js'); 4 | 5 | const FILE = '/tmp/test_map.json'; 6 | 7 | describe(__filename, () => { 8 | beforeEach(async () => { 9 | // Probably-futile attempt to idiot-proof this code 10 | assert(/^\/tmp/.test(FILE)); 11 | 12 | try { 13 | await fs.unlink(FILE); 14 | } catch (err) { 15 | if (err.code != 'ENOENT') { 16 | throw err; 17 | } 18 | } 19 | }); 20 | 21 | it('ES6 API', async () => { 22 | const pm = new PersistentMap(FILE); 23 | await pm.clear(); 24 | await pm.set('x', 123); 25 | await pm.set('y', 456); 26 | 27 | assert(pm instanceof Map, 'is instance of Map'); 28 | 29 | assert.equal(pm.size, 2); 30 | assert.equal(pm.get('x'), 123); 31 | assert.equal(pm.get('y'), 456); 32 | assert.deepEqual([...pm.keys()].sort(), ['x', 'y']); 33 | assert.deepEqual([...pm.values()].sort(), [123, 456]); 34 | assert.deepEqual([...pm.entries()].sort(), [['x', 123], ['y', 456]]); 35 | assert.deepEqual([...pm].sort(), [['x', 123], ['y', 456]]); // Iterable (same as entries()) 36 | }); 37 | 38 | it('set() & clear()', async () => { 39 | const pm = new PersistentMap(FILE); 40 | await assert.rejects(() => fs.stat(pm.filepath), 'File removed'); 41 | 42 | await pm.set('x', 123); 43 | await assert.doesNotReject(() => fs.stat(pm.filepath), 'set() created file'); 44 | 45 | await pm.clear(); 46 | assert.equal(pm.size, 0); 47 | 48 | await assert.rejects(() => fs.stat(pm.filepath), 'clear() deleted file'); 49 | }); 50 | 51 | it('clear()', async () => { 52 | const pm = new PersistentMap(FILE); 53 | 54 | await assert.rejects(() => fs.stat(pm.filepath), 'File removed'); 55 | 56 | await pm.set('x', 123); 57 | await assert.doesNotReject(() => fs.stat(pm.filepath), 'set() created file'); 58 | 59 | await pm.clear(); 60 | assert.equal(pm.size, 0); 61 | 62 | await assert.rejects(() => fs.stat(pm.filepath), 'clear() deleted file'); 63 | }); 64 | 65 | it('load() & compact()', async() => { 66 | // Size here chosen to trigger a compact a few actions the end of our 67 | // for-loop, below, so if the compact-load logic it will result in an 68 | // incomplete restore of the state (and trigger an error) 69 | const pm = new PersistentMap(FILE, {maxFileSize: 43000}); 70 | 71 | // Write enough state to trigger a compact() 72 | let val = ''; 73 | for (let i = 0; i < 1000; i++) { 74 | const key = `key_${i % 100}`; 75 | val += String.fromCharCode(32 + (i % 60)); 76 | val = val.replace(/.* /, ''); 77 | pm.set(key, val); 78 | } 79 | 80 | // Wait for it to write to file 81 | await pm.flush(); 82 | 83 | // Snapshot what the map has 84 | const obj = Object.fromEntries(pm.entries()); 85 | 86 | // Create and load another map 87 | const pm2 = new PersistentMap(FILE); 88 | await pm2.load(); 89 | 90 | // Verify states are the same 91 | assert.deepEqual( 92 | Object.fromEntries(pm.entries()), 93 | Object.fromEntries(pm2.entries()) 94 | ); 95 | 96 | /* 97 | await pm.compact(); 98 | const json = await fs.readFile(pm.filepath); 99 | 100 | assert.deepEqual(JSON.parse(json),[null, {x: 123, y: 456, foo: 'abc', bar: 'def'}]); 101 | */ 102 | }); 103 | 104 | it('perf', async () => { 105 | const pm = new PersistentMap(FILE, {maxFileSize: 100e3}); 106 | await pm.clear(); 107 | 108 | await new Promise(resolve => { 109 | const start = Date.now(); 110 | let n = 0; 111 | function writeSome() { 112 | for (let i = 0; i < 100; i++, n++) { 113 | const k = n & 0xff; 114 | pm.set(String(k), n); 115 | } 116 | 117 | // Generate 118 | if (Date.now() - start < 1000) { 119 | setImmediate(writeSome, 0); 120 | } else { 121 | console.log(`${(1e3 * n / (Date.now() - start)).toFixed(0)} writes / second`); 122 | resolve(); 123 | } 124 | } 125 | writeSome(); 126 | }); 127 | 128 | await pm.flush(); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /jsondiff/index.js: -------------------------------------------------------------------------------- 1 | // Reserved values 2 | const DROP = '\uE796-'; // Delete value 3 | const KEEP = '\uE796+'; // Keep value 4 | 5 | /** 6 | * Normalize a patch value by converting DROP values to undefined. This is 7 | * useful for doing code such as `if (jsondiff.value(patch.someValue)) ...` 8 | * 9 | * @param {any} value 10 | */ 11 | function value(val) { 12 | return val === DROP ? undefined : val; 13 | } 14 | 15 | /** 16 | * Generate a patch object that describes the difference between two states 17 | * 18 | * @param {any} before 19 | * @param {any} after 20 | * 21 | * @returns {any} Patch object as described in the README 22 | */ 23 | function diff(before, after) { 24 | if (after === undefined) return DROP; 25 | if (after === null) return before === null ? KEEP : null; 26 | if (before == null) return after; 27 | 28 | // Different types 29 | if (before.constructor.name !== after.constructor.name) return after; 30 | 31 | let type = after.constructor.name; 32 | switch (type) { 33 | case 'Boolean': 34 | case 'Number': 35 | case 'String': 36 | return before === after ? KEEP : after; 37 | 38 | case 'Date': // Not strictly JSON but useful 39 | return before.getTime() === after.getTime() ? KEEP : after; 40 | 41 | case 'Object': { 42 | let isEqual = true; 43 | const values = {}; 44 | for (const k of new Set([...Object.keys(before), ...Object.keys(after)])) { 45 | const bv = before[k], av = after[k]; 46 | if (av === undefined && !(k in before)) continue; 47 | const val = diff(bv, av); 48 | if (val === KEEP) continue; 49 | 50 | values[k] = val; 51 | isEqual = false; 52 | } 53 | 54 | return isEqual ? KEEP : values; 55 | } 56 | 57 | case 'Array': { 58 | let isEqual = before.length === after.length; 59 | const values = new Array(after.length); 60 | for (let i = 0, l = values.length; i < l; i++) { 61 | values[i] = diff(before[i], after[i]); 62 | if (values[i] !== KEEP) isEqual = false; 63 | } 64 | 65 | return isEqual ? KEEP : values; 66 | } 67 | 68 | default: 69 | throw Error(`Unexpected type: ${type}`); 70 | } 71 | }; 72 | 73 | /** 74 | * Apply a patch object to some 'before' state and return the 'after' state 75 | * 76 | * @param {any} before 77 | * @param {any} _patch 78 | * 79 | * @returns {any} The mutated state 80 | */ 81 | function patch(before, _patch) { 82 | if (_patch === DROP) return undefined; 83 | if (_patch === KEEP) _patch = before; 84 | if (_patch == null) return _patch; 85 | 86 | if (before === _patch) return before; 87 | 88 | const beforeType = before == null ? 'null' : before.constructor.name; 89 | const type = _patch.constructor.name; 90 | 91 | if (beforeType !== type) { 92 | switch (type) { 93 | case 'Object': before = {}; break; 94 | case 'Array': before = []; break; 95 | default: return _patch; 96 | } 97 | } 98 | 99 | switch (type) { 100 | case 'Boolean': 101 | case 'Number': 102 | case 'String': 103 | case 'Symbol': 104 | break; 105 | case 'Date': // Not strictly JSON but useful 106 | if (before.getTime() == _patch.getTime()) _patch = before; 107 | break; 108 | 109 | case 'Object': { 110 | let isEqual = true; 111 | const values = {...before}; 112 | for (const k in _patch) { 113 | if (value(_patch[k]) === undefined) { 114 | if (k in values) { 115 | delete values[k]; 116 | isEqual = false; 117 | } 118 | } else { 119 | const val = patch(before[k], _patch[k]); 120 | if (val !== before[k]) { 121 | values[k] = val; 122 | isEqual = false; 123 | } 124 | } 125 | } 126 | 127 | _patch = isEqual ? before : values; 128 | break; 129 | } 130 | 131 | case 'Array': { 132 | const values = new Array(_patch.length); 133 | let isEqual = before.length === _patch.length; 134 | for (let i = 0, l = _patch.length; i < l; i++) { 135 | const val = patch(before[i], _patch[i]); 136 | 137 | if (val !== before[i]) isEqual = false; 138 | values[i] = val; 139 | } 140 | _patch = isEqual ? before : values; 141 | break; 142 | } 143 | 144 | default: 145 | throw Error(`Unexpected type: ${type}`); 146 | } 147 | return before === _patch ? before : _patch; 148 | }; 149 | 150 | module.exports = { 151 | DROP, 152 | KEEP, 153 | diff, 154 | patch, 155 | value, 156 | merge(before, after) {return patch(before, diff(before, after));} 157 | }; 158 | -------------------------------------------------------------------------------- /jsondiff/src/README_js.md: -------------------------------------------------------------------------------- 1 | ```javascript --hide --run usage 2 | runmd.onRequire = path => path.replace(/^@broofa\/\w+/, '..'); 3 | const assert = require('assert'); 4 | 5 | const before = { 6 | name: 'my object', 7 | description: 'it\'s an object!', 8 | details: { 9 | it: 'has', 10 | an: 'array', 11 | with: ['a', 'few', 'elements'] 12 | } 13 | }; 14 | 15 | const after = { 16 | name: 'updated object', 17 | title: 'it\'s an object!', 18 | details: { 19 | it: 'has', 20 | an: 'array', 21 | with: ['a', 'few', 'more', 'elements', { than: 'before' }] 22 | } 23 | }; 24 | 25 | const deepPatch = [ 26 | {kind: 'E', path: ['name'], lhs: 'my object', rhs: 'updated object'}, 27 | {kind: 'D', path: ['description'], lhs: 'it\'s an object!'}, 28 | {kind: 'A', path: ['details', 'with'], index: 4, item: {kind: 'N', rhs: [Object]}}, 29 | {kind: 'A', path: ['details', 'with'], index: 3, item: {kind: 'N', rhs: 'elements'}}, 30 | {kind: 'E', path: ['details', 'with', 2], lhs: 'elements', rhs: 'more' }, 31 | {kind: 'N', path: ['title'], rhs: 'it\'s an object!'} 32 | ]; 33 | 34 | const rfcPatch = [ 35 | {op: 'remove', path: '/description'}, 36 | {op: 'add', path: '/title', value: 'it\'s an object!'}, 37 | {op: 'replace', path: '/name', value: 'updated object'}, 38 | {op: 'add', path: '/details/with/2', value: 'more'}, 39 | {op: 'add', path: '/details/with/-', value: {than: 'before'}} 40 | ]; 41 | 42 | ``` 43 | 44 | # @broofa/jsondiff 45 | 46 | Pragmatic and intuitive diff and patch functions for JSON data 47 | 48 | ## Installation 49 | 50 | `npm install @broofa/jsondiff` 51 | 52 | ## Usage 53 | 54 | Require it: 55 | 56 | ```javascript --run usage 57 | const jsondiff = require('@broofa/jsondiff'); 58 | 59 | // ... or ES6 module style: 60 | // import jsondiff from '@broofa/jsondiff'; 61 | ``` 62 | 63 | Start with some `before` and `after` state: 64 | ```javascript --run usage 65 | console.log(before); 66 | ``` 67 | 68 | ```javascript --run usage 69 | console.log(after); 70 | ``` 71 | 72 | Create a patch that descibes the difference between the two: 73 | ```javascript --run usage 74 | const patch = jsondiff.diff(before, after); 75 | console.log(patch); 76 | ``` 77 | *(Note the special DROP and KEEP values ("-" and "+")! These are explained in **Patch Objects**, below.)* 78 | 79 | Apply `patch` to the before state to reproduce the `after` state: 80 | ```javascript --run usage 81 | const patched = jsondiff.patch(before, patch); 82 | console.log(patched); 83 | ``` 84 | 85 | ## Why yet-another diff module? 86 | 87 | There are already several modules in this space - `deep-diff`, `rfc6902`, or `fast-json-patch`, to name a few. `deep-diff` is the most popular, however `rfc6902` is (to my mind) the most compelling because it will interoperate with other libraries that support [RFC6902 standard](https://tools.ietf.org/html/rfc6902). 88 | 89 | However ... the patch formats used by these modules tends to be cryptic and overly verbose - 90 | a list of the mutations needed to transform between the two states. In the case 91 | of `deep-diff` you end up with this patch: 92 | 93 | ```javascript --run usage 94 | console.log(deepPatch); 95 | ``` 96 | 97 | And for `rfc6902`: 98 | 99 | ```javascript --run usage 100 | console.log(rfcPatch); 101 | ``` 102 | 103 | The advantage(?) of this module is that the patch structure mirrors the 104 | structure of the target data. As such, it terse, readable, and resilient. 105 | 106 | That said, this module may not be for everyone. In particular, readers may find 107 | the DROP and KEEP values (described below) to be... "interesting". 108 | 109 | 110 | ## API 111 | 112 | ### jsondiff.diff(before, after) 113 | 114 | Creates and returns a "patch object" that describes the differences between 115 | `before` and `after`. This object is suitable for use in `patch()`. 116 | 117 | ### jsondiff.patch(before, patch) 118 | 119 | Applies a `patch` object to `before` and returns the result. 120 | 121 | Note: Any result value that is deep-equal to it's `before` counterpart will 122 | reference the 'before' value directly, allowing `===` to be used as a test 123 | for deep equality. 124 | 125 | ### jsondiff.value(val) 126 | 127 | Normalize patch values. Currently this just converts `DROP` values to 128 | `undefined`, otherwise returns the value. This is useful in determining if a 129 | patch has a meaningful value. E.g. 130 | 131 | ```javascript --run usage 132 | const newPatch = {foo: jsondiff.DROP, bar: 123}; 133 | 134 | newPatch.foo; // RESULT 135 | jsondiff.value(newPatch.foo); // RESULT 136 | jsondiff.value(newPatch.bar); // RESULT 137 | jsondiff.value(newPatch.whups); // RESULT 138 | ``` 139 | 140 | ## Patch Objects 141 | 142 | Patch objects are JSON objects with the same structure (schema) as the object 143 | they apply to. Applying a patch is (almost) as simple as doing a deep copy of 144 | the patch onto the target object. There are two special cases: 145 | 146 | 1. `jsondiff.DROP` ("`-`") values are "dropped" (deleted or set 147 | to `undefined`) 148 | 2. `jsondiff.KEEP` ("`+`") values are "kept" (resolve to the corresponding 149 | value in the target) 150 | 151 | Note: `DROP` and `KEEP` are, admittedly, a hack. If these exact string values 152 | appear in data outside of patch objects, `diff()` and `patch()` may not function 153 | correctly. That said, this is not expected to be an issue in real-world 154 | conditions. (Both strings include a "private use" Unicode character that should 155 | make them fairly unique.) 156 | -------------------------------------------------------------------------------- /jsondiff/README.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # @broofa/jsondiff 6 | 7 | Pragmatic and intuitive diff and patch functions for JSON data 8 | 9 | ## Installation 10 | 11 | `npm install @broofa/jsondiff` 12 | 13 | ## Usage 14 | 15 | Require it: 16 | 17 | ```javascript 18 | const jsondiff = require('@broofa/jsondiff'); 19 | 20 | // ... or ES6 module style: 21 | // import jsondiff from '@broofa/jsondiff'; 22 | 23 | ``` 24 | 25 | Start with some `before` and `after` state: 26 | ```javascript 27 | console.log(before); 28 | 29 | ⇒ { 30 | ⇒ name: 'my object', 31 | ⇒ description: "it's an object!", 32 | ⇒ details: { it: 'has', an: 'array', with: [ 'a', 'few', 'elements' ] } 33 | ⇒ } 34 | ``` 35 | 36 | ```javascript 37 | console.log(after); 38 | 39 | ⇒ { 40 | ⇒ name: 'updated object', 41 | ⇒ title: "it's an object!", 42 | ⇒ details: { 43 | ⇒ it: 'has', 44 | ⇒ an: 'array', 45 | ⇒ with: [ 'a', 'few', 'more', 'elements', { than: 'before' } ] 46 | ⇒ } 47 | ⇒ } 48 | ``` 49 | 50 | Create a patch that descibes the difference between the two: 51 | ```javascript 52 | const patch = jsondiff.diff(before, after); 53 | console.log(patch); 54 | 55 | ⇒ { 56 | ⇒ name: 'updated object', 57 | ⇒ description: '-', 58 | ⇒ details: { with: [ '+', '+', 'more', 'elements', { than: 'before' } ] }, 59 | ⇒ title: "it's an object!" 60 | ⇒ } 61 | ``` 62 | *(Note the special DROP and KEEP values ("-" and "+")! These are explained in **Patch Objects**, below.)* 63 | 64 | Apply `patch` to the before state to reproduce the `after` state: 65 | ```javascript 66 | const patched = jsondiff.patch(before, patch); 67 | console.log(patched); 68 | 69 | ⇒ { 70 | ⇒ name: 'updated object', 71 | ⇒ details: { 72 | ⇒ it: 'has', 73 | ⇒ an: 'array', 74 | ⇒ with: [ 'a', 'few', 'more', 'elements', { than: 'before' } ] 75 | ⇒ }, 76 | ⇒ title: "it's an object!" 77 | ⇒ } 78 | ``` 79 | 80 | ## Why yet-another diff module? 81 | 82 | There are already several modules in this space - `deep-diff`, `rfc6902`, or `fast-json-patch`, to name a few. `deep-diff` is the most popular, however `rfc6902` is (to my mind) the most compelling because it will interoperate with other libraries that support [RFC6902 standard](https://tools.ietf.org/html/rfc6902). 83 | 84 | However ... the patch formats used by these modules tends to be cryptic and overly verbose - 85 | a list of the mutations needed to transform between the two states. In the case 86 | of `deep-diff` you end up with this patch: 87 | 88 | ```javascript 89 | console.log(deepPatch); 90 | 91 | ⇒ [ 92 | ⇒ { 93 | ⇒ kind: 'E', 94 | ⇒ path: [ 'name' ], 95 | ⇒ lhs: 'my object', 96 | ⇒ rhs: 'updated object' 97 | ⇒ }, 98 | ⇒ { kind: 'D', path: [ 'description' ], lhs: "it's an object!" }, 99 | ⇒ { 100 | ⇒ kind: 'A', 101 | ⇒ path: [ 'details', 'with' ], 102 | ⇒ index: 4, 103 | ⇒ item: { kind: 'N', rhs: [ [Function: Object] ] } 104 | ⇒ }, 105 | ⇒ { 106 | ⇒ kind: 'A', 107 | ⇒ path: [ 'details', 'with' ], 108 | ⇒ index: 3, 109 | ⇒ item: { kind: 'N', rhs: 'elements' } 110 | ⇒ }, 111 | ⇒ { 112 | ⇒ kind: 'E', 113 | ⇒ path: [ 'details', 'with', 2 ], 114 | ⇒ lhs: 'elements', 115 | ⇒ rhs: 'more' 116 | ⇒ }, 117 | ⇒ { kind: 'N', path: [ 'title' ], rhs: "it's an object!" } 118 | ⇒ ] 119 | ``` 120 | 121 | And for `rfc6902`: 122 | 123 | ```javascript 124 | console.log(rfcPatch); 125 | 126 | ⇒ [ 127 | ⇒ { op: 'remove', path: '/description' }, 128 | ⇒ { op: 'add', path: '/title', value: "it's an object!" }, 129 | ⇒ { op: 'replace', path: '/name', value: 'updated object' }, 130 | ⇒ { op: 'add', path: '/details/with/2', value: 'more' }, 131 | ⇒ { op: 'add', path: '/details/with/-', value: { than: 'before' } } 132 | ⇒ ] 133 | ``` 134 | 135 | The advantage(?) of this module is that the patch structure mirrors the 136 | structure of the target data. As such, it terse, readable, and resilient. 137 | 138 | That said, this module may not be for everyone. In particular, readers may find 139 | the DROP and KEEP values (described below) to be... "interesting". 140 | 141 | 142 | ## API 143 | 144 | ### jsondiff.diff(before, after) 145 | 146 | Creates and returns a "patch object" that describes the differences between 147 | `before` and `after`. This object is suitable for use in `patch()`. 148 | 149 | ### jsondiff.patch(before, patch) 150 | 151 | Applies a `patch` object to `before` and returns the result. 152 | 153 | Note: Any result value that is deep-equal to it's `before` counterpart will 154 | reference the 'before' value directly, allowing `===` to be used as a test 155 | for deep equality. 156 | 157 | ### jsondiff.value(val) 158 | 159 | Normalize patch values. Currently this just converts `DROP` values to 160 | `undefined`, otherwise returns the value. This is useful in determining if a 161 | patch has a meaningful value. E.g. 162 | 163 | ```javascript 164 | const newPatch = {foo: jsondiff.DROP, bar: 123}; 165 | 166 | newPatch.foo; // ⇨ '-' 167 | jsondiff.value(newPatch.foo); // ⇨ undefined 168 | jsondiff.value(newPatch.bar); // ⇨ 123 169 | jsondiff.value(newPatch.whups); // ⇨ undefined 170 | 171 | ``` 172 | 173 | ## Patch Objects 174 | 175 | Patch objects are JSON objects with the same structure (schema) as the object 176 | they apply to. Applying a patch is (almost) as simple as doing a deep copy of 177 | the patch onto the target object. There are two special cases: 178 | 179 | 1. `jsondiff.DROP` ("`-`") values are "dropped" (deleted or set 180 | to `undefined`) 181 | 2. `jsondiff.KEEP` ("`+`") values are "kept" (resolve to the corresponding 182 | value in the target) 183 | 184 | Note: `DROP` and `KEEP` are, admittedly, a hack. If these exact string values 185 | appear in data outside of patch objects, `diff()` and `patch()` may not function 186 | correctly. That said, this is not expected to be an issue in real-world 187 | conditions. (Both strings include a "private use" Unicode character that should 188 | make them fairly unique.) 189 | 190 | ---- 191 | Markdown generated from [src/README_js.md](src/README_js.md) by [![RunMD Logo](http://i.imgur.com/h0FVyzU.png)](https://github.com/broofa/runmd) -------------------------------------------------------------------------------- /persistentmap/index.js: -------------------------------------------------------------------------------- 1 | const {promises: fs} = require('fs'); 2 | const path = require('path'); 3 | 4 | const CLEAR = Symbol('clear file'); 5 | 6 | /** 7 | * An ES6 Map, lazily persisted to an append-only transaction file, so it's 8 | * state can be restored, even if the process terminates unexpectedly. 9 | * 10 | * Write actions (set, delete, clear) are applied 11 | * immediately, but are saved async. Caller's wishing to insure a write action 12 | * is persisted should await the action before continuing. 13 | * 14 | * The transaction file format is a newline-separated list of JSON arrays 15 | * describing actions to apply to the map. Each action is a 1 or 2 element 16 | * array as follows: 17 | * 18 | * ["key"] = Delete "key" from map 19 | * ["key", {JSON value}] = Set "key" to value 20 | * 21 | * File size is capped at [approximately] `options.maxFileSize. If the 22 | * transaction file exceeds this size at the time of a write action, a new file 23 | * is started with the current map state . I.e. write actions are generally 24 | * very fast (on the order of 100K's/second or even 1M's/second) but may 25 | * occasionally take as long as needed to write the full state of the map. 26 | */ 27 | module.exports = class PersistentMap extends Map { 28 | _nBytes = 0; 29 | 30 | /** 31 | * @param {String} name 32 | * @param {Object} [options] 33 | * @param {Number} [options.maxFileSize = 1e6] Max size of transaction file (bytes) 34 | */ 35 | constructor(filepath, options) { 36 | super(); 37 | 38 | // Validate / normalize path 39 | filepath = path.resolve(filepath); 40 | 41 | this.options = Object.assign({maxFileSize: 1e6}, options); 42 | this.filepath = filepath; 43 | } 44 | 45 | /** 46 | * Apply a write action to the map 47 | * @param {String|CLEAR} key to set. If CLEAR, clears the map 48 | * @param {*} val to set 49 | */ 50 | _exec(key, val) { 51 | if (key === CLEAR) { 52 | // Clear map 53 | super.clear(); 54 | } else if (val !== undefined) { 55 | // Set a value 56 | super.set(key, val); 57 | } else { 58 | // Delete key 59 | super.delete(key); 60 | } 61 | } 62 | 63 | /** 64 | * Write action queue to disk. 65 | */ 66 | async _write() { 67 | if (this._writing) return; 68 | 69 | // Loop because items may get pushed onto queue while we're writing 70 | while (this._queue) { 71 | let q = this._queue; 72 | const {resolve, reject} = q; 73 | delete this._queue; 74 | this._writing = true; 75 | try { 76 | // Slice to most recent CLEAR action 77 | const clearIndex = q.reduce ((a, b, i) => b[0] === CLEAR ? i : a, -1); 78 | if (clearIndex >= 0) q = q.slice(clearIndex + 1); 79 | 80 | // Compose JSON to write 81 | const json = q.map(action => JSON.stringify(action)).join('\n'); 82 | 83 | if (clearIndex >= 0) { 84 | // If CLEAR, start with new file 85 | this._nBytes = 0; 86 | if (!json) { 87 | await fs.unlink(this.filepath); 88 | } else { 89 | const tmpFile = `${this.filepath}.tmp`; 90 | await fs.writeFile(tmpFile, json + '\n'); 91 | await fs.rename(tmpFile, this.filepath); 92 | } 93 | } else if (json) { 94 | await fs.appendFile(this.filepath, json + '\n'); 95 | } 96 | 97 | this._nBytes += json.length; 98 | resolve(); 99 | } catch (err) { 100 | if (err.code == 'ENOENT') { 101 | // unlinking non-existent file is okay 102 | resolve(err); 103 | } else { 104 | reject(err); 105 | } 106 | } finally { 107 | this._writing = false; 108 | } 109 | } 110 | } 111 | 112 | /** 113 | * Push action onto the write queue 114 | */ 115 | _push(...action) { 116 | // If empty action, just return queue promise 117 | if (action.length == 0) { 118 | return this._queue ? this._queue.promise : undefined; 119 | } 120 | 121 | // Auto-compact 122 | const {maxFileSize} = this.options; 123 | if (maxFileSize != null && this._nBytes >= maxFileSize) { 124 | this._nBytes = 0; 125 | return this.compact(); 126 | } 127 | 128 | // Create queue & promise if needed 129 | if (!this._queue) { 130 | const q = this._queue = []; 131 | q.promise = new Promise((res, rej) => { 132 | q.resolve = res; 133 | q.reject = rej; 134 | }); 135 | } 136 | 137 | const [key, val] = action; 138 | 139 | // Null key clears the map 140 | if (key === CLEAR) { 141 | this._queue.length = 0; 142 | this._queue.push([CLEAR]); // Clear 143 | 144 | // Special case - compact() passes current entries in val 145 | if (val && val[Symbol.iterator]) this._queue.push(...val); 146 | } else { 147 | this._queue.push(action); 148 | } 149 | 150 | // Grab promise here in case _write() clears the queue 151 | const writePromise = this._queue.promise; 152 | 153 | // Make sure write loop runs 154 | this._write(); 155 | 156 | // Return promise that resolves once action has been written 157 | return writePromise; 158 | } 159 | 160 | /** 161 | * Load map from transaction file. This also compacts the file before resolving. 162 | */ 163 | async load() { 164 | // Read file, separate into action array 165 | let lines; 166 | try { 167 | const json = await fs.readFile(this.filepath, 'utf8'); 168 | this._nBytes = json.length; 169 | lines = json.split('\n'); 170 | } catch (err) { 171 | if (err.code != 'ENOENT') throw err; 172 | lines = []; 173 | } 174 | 175 | // Apply each action 176 | super.clear(); 177 | lines.forEach((line, i) => { 178 | line = line.trim(); 179 | if (!line) return; 180 | let action; 181 | try { 182 | action = JSON.parse(line); 183 | } catch (err) { 184 | console.warn(`PersistentMap load() error @ line ${i + 1}: ${err.message}`, line); 185 | return; 186 | } 187 | 188 | this._exec(...action); 189 | }); 190 | 191 | return this; 192 | } 193 | 194 | /** 195 | * Clears transaction file and initializes it with the current map state 196 | */ 197 | compact() { 198 | return this._push(CLEAR, this.entries()); 199 | } 200 | 201 | /** 202 | * @returns {Promise} Resolves once all queued actions have been saved 203 | */ 204 | flush() { 205 | return this._push(); 206 | } 207 | 208 | // 209 | // Map mutation methods (override to add persistence) 210 | // 211 | 212 | clear() { 213 | this._exec(CLEAR); 214 | return this._push(CLEAR); 215 | } 216 | 217 | delete(k) { 218 | this._exec(k); 219 | // If empty, we clear (delete) the file 220 | return this._push(this.size == 0 ? CLEAR : k); 221 | } 222 | 223 | set(key, val) { 224 | this._exec(key, val); 225 | return this._push(key, val); 226 | } 227 | }; 228 | --------------------------------------------------------------------------------