commit f07410bc14a5662d73de00e2de1c2d8ee48670ce Author: AJ ONeal Date: Thu Sep 8 18:00:53 2016 -0600 v2.0.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5148e52 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1bffff4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 AJ ONeal + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..79a2810 --- /dev/null +++ b/README.md @@ -0,0 +1,148 @@ +cluster-store +============= + +Makes any storage strategy similar to `express/session` useful in both `cluster` and non-`cluster` environments +by wrapping it with `cluster-rpc`. + +Also works with **level-session-store** (leveldb), **connect-session-knex** (SQLite3), **session-file-store** (fs), +and any other embedded / in-process store. + +Note: Most people would probably prefer to just use Redis rather than wrap a dumb memstore as a service... +but I am not most people. + +Install +======= + +``` +npm install --save memstore-cluster@2.x +``` + +v1.x vs v2.x +------------ + +The old v1 used `ws` which makes it usable when clustering node processes without using `cluster`. + +If you need that functionaliy, use it. + +Usage +===== + +### standalone (non-cluster) +-------------- + +The usage is exactly the same as **master**, no changes necessary. + +### master + +In the **master**/**standalone** process you will create the real store instance +and then in the you must pass each worker via `addWorker()` so that it signals +the worker to create its own rpc-wrapped instance. + +```javascript +'use strict'; + +var cluster = require('cluster'); + +var cstore = require('cluster-store/master').create({ + name: 'foo-store' +}); + +cstore.addWorker(cluster.fork()); + +cstore.then(function (store) { + store.set('foo', 'bar'); +}); +``` + +### worker + +```javascript +'use strict'; + +// retrieve the instance +var cstore = require('cluster-store/worker').create({ + name: 'foo-store' +}); + +cstore.then(function (store) { + store.get('foo', function (err, result) { + console.log(result); + }); +}); +``` + +API +=== + +This is modeled after Express' +[Session Store Implementation](https://github.com/expressjs/session#session-store-implementation) + +**Note**: These are only exposed if the underlying store supports them. + +CRUD methods +------------ + +* `store.set(id, data, fn) => (error)` +* `store.get(id, fn) => (error, data)` +* `store.touch(id, data, fn) => (error)` +* `store.destroy(id, fn) => (error)` + +Helpers +------- + +* `store.all(fn) => (error, array)` +* `store.clear(fn) => (error)` +* `store.length(fn) => (error, length)` + +See @4.x for full details + +Example +======= + +```javascript +'use strict'; + +var cluster = require('cluster'); +var cstore; + + +if (cluster.isMaster) { + + + cstore = require('./master').create({ + name: 'foo-level' + }); + cstore.addWorker(cluster.fork()); + cstore.then(function (store) { + store.put('foo', 'bar'); + }); + + +} +else { + + + cstore = require('./worker').create({ + name: 'foo-level' + }); + + +} + + +cstore.then(function (store) { + setTimeout(function () { + store.get('foo', function (err, result) { + console.log(cluster.isMaster && '0' || cluster.worker.id.toString(), "store.get('foo')", result); + + if (!cluster.isMaster) { + process.exit(0); + } + }); + }, 250); +}); + +process.on('unhandledRejection', function (err) { + console.log('unhandledRejection', err); +}); +``` diff --git a/index.js b/index.js new file mode 100644 index 0000000..12f99c1 --- /dev/null +++ b/index.js @@ -0,0 +1,12 @@ +'use strict'; + +console.error(""); +console.error("One does not simply require('memstore-cluster');"); +console.error(""); +console.error("Usage:"); +console.error("\trequire('memstore-cluster/master').create({ name: ... });"); +console.error("\trequire('memstore-cluster/worker').create({ name: ... });"); +console.error(""); +console.error(""); + +process.exit(1); diff --git a/master.js b/master.js new file mode 100644 index 0000000..a4953da --- /dev/null +++ b/master.js @@ -0,0 +1,18 @@ +'use strict'; + +module.exports.create = function (opts) { + opts = opts || {}; + + var db = require('./memstore').create(); + + return require('cluster-rpc/master').create({ + instance: opts.store || db + , methods: [ + 'set', 'get', 'touch', 'destroy' + , 'all', 'length', 'clear' + , 'on', 'off', 'removeEventListener', 'addEventListener' + ] + , name: 'memstore.' + (opts.name || '') + , master: opts.master + }); +}; diff --git a/memstore.js b/memstore.js new file mode 100644 index 0000000..dcc0bec --- /dev/null +++ b/memstore.js @@ -0,0 +1,73 @@ +'use strict'; + +/*global Promise*/ +var defer; + +if ('function' === typeof setImmediate) { + defer = setImmediate; +} else { + defer = function (fn) { process.nextTick(fn.bind.apply(fn, arguments)); }; +} + +function create(opts) { + opts = opts || {}; + // don't leak prototypes as implicitly non-null + var db = Object.create(null); + + function log() { + if (!opts.debug) { + return; + } + + console.log.apply(console, arguments); + } + + return { + // required / recommended + set: function (id, data, fn) { + log('set(id, data)', id, data); + db[id] = data; + + if (!fn) { return; } + defer(fn, null); + } + , get: function (id, fn) { + log('get(id)', id); + if (!fn) { return; } + defer(fn, null, 'undefined' === typeof db[id] ? null : db[id]); + } + , touch: function (id, data, fn) { + db[id] = data; + + if (!fn) { return; } + defer(fn, null); + } + , destroy: function (id, fn) { + log('destroy(id)', id); + delete db[id]; + + if (!fn) { return; } + defer(fn, null); + } + // optional + , all: function (fn) { + if (!fn) { return; } + defer(fn, null, Object.keys(db).map(function (key) { + return db[key]; + })); + } + , length: function (fn) { + if (!fn) { return; } + defer(fn, null, Object.keys(db).length); + } + , clear: function (fn) { + log('clear()', id); + db = Object.create(null); + + if (!fn) { return; } + defer(fn, null); + } + }; +} + +module.exports.create = create; diff --git a/package.json b/package.json new file mode 100644 index 0000000..0f2d1eb --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "cluster-store", + "version": "2.0.0", + "description": "A wrapper to enable the use of any in-process store with node cluster via cluster process and worker messages (i.e. for Raspberry Pi servers).", + "main": "index.js", + "scripts": { + "test": "node test-cluster.js", + "start": "node server.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/coolaj86/cluster-store.git" + }, + "keywords": [ + "store", + "session", + "connect", + "express", + "memstore", + "cluster", + "rpi2" + ], + "author": "AJ ONeal (http://coolaj86.com/)", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/coolaj86/cluster-store/issues" + }, + "homepage": "https://github.com/coolaj86/cluster-store#readme", + "dependencies": { + "cluster-rpc": "^1.0.3" + } +} diff --git a/test.js b/test.js new file mode 100644 index 0000000..a6f5fb2 --- /dev/null +++ b/test.js @@ -0,0 +1,46 @@ +'use strict'; + +var cluster = require('cluster'); +var cstore; +//global.Promise = require('bluebird'); + + +if (cluster.isMaster) { + + + cstore = require('./master').create({ + name: 'foo-level' + }); + cstore.addWorker(cluster.fork()); + cstore.then(function (db) { + db.set('foo', 'bar'); + }); + + +} +else { + + + cstore = require('./worker').create({ + name: 'foo-level' + }); + + +} + + +cstore.then(function (db) { + setTimeout(function () { + db.get('foo', function (err, result) { + console.log(cluster.isMaster && '0' || cluster.worker.id.toString(), "db.get('foo')", result); + + if (!cluster.isMaster) { + process.exit(0); + } + }); + }, 250); +}); + +process.on('unhandledRejection', function (err) { + console.log('unhandledRejection', err); +}); diff --git a/worker.js b/worker.js new file mode 100644 index 0000000..e314c7e --- /dev/null +++ b/worker.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports.create = function (opts) { + return require('cluster-rpc/worker').create({ + name: 'memstore.' + (opts.name || '') + , worker: opts.worker + }); +};