@azuliani/node-service 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Client/Client.js +246 -0
- package/Client/MonitoredSocket.js +61 -0
- package/Client/PullClient.js +28 -0
- package/Client/RPCClient.js +83 -0
- package/Client/SharedObjectClient.js +283 -0
- package/Client/SinkClient.js +21 -0
- package/Client/SourceClient.js +30 -0
- package/LICENSE +22 -0
- package/README.md +116 -0
- package/Service/PushService.js +29 -0
- package/Service/RPCService.js +39 -0
- package/Service/Service.js +179 -0
- package/Service/SharedObjectService.js +125 -0
- package/Service/SinkService.js +37 -0
- package/Service/SourceService.js +31 -0
- package/index.js +9 -0
- package/misc/Validation.js +275 -0
- package/package.json +36 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const doValidation = require("../misc/Validation").SinkValidation;
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SinkClient {
|
|
6
|
+
constructor(endpoint, transports){
|
|
7
|
+
this.endpoint = endpoint;
|
|
8
|
+
this.transport = transports.sink;
|
|
9
|
+
if (!this.transport)
|
|
10
|
+
throw "Trying to construct Sink endpoint without pushpull transport";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
push(message){
|
|
14
|
+
doValidation(this.endpoint, message, false);
|
|
15
|
+
|
|
16
|
+
const OTW = message;
|
|
17
|
+
this.transport.send(JSON.stringify(OTW));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = SinkClient;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const EventEmitter = require("events").EventEmitter;
|
|
4
|
+
const doValidate = require("../misc/Validation").SourceValidation;
|
|
5
|
+
|
|
6
|
+
class SourceClient extends EventEmitter{
|
|
7
|
+
constructor(endpoint, transports){
|
|
8
|
+
super();
|
|
9
|
+
this.endpoint = endpoint;
|
|
10
|
+
this.transport = transports.source;
|
|
11
|
+
if (!this.transport)
|
|
12
|
+
throw "Trying to construct Source endpoint without Source transport";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
subscribe(){
|
|
16
|
+
this.transport.subscribe(this.endpoint.name);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
unsubscribe(){
|
|
20
|
+
this.transport.unsubscribe(this.endpoint.name);
|
|
21
|
+
};
|
|
22
|
+
_processMessage(data){
|
|
23
|
+
if (this.endpoint.name === data.endpoint){
|
|
24
|
+
doValidate(this.endpoint, data.message, true);
|
|
25
|
+
this.emit('message', data.message);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = SourceClient;
|
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015 Alexander Zuliani
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# node-service
|
|
2
|
+
|
|
3
|
+
Node.js library for building services with ZeroMQ-based messaging patterns. Provides both Service (server) and Client abstractions for different communication patterns.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @azuliani/node-service
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
const { Service, Client } = require('@azuliani/node-service');
|
|
15
|
+
|
|
16
|
+
// Define a descriptor
|
|
17
|
+
const descriptor = {
|
|
18
|
+
transports: {
|
|
19
|
+
rpc: { client: "tcp://127.0.0.1:5555", server: "tcp://127.0.0.1:5555" }
|
|
20
|
+
},
|
|
21
|
+
endpoints: [
|
|
22
|
+
{ name: "Echo", type: "RPC", requestSchema: { type: "string" }, replySchema: { type: "string" } }
|
|
23
|
+
]
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Create a service
|
|
27
|
+
const service = new Service(descriptor, {
|
|
28
|
+
Echo: (request) => request // Echo handler
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Create a client
|
|
32
|
+
const client = new Client(descriptor);
|
|
33
|
+
const reply = await client.Echo("Hello");
|
|
34
|
+
console.log(reply); // "Hello"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Service Types
|
|
38
|
+
|
|
39
|
+
Services are servers that expose endpoints. Created via `new Service(descriptor, handlers, initials, options)`.
|
|
40
|
+
|
|
41
|
+
| Type | Description |
|
|
42
|
+
|------|-------------|
|
|
43
|
+
| **RPCService** | Request/response pattern over ZMQ dealer/router |
|
|
44
|
+
| **SourceService** | Publishes messages to subscribers (ZMQ pub socket) |
|
|
45
|
+
| **SinkService** | Receives messages from clients (ZMQ pull socket) |
|
|
46
|
+
| **PushService** | Pushes messages to workers (ZMQ push socket) |
|
|
47
|
+
| **SharedObjectService** | Syncs object state to clients via diffs |
|
|
48
|
+
|
|
49
|
+
### Service Options
|
|
50
|
+
|
|
51
|
+
- `heartbeatMs` - Heartbeat interval in milliseconds (default: 5000)
|
|
52
|
+
|
|
53
|
+
## Client Types
|
|
54
|
+
|
|
55
|
+
Clients connect to services. Created via `new Client(descriptor, options)`.
|
|
56
|
+
|
|
57
|
+
| Type | Description |
|
|
58
|
+
|------|-------------|
|
|
59
|
+
| **RPCClient** | Calls RPC endpoints with timeout support |
|
|
60
|
+
| **SourceClient** | Subscribes to source messages (ZMQ sub socket) |
|
|
61
|
+
| **SinkClient** | Sends messages to sink (ZMQ push socket) |
|
|
62
|
+
| **PullClient** | Receives pushed messages (ZMQ pull socket) |
|
|
63
|
+
| **SharedObjectClient** | Maintains synchronized copy of server's SharedObject |
|
|
64
|
+
|
|
65
|
+
### Client Options
|
|
66
|
+
|
|
67
|
+
- `initDelay` - Delay in ms before SharedObjectClient fetches full state after subscribe (default: 100)
|
|
68
|
+
|
|
69
|
+
## Descriptor Format
|
|
70
|
+
|
|
71
|
+
Both Service and Client use the same descriptor object:
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
{
|
|
75
|
+
transports: {
|
|
76
|
+
source: { client: "tcp://...", server: "tcp://..." },
|
|
77
|
+
sink: { client: "tcp://...", server: "tcp://..." },
|
|
78
|
+
rpc: { client: "tcp://...", server: "tcp://..." },
|
|
79
|
+
pushpull: { client: "tcp://...", server: "tcp://..." }
|
|
80
|
+
},
|
|
81
|
+
endpoints: [
|
|
82
|
+
{ name: "MyRPC", type: "RPC", requestSchema: {...}, replySchema: {...} },
|
|
83
|
+
{ name: "MySource", type: "Source", messageSchema: {...} },
|
|
84
|
+
{ name: "MySO", type: "SharedObject", objectSchema: {...} }
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Schema Validation
|
|
90
|
+
|
|
91
|
+
Schema validation using `schema-inspector`. Schemas support:
|
|
92
|
+
- Standard types: `string`, `number`, `object`, `array`
|
|
93
|
+
- Special type `date` for automatic Date parsing from JSON
|
|
94
|
+
- Wildcard `*` in object properties for dynamic keys
|
|
95
|
+
- Set `skip: true` on schema to bypass validation
|
|
96
|
+
|
|
97
|
+
## Heartbeat System
|
|
98
|
+
|
|
99
|
+
Services automatically send heartbeat messages on the source transport. Clients detect disconnection when no messages arrive within 3x the heartbeat interval.
|
|
100
|
+
|
|
101
|
+
- Client learns heartbeat frequency from the first heartbeat message
|
|
102
|
+
- Heartbeat timeout triggers automatic disconnect/reconnect cycle
|
|
103
|
+
- On disconnect, SharedObjectClient emits synthetic deletion diffs before flushing data
|
|
104
|
+
|
|
105
|
+
## Lifecycle
|
|
106
|
+
|
|
107
|
+
Both `Service` and `Client` have a `close()` method for proper cleanup:
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
service.close(); // Stops heartbeat, closes all sockets
|
|
111
|
+
client.close(); // Stops heartbeat checking, closes all sockets
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const doValidation = require("../misc/Validation").SinkValidation;
|
|
4
|
+
|
|
5
|
+
class SourceService{
|
|
6
|
+
constructor(endpoint, transports){
|
|
7
|
+
this.endpoint = endpoint;
|
|
8
|
+
this.transport = transports.pushpull;
|
|
9
|
+
if (!this.transport)
|
|
10
|
+
throw "Trying to construct PushPull endpoint without pushpull transport";
|
|
11
|
+
this.stats = {updates: 0};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
push(message){
|
|
15
|
+
doValidation(this.endpoint, message, false);
|
|
16
|
+
|
|
17
|
+
const OTW = message;
|
|
18
|
+
this.transport.send(JSON.stringify(OTW));
|
|
19
|
+
this.stats.updates++;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
getStats(){
|
|
23
|
+
const current_stats = JSON.parse(JSON.stringify(this.stats));
|
|
24
|
+
this.stats.updates = 0;
|
|
25
|
+
return current_stats;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = SourceService;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const assert = require("assert");
|
|
4
|
+
|
|
5
|
+
const doValidation = require("../misc/Validation").RPCValidation;
|
|
6
|
+
|
|
7
|
+
class RPCService{
|
|
8
|
+
constructor(endpoint, handler){
|
|
9
|
+
this.endpoint = endpoint;
|
|
10
|
+
this.handler = handler;
|
|
11
|
+
this.stats = {updates: 0};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
call(data, callback){
|
|
15
|
+
assert(this.endpoint.name === data.endpoint);
|
|
16
|
+
|
|
17
|
+
doValidation(this.endpoint, 'input', data.input, true);
|
|
18
|
+
|
|
19
|
+
this.stats.updates++;
|
|
20
|
+
|
|
21
|
+
this.handler(data.input, (err, res) => {
|
|
22
|
+
|
|
23
|
+
if (!err){
|
|
24
|
+
doValidation(this.endpoint, 'output', res, false);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const reply = JSON.stringify({err,res});
|
|
28
|
+
callback(reply);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getStats(){
|
|
33
|
+
const current_stats = JSON.parse(JSON.stringify(this.stats));
|
|
34
|
+
this.stats.updates = 0;
|
|
35
|
+
return current_stats;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = RPCService;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const zmq = require("zeromq/v5-compat");
|
|
4
|
+
const http = require("http");
|
|
5
|
+
const EventEmitter = require("events").EventEmitter;
|
|
6
|
+
|
|
7
|
+
const RPCService = require("./RPCService");
|
|
8
|
+
const SourceService = require("./SourceService");
|
|
9
|
+
const SharedObjectService = require("./SharedObjectService");
|
|
10
|
+
const PushService = require("./PushService");
|
|
11
|
+
const SinkService = require("./SinkService");
|
|
12
|
+
|
|
13
|
+
class Service {
|
|
14
|
+
constructor(descriptor, handlers, initials, options = {}) {
|
|
15
|
+
this.descriptor = descriptor;
|
|
16
|
+
this.transports = {};
|
|
17
|
+
this.handlers = handlers || {};
|
|
18
|
+
this.initials = initials || {};
|
|
19
|
+
this._heartbeatMs = options.heartbeatMs ?? 5000;
|
|
20
|
+
|
|
21
|
+
this._setupTransports();
|
|
22
|
+
this._setupEndpoints();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_setupTransports() {
|
|
26
|
+
for (let transport in this.descriptor.transports) {
|
|
27
|
+
switch (transport) {
|
|
28
|
+
case 'source':
|
|
29
|
+
this._setupSource(this.descriptor.transports.source.server);
|
|
30
|
+
break;
|
|
31
|
+
case 'sink':
|
|
32
|
+
this._setupSink(this.descriptor.transports.sink.server);
|
|
33
|
+
break;
|
|
34
|
+
case 'rpc':
|
|
35
|
+
this._setupRpc(this.descriptor.transports.rpc.server);
|
|
36
|
+
break;
|
|
37
|
+
case 'pushpull':
|
|
38
|
+
this._setupPushPull(this.descriptor.transports.pushpull.server);
|
|
39
|
+
break;
|
|
40
|
+
default:
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_setupSource(hostname) {
|
|
47
|
+
const sock = new zmq.socket('pub');
|
|
48
|
+
|
|
49
|
+
sock.setsockopt(zmq.ZMQ_SNDHWM, 10000);
|
|
50
|
+
sock.setsockopt(zmq.ZMQ_LINGER, 0);
|
|
51
|
+
sock.setsockopt(39, 1); // ZMQ_IMMEDIATE
|
|
52
|
+
|
|
53
|
+
this.transports.source = sock;
|
|
54
|
+
sock.bind(hostname);
|
|
55
|
+
this._setupHeartbeat();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_setupHeartbeat() {
|
|
59
|
+
this._heartbeatInterval = setInterval(this._sendHeartbeat.bind(this), this._heartbeatMs);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_sendHeartbeat() {
|
|
63
|
+
const OTW = {
|
|
64
|
+
endpoint: '_heartbeat',
|
|
65
|
+
frequencyMs: this._heartbeatMs
|
|
66
|
+
};
|
|
67
|
+
this.transports.source.send([OTW.endpoint, JSON.stringify(OTW)]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_setupSink(hostname) {
|
|
71
|
+
const sock = new zmq.socket('pull');
|
|
72
|
+
this.transports.sink = sock;
|
|
73
|
+
|
|
74
|
+
sock.bind(hostname);
|
|
75
|
+
sock.on('message', this._sinkCallback.bind(this));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_sinkCallback(message) {
|
|
79
|
+
if (!this.SinkEndpoint) {
|
|
80
|
+
throw new Error("Got a pull message, but ot Pull enpoint is connected!");
|
|
81
|
+
}
|
|
82
|
+
this.SinkEndpoint._processMessage(JSON.parse(message));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_setupRpc(hostname) {
|
|
86
|
+
this.transports.rpc = new EventEmitter();
|
|
87
|
+
|
|
88
|
+
const hostnameAndPort = hostname.split(":");
|
|
89
|
+
const url = hostnameAndPort[1].substr(2);
|
|
90
|
+
const port = hostnameAndPort[2];
|
|
91
|
+
this._httpServer = http.createServer(this._rpcCallback.bind(this));
|
|
92
|
+
this._httpServer.listen(port, url);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
_rpcCallback(req, res) {
|
|
96
|
+
if (req.method === 'POST') {
|
|
97
|
+
let body = "";
|
|
98
|
+
req.on('data', function (data) {
|
|
99
|
+
body += data;
|
|
100
|
+
});
|
|
101
|
+
const self = this;
|
|
102
|
+
req.on('end', function () {
|
|
103
|
+
const parsedReq = JSON.parse(body);
|
|
104
|
+
const handler = self.RPCServices[parsedReq.endpoint];
|
|
105
|
+
if (handler) {
|
|
106
|
+
handler.call(parsedReq, (result) => {
|
|
107
|
+
res.writeHead(200, {'Content-Type': 'application/json'});
|
|
108
|
+
res.end(result);
|
|
109
|
+
});
|
|
110
|
+
} else {
|
|
111
|
+
res.writeHead(404, {'Content-Type': 'application/json'});
|
|
112
|
+
res.end(JSON.stringify({err: `Unknown endpoint: ${parsedReq.endpoint}`}));
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
res.writeHead(200, {'Content-Type': 'text/html'});
|
|
118
|
+
res.end("-");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
_setupPushPull(hostname) {
|
|
123
|
+
const sock = new zmq.socket('push');
|
|
124
|
+
sock.bind(hostname);
|
|
125
|
+
this.transports.pushpull = sock;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
_setupEndpoints() {
|
|
129
|
+
this.RPCServices = {};
|
|
130
|
+
|
|
131
|
+
for (let endpoint of this.descriptor.endpoints) {
|
|
132
|
+
switch (endpoint.type) {
|
|
133
|
+
case 'RPC': {
|
|
134
|
+
const handler = this.handlers[endpoint.name];
|
|
135
|
+
if (!handler)
|
|
136
|
+
throw "Missing handler: " + endpoint.name;
|
|
137
|
+
this.RPCServices[endpoint.name] = new RPCService(endpoint, handler);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case 'Source':
|
|
141
|
+
this[endpoint.name] = new SourceService(endpoint, this.transports);
|
|
142
|
+
break;
|
|
143
|
+
case 'SharedObject':
|
|
144
|
+
this[endpoint.name] = new SharedObjectService(endpoint, this.transports, this.initials[endpoint.name]);
|
|
145
|
+
this.RPCServices["_SO_" + endpoint.name] = this[endpoint.name];
|
|
146
|
+
break;
|
|
147
|
+
case 'PushPull':
|
|
148
|
+
this[endpoint.name] = new PushService(endpoint, this.transports);
|
|
149
|
+
break;
|
|
150
|
+
case 'Sink':
|
|
151
|
+
this[endpoint.name] = new SinkService(endpoint, this.transports);
|
|
152
|
+
this.SinkEndpoint = this[endpoint.name];
|
|
153
|
+
break;
|
|
154
|
+
default:
|
|
155
|
+
throw "Unknown endpoint type";
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
close() {
|
|
161
|
+
if (this._heartbeatInterval) {
|
|
162
|
+
clearInterval(this._heartbeatInterval);
|
|
163
|
+
}
|
|
164
|
+
if (this._httpServer) {
|
|
165
|
+
this._httpServer.close();
|
|
166
|
+
}
|
|
167
|
+
if (this.transports.source) {
|
|
168
|
+
this.transports.source.close();
|
|
169
|
+
}
|
|
170
|
+
if (this.transports.sink) {
|
|
171
|
+
this.transports.sink.close();
|
|
172
|
+
}
|
|
173
|
+
if (this.transports.pushpull) {
|
|
174
|
+
this.transports.pushpull.close();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = Service;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const assert = require("assert");
|
|
4
|
+
const fastcopy = require("fast-copy").default;
|
|
5
|
+
const doValidate = require("../misc/Validation").SharedObjectValidation;
|
|
6
|
+
const deepDiff = require("@azuliani/deep-diff");
|
|
7
|
+
|
|
8
|
+
class SharedObjectService{
|
|
9
|
+
constructor(endpoint, transports, initial){
|
|
10
|
+
if (!transports.rpc || !transports.source)
|
|
11
|
+
throw new Error("Shared objects need both Source and RPC transports to be configured");
|
|
12
|
+
|
|
13
|
+
doValidate(endpoint, initial);
|
|
14
|
+
this.data = initial;
|
|
15
|
+
this._lastTransmit = fastcopy(initial);
|
|
16
|
+
this._v = 0;
|
|
17
|
+
this.endpoint = endpoint;
|
|
18
|
+
this.diffTransport = transports.source;
|
|
19
|
+
this.stats = {updates: 0};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
call(data, callback) {
|
|
23
|
+
if (data.input === "init") {
|
|
24
|
+
callback(JSON.stringify({err: null, res: {data: this.data, v: this._v}}));
|
|
25
|
+
} else {
|
|
26
|
+
callback(JSON.stringify({err: `Unknown command: ${data.input}`}));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
notify(hint, dirtyBypass){
|
|
31
|
+
const now = new Date();
|
|
32
|
+
|
|
33
|
+
if (!hint)
|
|
34
|
+
hint = [];
|
|
35
|
+
|
|
36
|
+
doValidate(this.endpoint, this.data, hint);
|
|
37
|
+
|
|
38
|
+
const diffs = diffAndReverseAndApplyWithHint(this._lastTransmit, this.data, hint, dirtyBypass);
|
|
39
|
+
|
|
40
|
+
if (diffs && diffs.length) {
|
|
41
|
+
this.stats.updates++;
|
|
42
|
+
this._v++;
|
|
43
|
+
const OTW = {
|
|
44
|
+
endpoint: "_SO_" + this.endpoint.name,
|
|
45
|
+
message: {diffs, v: this._v, now}
|
|
46
|
+
};
|
|
47
|
+
this.diffTransport.send([OTW.endpoint,JSON.stringify(OTW)]);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getStats(){
|
|
52
|
+
const current_stats = JSON.parse(JSON.stringify(this.stats));
|
|
53
|
+
this.stats.updates = 0;
|
|
54
|
+
return current_stats;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function diffAndReverseAndApplyWithHint(lhs, rhs, hint, bypass){
|
|
59
|
+
let lhsWithHint = lhs;
|
|
60
|
+
let rhsWithHint = rhs;
|
|
61
|
+
let lhsParent = null;
|
|
62
|
+
let i = 0;
|
|
63
|
+
|
|
64
|
+
while(i < hint.length){
|
|
65
|
+
// Stop if add or delete.
|
|
66
|
+
if (!(hint[i] in lhsWithHint) || !(hint[i] in rhsWithHint)){
|
|
67
|
+
break
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
lhsParent = lhsWithHint;
|
|
71
|
+
|
|
72
|
+
lhsWithHint = lhsWithHint[hint[i]];
|
|
73
|
+
rhsWithHint = rhsWithHint[hint[i]];
|
|
74
|
+
i++;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const hintUsed = hint.slice(0,i);
|
|
78
|
+
|
|
79
|
+
const reportDiffs = []; // Separate because of clone changes
|
|
80
|
+
|
|
81
|
+
if (hintUsed.length < hint.length){ // SHORTCUT
|
|
82
|
+
if (!(hint[i] in lhsWithHint) && (hint[i] in rhsWithHint)){ // SHORTCUT ADD
|
|
83
|
+
const cloned = fastcopy(rhsWithHint[hint[i]]);
|
|
84
|
+
reportDiffs.push({kind: 'N', path: [...hintUsed, hint[i]], rhs: cloned});
|
|
85
|
+
lhsWithHint[hint[i]] = cloned
|
|
86
|
+
}else if (!(hint[i] in rhsWithHint)){ // SHORTCUT DEL
|
|
87
|
+
reportDiffs.push({kind: 'D', path: [...hintUsed, hint[i]], lhs: lhsWithHint[hint[i]]});
|
|
88
|
+
delete lhsWithHint[hint[i]];
|
|
89
|
+
}else{
|
|
90
|
+
throw new Error("Wut?");
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
if (!bypass) {
|
|
94
|
+
const diffs = deepDiff.diff(lhsWithHint, rhsWithHint);
|
|
95
|
+
if (diffs) {
|
|
96
|
+
for (let i = diffs.length - 1; i >= 0; i--) {
|
|
97
|
+
const diff = fastcopy(diffs[i]);
|
|
98
|
+
if (diff.path) {
|
|
99
|
+
diff.path = [...hintUsed, ...diff.path];
|
|
100
|
+
} else {
|
|
101
|
+
diff.path = hintUsed;
|
|
102
|
+
}
|
|
103
|
+
deepDiff.applyChange(lhs, rhs, diff);
|
|
104
|
+
reportDiffs.push(diff);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
// BYPASS
|
|
109
|
+
assert(lhsParent && hint[0]);
|
|
110
|
+
|
|
111
|
+
reportDiffs.push({
|
|
112
|
+
kind: 'N',
|
|
113
|
+
path: hint,
|
|
114
|
+
rhs: rhsWithHint
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
lhsParent[hint[hint.length-1]] = rhsWithHint;
|
|
118
|
+
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return reportDiffs;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = SharedObjectService;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const EventEmitter = require("events").EventEmitter;
|
|
4
|
+
|
|
5
|
+
class SinkService extends EventEmitter {
|
|
6
|
+
constructor(endpoint, transports, hostname) {
|
|
7
|
+
super();
|
|
8
|
+
this.endpoint = endpoint;
|
|
9
|
+
this.transport = transports.sink;
|
|
10
|
+
this.hostname = hostname;
|
|
11
|
+
if (!this.transport)
|
|
12
|
+
throw new Error("Trying to construct Sink endpoint without Source transport");
|
|
13
|
+
this.stats = {updates: 0};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
subscribe() {
|
|
17
|
+
this.transport.connect(this.hostname);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
unsubscribe() {
|
|
21
|
+
this.transport.disconnect(this.hostname);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
_processMessage(data) {
|
|
25
|
+
this.stats.updates++;
|
|
26
|
+
this.emit("message", data);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getStats(){
|
|
30
|
+
const current_stats = JSON.parse(JSON.stringify(this.stats));
|
|
31
|
+
this.stats.updates = 0;
|
|
32
|
+
return current_stats;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
module.exports = SinkService;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const doValidation = require("../misc/Validation").SourceValidation;
|
|
4
|
+
|
|
5
|
+
class SourceService{
|
|
6
|
+
constructor(endpoint, transports){
|
|
7
|
+
this.endpoint = endpoint;
|
|
8
|
+
this.transport = transports.source;
|
|
9
|
+
if (!this.transport)
|
|
10
|
+
throw "Trying to construct Source endpoint without Source transport";
|
|
11
|
+
this.stats = {updates: 0};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
send(message){
|
|
15
|
+
doValidation(this.endpoint, message, false);
|
|
16
|
+
const OTW = {
|
|
17
|
+
endpoint: this.endpoint.name,
|
|
18
|
+
message: message
|
|
19
|
+
};
|
|
20
|
+
this.transport.send([OTW.endpoint, JSON.stringify(OTW)]);
|
|
21
|
+
this.stats.updates++;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getStats(){
|
|
25
|
+
const current_stats = JSON.parse(JSON.stringify(this.stats));
|
|
26
|
+
this.stats.updates = 0;
|
|
27
|
+
return current_stats;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = SourceService;
|