Compare commits
No commits in common. "master" and "v1.0.3" have entirely different histories.
14
.gitignore
vendored
14
.gitignore
vendored
@ -1,7 +1,6 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
@ -14,9 +13,6 @@ 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
|
||||
|
||||
@ -26,12 +22,6 @@ coverage
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
# Dependency directory
|
||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||
node_modules
|
||||
jspm_packages
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
215
LICENSE
215
LICENSE
@ -1,21 +1,202 @@
|
||||
MIT License
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
Copyright (c) 2016 AJ ONeal
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
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:
|
||||
1. Definitions.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
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.
|
||||
|
||||
160
README.md
160
README.md
@ -1,104 +1,74 @@
|
||||
cluster-store
|
||||
Cluster Store
|
||||
=============
|
||||
|
||||
Makes any storage strategy similar to `express/session` useful in both `cluster` and non-`cluster` environments
|
||||
by wrapping it with `cluster-rpc`.
|
||||
A very simple in-memory object store for use with node cluster
|
||||
(or even completely and unrelated node processes).
|
||||
|
||||
Also works with **level-session-store** (leveldb), **connect-session-knex** (SQLite3), **session-file-store** (fs),
|
||||
and any other embedded / in-process store.
|
||||
Node.js runs on a single core, which isn't very effective.
|
||||
|
||||
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.
|
||||
You can run multiple Node.js instances to take advantage of multiple cores,
|
||||
but if you do that, you can't share memory between processes.
|
||||
|
||||
Install
|
||||
=======
|
||||
This module will either run client-server style in environments that benefit from it
|
||||
(such as the Raspberry Pi 2 with 4 cores), or in-process for environments that don't
|
||||
(such as the Raspberry Pi B and B+).
|
||||
|
||||
```
|
||||
npm install --save cluster-store@2.x
|
||||
```
|
||||
**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.
|
||||
|
||||
v1.x vs v2.x
|
||||
------------
|
||||
|
||||
The [old v1](https://github.com/coolaj86/cluster-store/tree/v1.x)
|
||||
used `ws` which makes it usable when clustering node processes without using `cluster`.
|
||||
|
||||
If you need that functionaliy, use v1 instead of v2.
|
||||
Also works with **level-session-store** (leveldb), **connect-session-knex** (SQLite3),
|
||||
**session-file-store** (fs), and any other embedded / in-process store.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
In its simplest form, you use this module nearly exactly the way you would
|
||||
the any other storage module, with the exception that you must wait for
|
||||
the inter-process initialization to complete.
|
||||
The default behavior is to try to connect to a master and, if that fails, to become the master.
|
||||
|
||||
When not using any of the options the usage is the same for the master and the worker:
|
||||
|
||||
```javascript
|
||||
require('cluster-store').create().then(function (store) {
|
||||
// initialization is now complete
|
||||
store.set('foo', 'bar');
|
||||
});
|
||||
```
|
||||
|
||||
### standalone (non-cluster)
|
||||
--------------
|
||||
|
||||
There is no disadvantage to using this module standalone.
|
||||
The additional overhead of inter-process communication is only added when
|
||||
a worker is added.
|
||||
|
||||
As such, the standalone usage is identical to usage in master process, as seen below.
|
||||
|
||||
### master
|
||||
|
||||
In the **master** process you will create the real store instance.
|
||||
|
||||
If you need to manually specify which worker will be enabled for this funcitonality
|
||||
you must set `addOnFork` to `false` and call `addWorker()` manually.
|
||||
|
||||
```javascript
|
||||
'use strict';
|
||||
However, if you are in fact using the `cluster` rather than spinning up random instances,
|
||||
you'll probably prefer to use this pattern:
|
||||
|
||||
```js
|
||||
var cluster = require('cluster');
|
||||
var cstore = require('cluster-store');
|
||||
var numCores = require('os').cpus().length;
|
||||
|
||||
var cstore = require('cluster-store/master').create({
|
||||
name: 'foo-store' // necessary when using multiple instances
|
||||
, store: null // use default in-memory store
|
||||
, addOnFork: true // default
|
||||
});
|
||||
var opts = {
|
||||
sock: '/tmp/memstore.sock'
|
||||
|
||||
// if you addOnFork is set to false you can add specific forks manually
|
||||
//cstore.addWorker(cluster.fork());
|
||||
// If left 'null' or 'undefined' this defaults to a similar memstore
|
||||
// with no special logic for 'cookie' or 'expires'
|
||||
, store: cluster.isMaster && new require('express-session/session/memory')()
|
||||
|
||||
cstore.then(function (store) {
|
||||
store.set('foo', 'bar');
|
||||
});
|
||||
```
|
||||
// a good default to use for instances where you might want
|
||||
// to cluster or to run standalone, but with the same API
|
||||
, serve: cluster.isMaster
|
||||
, connect: cluster.isWorker
|
||||
, standalone: (1 === numCores) // overrides serve and connect
|
||||
};
|
||||
|
||||
Note: `store` can be replaced with any `express/session`-compatible store, such as:
|
||||
cstore.create(opts).then(function (store) {
|
||||
// same api as new new require('express-session/session/memory')(
|
||||
|
||||
* `new require('express-session/session/memory')()`
|
||||
* `require('level-session-store')(session)`
|
||||
* and others
|
||||
|
||||
### 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);
|
||||
store.get(id, function (err, data) {
|
||||
console.log(data);
|
||||
});
|
||||
|
||||
// app.use(expressSession({ secret: 'keyboard cat', store: store }));
|
||||
});
|
||||
|
||||
process.on('unhandledPromiseRejection', function (err) {
|
||||
console.error('Unhandled Promise Rejection');
|
||||
console.error(err);
|
||||
console.error(err.stack);
|
||||
|
||||
throw err;
|
||||
});
|
||||
```
|
||||
|
||||
If you wish to always use clustering, even on a single core system, see `test-cluster.js`.
|
||||
|
||||
Likewise, if you wish to use standalone mode in a particular worker process see `test-standalone.js`.
|
||||
|
||||
API
|
||||
===
|
||||
|
||||
@ -124,28 +94,22 @@ Helpers
|
||||
|
||||
See <https://github.com/expressjs/session#session-store-implementation>@4.x for full details
|
||||
|
||||
Example
|
||||
=======
|
||||
Standalone / Master Mode is in-process
|
||||
========================
|
||||
|
||||
```javascript
|
||||
'use strict';
|
||||
The `master` in the cluster (meaning `opts.serve = true`) will directly hold the specified store
|
||||
(a simple memory store by default, or `express-session/session/memory` in the example above)
|
||||
|
||||
var cluster = require('cluster');
|
||||
Likewise, when only one process is being used (`opts.standalone = true`) the listener is
|
||||
not started and API is completely in-process.
|
||||
|
||||
require('cluster-store').create({
|
||||
name: 'foo-store'
|
||||
}).then(function (store) {
|
||||
if (cluster.isMaster) {
|
||||
store.set('foo', 'bar');
|
||||
}
|
||||
If you take a look at `memstore.js` you'll see that it's a rather simple memory store instance.
|
||||
|
||||
store.get('foo', function (err, result) {
|
||||
console.log(result);
|
||||
});
|
||||
});
|
||||
Security Warning
|
||||
================
|
||||
|
||||
if (cluster.isMaster) {
|
||||
cluster.fork();
|
||||
cluster.fork();
|
||||
}
|
||||
```
|
||||
Note that any application on the system could connect to the socket.
|
||||
|
||||
In the future I may add a `secret` field in the options object to be
|
||||
used for authentication across processes. This would not be difficult,
|
||||
it's just not necessary for my use case at the moment.
|
||||
|
||||
199
client.js
Normal file
199
client.js
Normal file
@ -0,0 +1,199 @@
|
||||
'use strict';
|
||||
|
||||
/*global Promise*/
|
||||
|
||||
function startServer(opts) {
|
||||
return require('./server').create(opts).then(function (server) {
|
||||
// this process doesn't need to connect to itself
|
||||
// through a socket
|
||||
return server.masterClient;
|
||||
});
|
||||
}
|
||||
|
||||
function getConnection(opts) {
|
||||
return new Promise(function (resolve) {
|
||||
//setTimeout(function () {
|
||||
var WebSocket = require('ws');
|
||||
var ws = new WebSocket('ws+unix:' + opts.sock);
|
||||
|
||||
ws.on('error', function (err) {
|
||||
console.error('[ERROR] ws connection failed, retrying');
|
||||
console.error(err);
|
||||
|
||||
function retry() {
|
||||
setTimeout(function () {
|
||||
getConnection(opts).then(resolve, retry);
|
||||
}, 100 + (parseInt(require('crypto').randomBytes(2).toString('hex'), 16) % 250));
|
||||
}
|
||||
|
||||
if (!opts.connect && ('ENOENT' === err.code || 'ECONNREFUSED' === err.code)) {
|
||||
console.log('[NO SERVER] attempting to create a server #######################');
|
||||
return startServer(opts).then(function (client) {
|
||||
// ws.masterClient = client;
|
||||
resolve({ masterClient: client });
|
||||
}, function () {
|
||||
retry();
|
||||
});
|
||||
}
|
||||
|
||||
retry();
|
||||
});
|
||||
|
||||
/*
|
||||
ws.on('open', function () {
|
||||
resolve(ws);
|
||||
});
|
||||
*/
|
||||
ws.___listeners = [];
|
||||
ws.on('message', function (data) {
|
||||
ws.___listeners.forEach(function (fn) {
|
||||
try {
|
||||
fn(data);
|
||||
} catch(e) {
|
||||
console.error("[ERROR] ws.on('message', fn) (multi-callback)");
|
||||
console.error(e);
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function onInitMessage(str) {
|
||||
// TODO there's no way to remove a listener... what to do?
|
||||
var data;
|
||||
|
||||
try {
|
||||
data = JSON.parse(str);
|
||||
} catch(e) {
|
||||
console.error('[ERROR]');
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
if ('methods' !== data.type) {
|
||||
return;
|
||||
}
|
||||
|
||||
var index = ws.___listeners.indexOf(onInitMessage);
|
||||
ws.___listeners.splice(index, 1);
|
||||
ws._methods = data.methods;
|
||||
|
||||
resolve(ws);
|
||||
}
|
||||
|
||||
ws.___listeners.push(onInitMessage);
|
||||
//}, 100 + (Math.random() * 250));
|
||||
});
|
||||
}
|
||||
|
||||
function create(opts) {
|
||||
if (!opts.sock) {
|
||||
opts.sock = '/tmp/memstore' + '.sock';
|
||||
}
|
||||
|
||||
var promise;
|
||||
var numcpus = require('os').cpus().length;
|
||||
if (opts.standalone || (1 === numcpus && !opts.serve && !opts.connect)) {
|
||||
return require('./memstore').create(opts);
|
||||
}
|
||||
|
||||
function retryServe() {
|
||||
return startServer(opts).then(function (client) {
|
||||
// ws.masterClient = client;
|
||||
return { masterClient: client };
|
||||
}, function (err) {
|
||||
console.error('[ERROR] retryServe()');
|
||||
console.error(err);
|
||||
retryServe();
|
||||
});
|
||||
}
|
||||
|
||||
if (opts.serve) {
|
||||
promise = retryServe();
|
||||
} else {
|
||||
promise = getConnection(opts);
|
||||
}
|
||||
|
||||
// TODO maybe use HTTP POST instead?
|
||||
return promise.then(function (ws) {
|
||||
if (ws.masterClient) {
|
||||
return ws.masterClient;
|
||||
}
|
||||
|
||||
var db = {};
|
||||
|
||||
function rpc(fname, args) {
|
||||
var id;
|
||||
var cb;
|
||||
|
||||
if ('function' === typeof args[args.length - 1]) {
|
||||
// TODO if off, search for cb and derive id from previous onMessage
|
||||
id = require('crypto').randomBytes(16).toString('hex');
|
||||
cb = args.pop();
|
||||
}
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'rpc'
|
||||
, func: fname
|
||||
, args: args
|
||||
, hasCallback: !!cb
|
||||
, filename: opts.filename
|
||||
, id: id
|
||||
}));
|
||||
|
||||
if (!cb) {
|
||||
return;
|
||||
}
|
||||
|
||||
function onMessage(data) {
|
||||
var cmd;
|
||||
|
||||
try {
|
||||
cmd = JSON.parse(data.toString('utf8'));
|
||||
} catch(e) {
|
||||
console.error('[ERROR] in client, from sql server parse json');
|
||||
console.error(e);
|
||||
console.error(data);
|
||||
console.error();
|
||||
|
||||
//ws.send(JSON.stringify({ type: 'error', value: { message: e.message, code: "E_PARSE_JSON" } }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd.id !== id) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
// TODO not sure how to handle 'emit' or 'off'...
|
||||
// it'll just be broken for now
|
||||
if ('off' === fname || 'remove.*Listener'.test(fname)) {
|
||||
var index = ws.___listeners.indexOf(onMessage);
|
||||
ws.___listeners.splice(index, 1);
|
||||
}
|
||||
*/
|
||||
|
||||
if ('on' !== fname && ! /add.*Listener/.test(fname)) {
|
||||
var index = ws.___listeners.indexOf(onMessage);
|
||||
ws.___listeners.splice(index, 1);
|
||||
}
|
||||
|
||||
cb.apply(cmd.this, cmd.args);
|
||||
}
|
||||
|
||||
// TODO search index by cb for 'off'
|
||||
// and pass it along to the rpc with the original id
|
||||
onMessage.__cb = cb;
|
||||
onMessage.__id = id;
|
||||
ws.___listeners.push(onMessage);
|
||||
}
|
||||
|
||||
ws._methods.forEach(function (key) {
|
||||
db[key] = function () {
|
||||
rpc(key, Array.prototype.slice.call(arguments));
|
||||
};
|
||||
});
|
||||
|
||||
return db;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports.create = create;
|
||||
20
cluster.js
Normal file
20
cluster.js
Normal file
@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
var memstore = require('./index');
|
||||
|
||||
function create(opts) {
|
||||
var cluster = require('cluster');
|
||||
var numCores = require('os').cpus().length;
|
||||
|
||||
if (!opts.serve && ('boolean' !== typeof opts.serve)) {
|
||||
opts.serve = (numCores > 1) && cluster.isMaster;
|
||||
}
|
||||
|
||||
if (!opts.connect && ('boolean' !== typeof opts.connect)) {
|
||||
opts.connect = (numCores > 1) && cluster.isWorker;
|
||||
}
|
||||
|
||||
return memstore.create(opts);
|
||||
}
|
||||
|
||||
module.exports.create = create;
|
||||
8
index.js
8
index.js
@ -1,9 +1,3 @@
|
||||
'use strict';
|
||||
|
||||
var cluster = require('cluster');
|
||||
|
||||
if (cluster.isMaster) {
|
||||
module.exports = require('./master');
|
||||
} else {
|
||||
module.exports = require('./worker');
|
||||
}
|
||||
module.exports = require('./client');
|
||||
|
||||
19
master.js
19
master.js
@ -1,19 +0,0 @@
|
||||
'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
|
||||
, addOnFork: opts.addOnFork
|
||||
});
|
||||
};
|
||||
19
memstore.js
19
memstore.js
@ -9,30 +9,19 @@ if ('function' === typeof setImmediate) {
|
||||
defer = function (fn) { process.nextTick(fn.bind.apply(fn, arguments)); };
|
||||
}
|
||||
|
||||
function create(opts) {
|
||||
opts = opts || {};
|
||||
function create(/*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 {
|
||||
return Promise.resolve({
|
||||
// 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]);
|
||||
}
|
||||
@ -43,7 +32,6 @@ function create(opts) {
|
||||
defer(fn, null);
|
||||
}
|
||||
, destroy: function (id, fn) {
|
||||
log('destroy(id)', id);
|
||||
delete db[id];
|
||||
|
||||
if (!fn) { return; }
|
||||
@ -61,13 +49,12 @@ function create(opts) {
|
||||
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;
|
||||
|
||||
20
package.json
20
package.json
@ -1,8 +1,7 @@
|
||||
{
|
||||
"name": "cluster-store",
|
||||
"version": "2.0.8",
|
||||
"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).",
|
||||
"homepage": "https://git.coolaj86.com/coolaj86/cluster-store.js",
|
||||
"version": "1.0.3",
|
||||
"description": "A wrapper to enable the use of a in-process store with node cluster via a socket server (i.e. for Raspberry Pi 2).",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "node test-cluster.js",
|
||||
@ -10,10 +9,8 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://git.coolaj86.com/coolaj86/cluster-store.js.git"
|
||||
"url": "git+https://github.com/coolaj86/cluster-store.git"
|
||||
},
|
||||
"bundleDependencies": false,
|
||||
"deprecated": false,
|
||||
"keywords": [
|
||||
"store",
|
||||
"session",
|
||||
@ -23,16 +20,13 @@
|
||||
"cluster",
|
||||
"rpi2"
|
||||
],
|
||||
"author": {
|
||||
"name": "AJ ONeal",
|
||||
"email": "coolaj86@gmail.com",
|
||||
"url": "https://coolaj86.com/"
|
||||
},
|
||||
"author": "AJ ONeal <coolaj86@gmail.com> (http://coolaj86.com/)",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://git.coolaj86.com/coolaj86/cluster-store.js/issues"
|
||||
"url": "https://github.com/coolaj86/cluster-store/issues"
|
||||
},
|
||||
"homepage": "https://github.com/coolaj86/cluster-store#readme",
|
||||
"dependencies": {
|
||||
"cluster-rpc": "^v1.0.6"
|
||||
"ws": "^0.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
150
server.js
Normal file
150
server.js
Normal file
@ -0,0 +1,150 @@
|
||||
'use strict';
|
||||
/*global Promise*/
|
||||
|
||||
var wsses = {};
|
||||
|
||||
function createApp(server, options) {
|
||||
var promise;
|
||||
|
||||
if (wsses[options.filename]) {
|
||||
return Promise.resolve(wsses[options.filename]);
|
||||
}
|
||||
|
||||
if (options.store) {
|
||||
promise = Promise.resolve(options.store);
|
||||
} else {
|
||||
promise = require('./memstore').create(options);
|
||||
}
|
||||
|
||||
return promise.then(function (db) {
|
||||
var url = require('url');
|
||||
//var express = require('express');
|
||||
//var app = express();
|
||||
var wss = server.wss;
|
||||
|
||||
function app(req, res) {
|
||||
res.end('NOT IMPLEMENTED');
|
||||
}
|
||||
|
||||
function getMethods(db) {
|
||||
/*
|
||||
var instanceMethods = Object.keys(db)
|
||||
.map(function (key) { return 'function' === typeof db[key] ? key : null; })
|
||||
.filter(function (key) { return key; })
|
||||
;
|
||||
|
||||
var protoMethods = Object.keys(Object.getPrototypeOf(db))
|
||||
.map(function (key) { return 'function' === typeof Object.getPrototypeOf(db)[key] ? key : null; })
|
||||
.filter(function (key) { return key; })
|
||||
;
|
||||
|
||||
return instanceMethods.concat(protoMethods);
|
||||
*/
|
||||
|
||||
return [
|
||||
'set', 'get', 'touch', 'destroy'
|
||||
, 'all', 'length', 'clear'
|
||||
, 'emit', 'on', 'off', 'once'
|
||||
, 'removeListener', 'addListener'
|
||||
, 'removeEventListener', 'addEventListener'
|
||||
].filter(function (key) {
|
||||
if ('function' === typeof db[key]) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
wss.on('connection', function (ws) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'methods'
|
||||
, methods: getMethods(db)
|
||||
}));
|
||||
|
||||
var location = url.parse(ws.upgradeReq.url, true);
|
||||
// you might use location.query.access_token to authenticate or share sessions
|
||||
// or ws.upgradeReq.headers.cookie (see http://stackoverflow.com/a/16395220/151312
|
||||
|
||||
ws.__session_id = location.query.session_id || require('crypto').randomBytes(16).toString('hex');
|
||||
|
||||
ws.on('message', function (buffer) {
|
||||
var cmd;
|
||||
|
||||
try {
|
||||
cmd = JSON.parse(buffer.toString('utf8'));
|
||||
} catch(e) {
|
||||
console.error('[ERROR] parse json');
|
||||
console.error(e);
|
||||
console.error(buffer);
|
||||
console.error();
|
||||
ws.send(JSON.stringify({ type: 'error', value: { message: e.message, code: "E_PARSE_JSON" } }));
|
||||
return;
|
||||
}
|
||||
|
||||
switch(cmd.type) {
|
||||
case 'init':
|
||||
break;
|
||||
|
||||
case 'rpc':
|
||||
if (cmd.hasCallback) {
|
||||
cmd.args.push(function () {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
this: this
|
||||
, args: args
|
||||
, id: cmd.id
|
||||
}));
|
||||
});
|
||||
|
||||
// TODO handle 'off' by id
|
||||
cmd.args[cmd.args.length - 1].__id = cmd.id;
|
||||
}
|
||||
|
||||
db[cmd.func].apply(db, cmd.args);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error('UNKNOWN TYPE');
|
||||
//break;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
ws.send(JSON.stringify({ type: 'session', value: ws.__session_id }));
|
||||
});
|
||||
|
||||
app.masterClient = db;
|
||||
//wsses[options.filename] = app;
|
||||
|
||||
return app;
|
||||
});
|
||||
}
|
||||
|
||||
function create(options) {
|
||||
var server = require('http').createServer();
|
||||
var WebSocketServer = require('ws').Server;
|
||||
var wss = new WebSocketServer({ server: server });
|
||||
//var port = process.env.PORT || process.argv[0] || 4080;
|
||||
|
||||
var fs = require('fs');
|
||||
var ps = [];
|
||||
|
||||
ps.push(new Promise(function (resolve) {
|
||||
fs.unlink(options.sock, function () {
|
||||
// ignore error when socket doesn't exist
|
||||
|
||||
server.listen(options.sock, resolve);
|
||||
});
|
||||
}));
|
||||
|
||||
ps.push(createApp({ server: server, wss: wss }, options).then(function (app) {
|
||||
server.on('request', app);
|
||||
return { masterClient: app.masterClient };
|
||||
}));
|
||||
|
||||
return Promise.all(ps).then(function (results) {
|
||||
return results[1];
|
||||
});
|
||||
}
|
||||
|
||||
module.exports.create = create;
|
||||
26
simplest.js
26
simplest.js
@ -1,26 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var cluster = require('cluster');
|
||||
var cstore;
|
||||
|
||||
require('./').create({
|
||||
name: 'foo-store'
|
||||
}).then(function (store) {
|
||||
if (cluster.isMaster) {
|
||||
cluster.fork();
|
||||
cluster.fork();
|
||||
|
||||
store.set('foo', 'bar');
|
||||
}
|
||||
|
||||
store.get('foo', function (err, result) {
|
||||
console.log(cluster.isMaster && '0' || cluster.worker.id.toString(), 'foo', result);
|
||||
if (!cluster.isMaster) {
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', function (err) {
|
||||
console.log('unhandledRejection', err);
|
||||
});
|
||||
15
standalone.js
Normal file
15
standalone.js
Normal file
@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
var memstore = require('./index');
|
||||
|
||||
function create(opts) {
|
||||
opts.standalone = true;
|
||||
|
||||
// TODO if cluster *is* used issue a warning?
|
||||
// I suppose the user could be issuing a different filename for each
|
||||
// ... but then they have no need to use this module, right?
|
||||
|
||||
return memstore.create(opts);
|
||||
}
|
||||
|
||||
module.exports.create = create;
|
||||
67
test-cluster.js
Normal file
67
test-cluster.js
Normal file
@ -0,0 +1,67 @@
|
||||
'use strict';
|
||||
|
||||
var cluster = require('cluster');
|
||||
var numCores = 2;
|
||||
//var numCores = require('os').cpus().length;
|
||||
var id = (cluster.isMaster && '0' || cluster.worker.id).toString();
|
||||
|
||||
function run() {
|
||||
var mstore = require('./cluster');
|
||||
|
||||
return mstore.create({
|
||||
standalone: null
|
||||
, serve: null
|
||||
, connect: null
|
||||
}).then(function (store) {
|
||||
store.set('foo', 'bar', function (err) {
|
||||
if (err) { console.error(err); return; }
|
||||
|
||||
store.get('baz', function (err, data) {
|
||||
if (err) { console.error(err); return; }
|
||||
if (null !== data) {
|
||||
console.error(id, 'should be null:', data);
|
||||
}
|
||||
});
|
||||
|
||||
store.get('foo', function (err, data) {
|
||||
if (err) { console.error(err); return; }
|
||||
if ('bar' !== data) {
|
||||
console.error(id, 'should be bar:', data);
|
||||
}
|
||||
|
||||
store.set('quux', { message: 'hey' }, function (/*err*/) {
|
||||
store.get('quux', function (err, data) {
|
||||
if (err) { console.error(err); return; }
|
||||
if (!data || 'hey' !== data.message) {
|
||||
console.error(id, "should be { message: 'hey' }:", data);
|
||||
} else {
|
||||
console.log('Are there any errors above? If not, we passed!');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (cluster.isMaster) {
|
||||
// not a bad idea to setup the master before forking the workers
|
||||
run().then(function () {
|
||||
var i;
|
||||
|
||||
for (i = 1; i <= numCores; i += 1) {
|
||||
cluster.fork();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
|
||||
// The native Promise implementation ignores errors because... dumbness???
|
||||
process.on('unhandledPromiseRejection', function (err) {
|
||||
console.error('Unhandled Promise Rejection');
|
||||
console.error(err);
|
||||
console.error(err.stack);
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
37
test-standalone.js
Normal file
37
test-standalone.js
Normal file
@ -0,0 +1,37 @@
|
||||
'use strict';
|
||||
|
||||
function run() {
|
||||
var mstore = require('./standalone');
|
||||
|
||||
mstore.create({
|
||||
sock: '/tmp/memstore.sock'
|
||||
, standalone: null
|
||||
, serve: null
|
||||
, connect: null
|
||||
}).then(function (store) {
|
||||
store.set('foo', 'bar', function (err) {
|
||||
if (err) { console.error(err); return; }
|
||||
|
||||
store.get('baz', function (err, data) {
|
||||
if (err) { console.error(err); return; }
|
||||
console.log('should be null:', data);
|
||||
});
|
||||
|
||||
store.get('foo', function (err, data) {
|
||||
if (err) { console.error(err); return; }
|
||||
console.log('should be bar:', data);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
run();
|
||||
|
||||
// The native Promise implementation ignores errors because... dumbness???
|
||||
process.on('unhandledPromiseRejection', function (err) {
|
||||
console.error('Unhandled Promise Rejection');
|
||||
console.error(err);
|
||||
console.error(err.stack);
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
48
test.js
48
test.js
@ -1,48 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var cluster = require('cluster');
|
||||
var cstore;
|
||||
//global.Promise = require('bluebird');
|
||||
|
||||
|
||||
if (cluster.isMaster) {
|
||||
|
||||
|
||||
cstore = require('./master').create({
|
||||
name: 'foo-level'
|
||||
});
|
||||
cstore.then(function (db) {
|
||||
db.set('foo', 'bar');
|
||||
});
|
||||
|
||||
cluster.fork();
|
||||
cluster.fork();
|
||||
|
||||
|
||||
}
|
||||
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);
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user