node-jet
Version:
Jet Realtime Message Bus for the Web. Daemon and Peer implementation.
285 lines (284 loc) • 10.7 kB
JavaScript
'use strict';
import { Logger } from '../log.js';
import { createPathMatcher } from './path_matcher.js';
import { Subscription } from './subscription.js';
import { Route } from './route.js';
import { ConnectionInUse, InvvalidCredentials, notAllowed, NotAuthorized, NotFound, Occupied } from '../errors.js';
import { JsonRPCServer } from '../../2_jsonrpc/server.js';
import { EventEmitter } from '../../1_socket/index.js';
import { UserManager } from './UserManager.js';
const version = '2.2.0';
const defaultListenOptions = {
tcpPort: 11122,
wsPort: 11123
};
class InfoObject {
name;
version;
protocolVersion;
features;
constructor(options, authenticate) {
this.name = options.name || 'node-jet';
this.version = version;
this.protocolVersion = '1.1.0';
this.features = {
batches: options.features?.batches || false,
fetch: options.features?.fetch || 'full',
asNotification: options.features?.asNotification || false,
authenticate
};
}
}
/**
* Creates a Daemon instance
*
* In most cases you need one Jet Daemon instance running.
* All Peers connect to it as in typical master(Daemon) slave(Peer)
* architectures.
*
*/
export class Daemon extends EventEmitter {
infoObject;
log;
jsonRPCServer;
routes = {};
subscriber = [];
authenticator;
/**
* Constructor for creating the instance
* @param {DaemonOptions & InfoOptions} [options] Options for the daemon creation
*/
constructor(options = {}) {
super();
this.authenticator = new UserManager(options.username, options.password);
this.infoObject = new InfoObject(options, this.authenticator.enabled);
this.log = new Logger(options.log);
}
asNotification = () => this.infoObject.features.asNotification;
simpleFetch = () => this.infoObject.features.fetch === 'simple';
respond = (peer, id) => {
if (this.asNotification()) {
peer.respond(id, {}, true);
this.emit('notify');
}
else {
this.emit('notify');
peer.respond(id, {}, true);
}
};
authenticate = (peer, id, params) => {
if (this.authenticator.login(params.user, params.password)) {
peer.user = params.user;
peer.respond(id, {}, true);
}
else {
peer.respond(id, new InvvalidCredentials(params.user), false);
}
};
addUser = (peer, id, params) => {
try {
this.authenticator.addUser(peer.user, params.user, params.password, params.groups);
peer.respond(id, {}, true);
}
catch (ex) {
peer.respond(id, ex, false);
}
};
/*
Add as Notification: The message is acknowledged,then all the peers are informed about the new state
Add synchronous: First all Peers are informed about the new value then message is acknowledged
*/
add = (peer, id, params) => {
const { path } = params;
if (path in this.routes) {
peer.respond(id, new Occupied(path), false);
return;
}
this.routes[path] = new Route(peer, path, params.value, params.access);
if (typeof params.value !== 'undefined') {
this.subscriber.forEach((fetchRule) => {
if (this.simpleFetch() || fetchRule.matchesPath(path)) {
fetchRule.addRoute(this.routes[path]);
}
});
}
this.respond(peer, id);
};
/*
Change as Notification: The message is acknowledged,then all the peers are informed about the value change
change synchronous: First all Peers are informed about the new value then the message is acknowledged
*/
change = (peer, id, msg) => {
if (msg.path in this.routes && typeof msg.value !== 'undefined') {
this.routes[msg.path].updateValue(msg.value);
this.respond(peer, id);
}
else {
peer.respond(id, new NotFound(), false);
}
};
/*
Fetch as Notification: The message is acknowledged,then the peer is informed of all the states matching the fetchrule
Fetch synchronous: First the peer is informed of all the states matching the fetchrule then the message is acknowledged
*/
fetch = (peer, id, msg) => {
if (this.simpleFetch() &&
this.subscriber.find((sub) => sub.owner === peer)) {
peer.respond(id, new ConnectionInUse('Only one fetcher per peer in simple fetch Mode'), false);
return;
}
if (this.subscriber.find((sub) => sub.id === msg.id)) {
peer.respond(id, new Occupied('FetchId already in use'), false);
return;
}
try {
const sub = new Subscription(msg, peer);
this.addListener('notify', sub.send);
this.subscriber.push(sub);
sub.setRoutes(Object.values(this.routes).filter((route) => this.routes[route.path].value !== undefined && //check if state
(this.simpleFetch() || sub.matchesPath(route.path)) //check if simpleFetch or pathrule matches
));
this.respond(peer, id);
}
catch (err) {
peer.respond(id, err, false);
}
};
/*
Unfetch synchronous: Unfetch fires and no more updates are send with the given fetch_id. Message is acknowledged
*/
unfetch = (peer, id, params) => {
const subIdx = this.subscriber.findIndex((fetch) => fetch.id === params.id);
if (subIdx < 0) {
peer.respond(id, new NotFound(`No Subscription with id ${params.id} found`), false);
return;
}
if (this.subscriber[subIdx].owner !== peer) {
peer.respond(id, new notAllowed(`Peer does not own subscription with id ${params.id}`), false);
return;
}
this.subscriber[subIdx].close();
this.subscriber.splice(subIdx, 1);
peer.respond(id, {}, true);
};
/*
Get synchronous: Only synchronous implementation-> all the values are added to an array and send as response
*/
get = (peer, id, params) => {
try {
const matcher = createPathMatcher(params);
const resp = Object.keys(this.routes)
.filter((route) => matcher(route) &&
this.authenticator.isAllowed('get', peer.user, this.routes[route].access))
.map((route) => ({
path: route,
value: this.routes[route].value
}));
peer.respond(id, resp, true);
}
catch (ex) {
peer.respond(id, ex, false);
}
};
/*
remove synchronous: Only synchronous implementation-> state is removed then message is acknowledged
*/
remove = (peer, id, params) => {
const route = params.path;
if (!(route in this.routes)) {
peer.respond(id, new NotFound(route), false);
return;
}
this.routes[route].remove();
delete this.routes[route];
this.respond(peer, id);
};
/*
Call and Set requests: Call and set requests are always forwarded synchronous
*/
forward = async (method, user, params) => {
if (!(params.path in this.routes)) {
return await Promise.reject(new NotFound(params.path));
}
if (!this.authenticator.isAllowed('set', user, this.routes[params.path].access)) {
return await Promise.reject(new NotAuthorized(params.path));
}
return await this.routes[params.path].owner.sendRequest(method, params, true);
};
/*
Info requests: Info requests are always synchronous
*/
info = (peer, id) => {
peer.respond(id, this.infoObject, true);
};
configure = (peer, id) => {
peer.respond(id, {}, true);
};
filterRoutesByPeer = (peer) => Object.entries(this.routes)
.filter(([, route]) => route.owner === peer)
.map((el) => el[0]);
/**
* This function starts to listen on the specified port
* @param listenOptions
*/
listen = (listenOptions = defaultListenOptions) => {
this.jsonRPCServer = new JsonRPCServer(this.log, listenOptions, this.infoObject.features.batches);
this.jsonRPCServer.addListener('connection', (newPeer) => {
this.log.info('Peer connected');
newPeer.addListener('info', this.info);
newPeer.addListener('configure', this.configure);
newPeer.addListener('authenticate', this.authenticate);
newPeer.addListener('addUser', this.addUser);
newPeer.addListener('add', this.add);
newPeer.addListener('change', this.change);
newPeer.addListener('remove', this.remove);
newPeer.addListener('get', this.get);
newPeer.addListener('fetch', this.fetch);
newPeer.addListener('unfetch', this.unfetch);
newPeer.addListener('set', async (peer, id, params) => {
await this.forward('set', peer.user, params)
.then((res) => {
newPeer.respond(id, res, true);
})
.catch((err) => {
newPeer.respond(id, err, false);
})
.finally(() => {
newPeer.send();
});
});
newPeer.addListener('call', async (peer, id, params) => {
await this.forward('call', peer.user, params)
.then((res) => {
newPeer.respond(id, res, true);
})
.catch((err) => {
newPeer.respond(id, err, false);
})
.finally(() => {
newPeer.send();
});
});
});
this.jsonRPCServer.addListener('disconnect', (peer) => {
this.filterRoutesByPeer(peer).forEach((route) => {
this.log.warn('Removing route that was owned by peer');
this.routes[route].remove();
delete this.routes[route];
});
this.subscriber = this.subscriber.filter((fetcher) => {
if (fetcher.owner !== peer) {
return true;
}
fetcher.close();
return false;
});
});
this.jsonRPCServer.listen();
this.log.info('Daemon started');
};
close = () => {
this.jsonRPCServer.close();
};
}
export default Daemon;