├── json.js ├── test ├── file.js └── index.js ├── package.json ├── README.md ├── LICENSE ├── index.js └── store.js /json.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | encode: JSON.stringify, 4 | decode: function (data) { return JSON.parse(data.toString()) }, 5 | buffer: false 6 | } 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/file.js: -------------------------------------------------------------------------------- 1 | 2 | var LS = require('../') 3 | 4 | var tape = require('tape') 5 | var urlFriendly = require('base64-url').escape 6 | 7 | tape('read and write from a file', function (t) { 8 | var ls1 = LS('/tmp/lossy-store_test', null, urlFriendly) 9 | var key = 'abcd/def+123' 10 | var value = {random: Math.random()} 11 | ls1.set(key, value) 12 | ls1.onDrain(function () { 13 | console.log('DRAINED') 14 | var ls2 = LS('/tmp/lossy-store_test', null, urlFriendly) 15 | ls2.ensure(key, function () { 16 | t.deepEqual(ls2.get(key), value) 17 | t.end() 18 | }) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lossy-store", 3 | "description": "", 4 | "version": "1.2.4", 5 | "homepage": "https://github.com/dominictarr/lossy-store", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/dominictarr/lossy-store.git" 9 | }, 10 | "dependencies": { 11 | "mkdirp": "^0.5.1", 12 | "tape": "^4.6.3" 13 | }, 14 | "devDependencies": { 15 | "base64-url": "^1.3.3" 16 | }, 17 | "scripts": { 18 | "test": "set -e; for t in test/*.js; do node $t; done" 19 | }, 20 | "author": "'Dominic Tarr' (http://dominictarr.com)", 21 | "license": "MIT" 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lossy-store 2 | 3 | simple mini database that does not promise durability, for when you do not need it! 4 | 5 | It's a key value store, and each value is a file. Each value is stored in it's own file. 6 | 7 | ## api 8 | 9 | ### store = LossyStore(dir, codec?) 10 | 11 | create a lossy store with the given [codec](https://www.npmjs.com/package/flumecodec) 12 | (or JSON by default) at the `dir` 13 | 14 | ### store.has(key) 15 | 16 | returns true if this key is currently in the store. 17 | 18 | ### store.ensure(key, cb) 19 | 20 | ensure that this key is loaded from the file system. 21 | if the file has already been read, `cb` is called immediately. 22 | if `set` is called while waiting for the filesystem, `cb` is called immediately. 23 | 24 | ### store.get (key, cb) 25 | 26 | get the current value for key, loading it if necessary 27 | 28 | ### store.get (key) => value 29 | 30 | return the currently set `value` for `key`. may be null. 31 | 32 | ### store.set(key, value) 33 | 34 | Set a new value. this will trigger a write to be performed (at some point) 35 | 36 | ## License 37 | 38 | MIT 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 'Dominic Tarr' 2 | 3 | Permission is hereby granted, free of charge, 4 | to any person obtaining a copy of this software and 5 | associated documentation files (the "Software"), to 6 | deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, 8 | merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom 10 | the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 20 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var json = require('./json') 3 | var Store = require('./store') 4 | var fs = require('fs') 5 | var path = require('path') 6 | var mkdirp = require('mkdirp') 7 | 8 | module.exports = function (dir, codec, keyCodec) { 9 | if(!dir || !fs.readFile) { 10 | if(!dir) 11 | console.error('lossy store missing dir, skipping persistence') 12 | else 13 | console.error('lossy store has no fs access, skipping persistence') 14 | 15 | return Store( 16 | function (v, cb) { cb() }, 17 | function (k,v,cb) { cb() } 18 | ) 19 | } 20 | 21 | codec = codec || json 22 | var keyEncode = keyCodec 23 | ? keyCodec.encode || keyCodec 24 | : function (e) { return e } 25 | 26 | var ready = false 27 | function mkdir (cb) { 28 | if(ready) cb() 29 | else mkdirp(dir, function () { 30 | ready = true 31 | cb() 32 | }) 33 | } 34 | 35 | function toPath(id) { 36 | return path.join(dir, keyEncode(id)) 37 | } 38 | 39 | return Store(function read (id, cb) { 40 | fs.readFile(toPath(id), function (err, value) { 41 | if(err) return cb(err) 42 | try { value = codec.decode(value) } 43 | catch (err) { return cb(err) } 44 | return cb(null, value) 45 | }) 46 | }, function write (id, value, cb) { 47 | try { value = codec.encode(value) } 48 | catch (err) { return cb(err) } 49 | mkdir(function () { 50 | fs.writeFile(toPath(id), value, cb) 51 | }) 52 | }) 53 | } 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 2 | var tape = require('tape') 3 | var Store = require('../store') 4 | 5 | //mock exposes the read and write callbacks directly to the tests 6 | //this allows the tests to be absolutely explicit about orderings. 7 | 8 | function Mock() { 9 | var reading = {} 10 | return mock = { 11 | store: {}, 12 | reading: reading, 13 | read: function (k, cb) { 14 | reading[k] = function (err, value) { 15 | reading[k] = null 16 | cb(err, value) 17 | } 18 | }, 19 | write: function (k, v, cb) { 20 | mock.writeKey = k 21 | mock.writeValue = v 22 | 23 | mock.writing = function (err) { 24 | mock.writing = null 25 | mock.writeKey = null 26 | mock.writeValue = null 27 | cb(err) 28 | } 29 | } 30 | } 31 | } 32 | 33 | tape('simple', function (t) { 34 | 35 | var mock = Mock() 36 | var store = Store(mock.read, mock.write) 37 | 38 | store.set('foo', {bar: true}) 39 | t.deepEqual(store.get('foo'), {bar:true}) 40 | t.end() 41 | }) 42 | 43 | tape('ensure', function (t) { 44 | 45 | var mock = Mock(), ensure 46 | var store = Store(mock.read, mock.write) 47 | 48 | store.ensure('foo', function () { 49 | ensure = true 50 | t.deepEqual(store.get('foo'), {bar:false}) 51 | }) 52 | 53 | t.ok(mock.reading.foo) 54 | mock.reading.foo(null, {bar: false}) 55 | t.deepEqual(store.get('foo'), {bar:false}) 56 | t.ok(ensure) 57 | t.end() 58 | }) 59 | 60 | 61 | tape('write only once at a time', function (t) { 62 | 63 | var mock = Mock(), ensure 64 | var store = Store(mock.read, mock.write) 65 | var data = {} 66 | 67 | store.set('foo', 1) 68 | store.set('bar', 2) 69 | store.set('baz', 3) 70 | t.ok(mock.writing) 71 | while(mock.writing) { 72 | data[mock.writeKey] = mock.writeValue 73 | mock.writing() 74 | } 75 | t.deepEqual(data, {foo: 1, bar: 2, baz: 3}) 76 | t.end() 77 | 78 | }) 79 | 80 | tape('set fires ensure', function (t) { 81 | 82 | var mock = Mock(), ensure 83 | var store = Store(mock.read, mock.write) 84 | 85 | store.ensure('foo', function () { 86 | ensure = true 87 | t.deepEqual(store.get('foo'), 3) 88 | }) 89 | 90 | t.notOk(ensure) 91 | 92 | store.set('foo', 3) 93 | 94 | t.ok(ensure) 95 | 96 | t.end() 97 | 98 | }) 99 | -------------------------------------------------------------------------------- /store.js: -------------------------------------------------------------------------------- 1 | 2 | function isEmpty (o) { 3 | for(var k in o) return false 4 | return true 5 | } 6 | module.exports = function (read, write) { 7 | 8 | var store = {}, dirty = {}, reading = {}, writing = false, waiting = [] 9 | 10 | //this writes only once at a time. 11 | //at least, we want it to write at most once per file. 12 | //or maybe if it's written recently then wait. 13 | //anyway, this is good enough for now. 14 | 15 | function apply_write (key, value, err) { 16 | var _reading = reading[key] 17 | reading[key] = null 18 | while(_reading && _reading.length) 19 | _reading.shift()(err, value) 20 | _write() 21 | } 22 | 23 | function _write () { 24 | if(writing) return 25 | var d = 0 26 | //note, only one key is written at a time. 27 | for(var k in dirty) { 28 | if(dirty[k]) { 29 | dirty[k] = false 30 | writing = true 31 | return write(k, store[k], function (err) { 32 | writing = false 33 | _write() 34 | }) 35 | } 36 | } 37 | //if we wrote something, we returned. 38 | //so clear todo list and fire listeners. 39 | dirty = {} 40 | while(waiting.length) 41 | waiting.shift()() 42 | } 43 | 44 | function has (key) { 45 | return store[key] !== undefined 46 | } 47 | var self 48 | return self = { 49 | has: has, 50 | ensure: function (key, cb) { 51 | if(has(key)) cb(null, store[key]) 52 | else if(reading[key]) 53 | reading[key].push(cb) 54 | else { 55 | var cbs = reading[key] = [cb] 56 | read(key, function (err, value) { 57 | //unusual, but incase someone overwrites the value 58 | //while we are reading. see apply_write 59 | if(cbs !== reading[key]) return 60 | 61 | apply_write(key, store[key] = value, err) 62 | }) 63 | } 64 | }, 65 | get: function (key, cb) { 66 | if(cb) self.ensure(key, cb) 67 | else return store[key] 68 | }, 69 | //if set is called during a read, 70 | //cb the readers immediately, and cancel the current read. 71 | set: function (key, value) { 72 | store[key] = value 73 | //not urgent, but save this if we are not doing anything. 74 | dirty[key] = true 75 | apply_write(key, value) 76 | }, 77 | onDrain: function (cb) { 78 | if(isEmpty(dirty)) cb() 79 | else waiting.push(cb) 80 | } 81 | } 82 | } 83 | 84 | 85 | 86 | 87 | 88 | --------------------------------------------------------------------------------