simpleddp-node
Version:
The aim of this library is to simplify the process of working with meteor server over DDP protocol using external JS environments
546 lines (545 loc) • 21.1 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ddpCollection = exports.ddpSubscription = exports.ddpEventListener = void 0;
const ejson_1 = __importDefault(require("ejson"));
const ddp_1 = __importDefault(require("./ddp/ddp"));
const isEqual_js_1 = require("./helpers/isEqual.js");
const fullCopy_js_1 = require("./helpers/fullCopy.js");
const ddpEventListener_js_1 = require("./classes/ddpEventListener.js");
Object.defineProperty(exports, "ddpEventListener", { enumerable: true, get: function () { return ddpEventListener_js_1.ddpEventListener; } });
const ddpSubscription_js_1 = require("./classes/ddpSubscription.js");
Object.defineProperty(exports, "ddpSubscription", { enumerable: true, get: function () { return ddpSubscription_js_1.ddpSubscription; } });
const ddpCollection_js_1 = require("./classes/ddpCollection.js");
Object.defineProperty(exports, "ddpCollection", { enumerable: true, get: function () { return ddpCollection_js_1.ddpCollection; } });
function uniqueIdFuncGen() {
let idCounter = 0;
return function () {
return idCounter++;
};
}
const simpleDDPcounter = uniqueIdFuncGen();
function connectPlugins(plugins, ...places) {
if (plugins && Array.isArray(plugins)) {
plugins.forEach((p) => {
places.forEach((place) => {
if (p[place]) {
// @ts-ignore
p[place].call(this);
}
});
});
}
}
/**
* Creates an instance of DDPServerConnection class. After being constructed, the instance will
* establish a connection with the DDP server and will try to maintain it open.
*/
class DDPClient {
_id = simpleDDPcounter();
_opGenId = uniqueIdFuncGen();
_opts;
ddpConnection;
subs = [];
/**
All collections data received from server.
*/
collections = {};
onChangeFuncs = [];
connected = false;
maxTimeout;
clearDataOnReconnection;
tryingToConnect;
tryingToDisconnect = false;
willTryToReconnect;
connectedEvent;
connectedEventRestartSubs;
disconnectedEvent;
addedEvent;
changedEvent;
removedEvent;
userId;
loggedIn = false;
token;
/**
* @example
* var opts = {
* endpoint: "ws://someserver.com/websocket",
* SocketConstructor: WebSocket,
* reconnectInterval: 5000
* };
* var server = new simpleDDP(opts);
*/
constructor(opts, plugins) {
this._opts = opts;
this.ddpConnection = new ddp_1.default(opts);
this.maxTimeout = opts.maxTimeout;
this.clearDataOnReconnection = opts.clearDataOnReconnection === undefined ? true : opts.clearDataOnReconnection;
this.tryingToConnect = opts.autoConnect === undefined ? true : opts.autoConnect;
this.willTryToReconnect = opts.autoReconnect === undefined ? true : opts.autoReconnect;
const pluginConnector = connectPlugins.bind(this, plugins);
// plugin init section
pluginConnector('init', 'beforeConnected');
this.connectedEvent = this.on('connected', () => {
this.connected = true;
this.tryingToConnect = false;
});
pluginConnector('afterConnected', 'beforeSubsRestart');
this.connectedEventRestartSubs = this.on('connected', () => {
if (this.clearDataOnReconnection) {
// we have to clean local collections
this.clearData().then(() => {
this.ddpConnection.emit('clientReady');
this.restartSubs();
});
}
else {
this.ddpConnection.emit('clientReady');
this.restartSubs();
}
});
pluginConnector('afterSubsRestart', 'beforeDisconnected');
this.disconnectedEvent = this.on('disconnected', () => {
this.connected = false;
this.tryingToDisconnect = false;
this.tryingToConnect = this.willTryToReconnect;
});
pluginConnector('afterDisconnected', 'beforeAdded');
this.addedEvent = this.on('added', (m) => this.dispatchAdded(m));
pluginConnector('afterAdded', 'beforeChanged');
this.changedEvent = this.on('changed', (m) => this.dispatchChanged(m));
pluginConnector('afterChanged', 'beforeRemoved');
this.removedEvent = this.on('removed', (m) => this.dispatchRemoved(m));
pluginConnector('afterRemoved', 'after');
}
login(obj) {
var atStart = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
return new Promise((resolve, reject) => {
this.apply('login', [obj], atStart).then((m) => {
if (m && 'id' in m) {
this.userId = m.id;
this.token = m.token;
this.loggedIn = true;
if (m.type == 'resume') {
this.ddpConnection.emit('loginResume', m);
}
else {
this.ddpConnection.emit('login', m);
}
resolve(m);
}
else {
reject(m);
}
}, reject);
});
}
;
logout() {
return new Promise((resolve, reject) => {
if (this.loggedIn) {
this.apply('logout').then(() => {
this.userId = undefined;
this.token = undefined;
this.loggedIn = false;
this.ddpConnection.emit('logout');
resolve();
}, reject);
}
else {
resolve();
}
});
}
;
restartSubs() {
this.subs.forEach((sub) => {
if (sub.isOn()) {
sub.restart();
}
});
}
/**
* Use this for fetching the subscribed data and for reactivity inside the collection.
*/
collection(name) {
return new ddpCollection_js_1.ddpCollection(name, this);
}
/**
* Dispatcher for ddp added messages.
*/
dispatchAdded(m) {
// m везде одинаковое, стоит наверное копировать
// m is always the same, it is probably worth copying
if (this.collections.hasOwnProperty(m.collection)) {
const i = this.collections[m.collection].findIndex((obj) => obj._id == m.id);
if (i > -1) {
// new sub knows nothing about old sub
this.collections[m.collection].splice(i, 1);
}
}
if (!this.collections.hasOwnProperty(m.collection))
this.collections[m.collection] = [];
const newObj = { _id: m.id, ...m.fields };
const i = this.collections[m.collection].push(newObj);
const fields = {};
if (m.fields) {
Object.keys(m.fields).map((p) => {
fields[p] = 1;
});
}
this.onChangeFuncs.forEach((l) => {
if (l.collection == m.collection) {
const hasFilter = l.hasOwnProperty('filter');
const newObjFullCopy = (0, fullCopy_js_1.fullCopy)(newObj);
if (!hasFilter) {
l.f({ changed: false, added: newObjFullCopy, removed: false });
}
else if (hasFilter && l.filter(newObjFullCopy, i - 1, this.collections[m.collection])) {
// @ts-ignore
l.f({ prev: false, next: newObjFullCopy, fields, fieldsChanged: newObjFullCopy, fieldsRemoved: [] });
}
}
});
}
/**
* Dispatcher for ddp changed messages.
*/
dispatchChanged(m) {
if (!this.collections.hasOwnProperty(m.collection))
this.collections[m.collection] = [];
const i = this.collections[m.collection].findIndex((obj) => obj._id == m.id);
if (i > -1) {
const t = this.collections[m.collection][i];
const prev = (0, fullCopy_js_1.fullCopy)(this.collections[m.collection][i]);
const fields = {};
let fieldsChanged = {};
let fieldsRemoved = [];
if (m.fields) {
fieldsChanged = m.fields;
Object.keys(m.fields).map((p) => {
fields[p] = 1;
});
Object.assign(this.collections[m.collection][i], m.fields);
}
if (m.cleared) {
fieldsRemoved = m.cleared;
m.cleared.forEach((fieldName) => {
fields[fieldName] = 0;
delete this.collections[m.collection][i][fieldName];
});
}
const next = this.collections[m.collection][i];
this.onChangeFuncs.forEach((l) => {
if (l.collection == m.collection) {
// perhaps add a parameter inside l object to choose if full copy should occur
const hasFilter = l.hasOwnProperty('filter');
if (!hasFilter) {
l.f({
// @ts-ignore
changed: { prev, next: (0, fullCopy_js_1.fullCopy)(next), fields, fieldsChanged, fieldsRemoved },
added: false,
removed: false
});
}
else {
const fCopyNext = (0, fullCopy_js_1.fullCopy)(next);
const prevFilter = l.filter(prev, i, this.collections[m.collection]);
const nextFilter = l.filter(fCopyNext, i, this.collections[m.collection]);
if (prevFilter || nextFilter) {
l.f({
// @ts-ignore
prev,
next: fCopyNext,
fields,
fieldsChanged,
fieldsRemoved,
predicatePassed: [prevFilter, nextFilter]
});
}
}
}
});
}
else {
this.dispatchAdded(m);
}
}
/**
* Dispatcher for ddp removed messages.
*/
dispatchRemoved(m) {
if (!this.collections.hasOwnProperty(m.collection))
this.collections[m.collection] = [];
const i = this.collections[m.collection].findIndex((obj) => obj._id == m.id);
if (i > -1) {
const removedObj = this.collections[m.collection].splice(i, 1)[0];
this.onChangeFuncs.forEach((l) => {
if (l.collection == m.collection) {
const hasFilter = l.hasOwnProperty('filter');
if (!hasFilter) {
// возможно стоит сделать fullCopy, чтобы было как в случае dispatchAdded и dispatchChanged
// perhaps you should make a fullCopy so that it is like in the case of dispatchAdded and dispatchChanged
l.f({ changed: false, added: false, removed: removedObj });
}
else if (l.filter(removedObj, i, this.collections[m.collection])) {
// @ts-ignore
l.f({ prev: removedObj, next: false });
}
}
});
}
}
/**
* Connects to the ddp server. The method is called automatically by the class constructor if the autoConnect option is set to true (default behavior).
* @public
* @return {Promise} - Promise which resolves when connection is established.
*/
connect() {
this.willTryToReconnect = this._opts.autoReconnect === undefined ? true : this._opts.autoReconnect;
return new Promise((resolve, reject) => {
if (!this.tryingToConnect) {
this.ddpConnection.connect();
this.tryingToConnect = true;
}
if (!this.connected) {
let stoppingInterval;
const connectionHandler = this.on('connected', () => {
clearTimeout(stoppingInterval);
connectionHandler.stop();
this.tryingToConnect = false;
resolve();
});
if (this.maxTimeout) {
stoppingInterval = setTimeout(() => {
connectionHandler.stop();
this.tryingToConnect = false;
reject(new Error('MAX_TIMEOUT_REACHED'));
}, this.maxTimeout);
}
}
else {
resolve();
}
});
}
/**
* Disconnects from the ddp server by closing the WebSocket connection. You can listen on the disconnected event to be notified of the disconnection.
*/
disconnect() {
this.willTryToReconnect = false;
return new Promise((resolve, reject) => {
if (!this.tryingToDisconnect) {
this.ddpConnection.disconnect();
this.tryingToDisconnect = true;
}
if (this.connected) {
const connectionHandler = this.on('disconnected', () => {
connectionHandler.stop();
this.tryingToDisconnect = false;
resolve();
});
}
else {
resolve();
}
});
}
/**
* Calls a remote method with arguments passed in array.
* @example
* server.apply("method1").then(function(result) {
* console.log(result); //show result message in console
* if (result.someId) {
* //server sends us someId, lets call next method using this id
* return server.apply("method2",[result.someId]);
* } else {
* //we didn't recieve an id, lets throw an error
* throw "no id sent";
* }
* }).then(function(result) {
* console.log(result); //show result message from second method
* }).catch(function(error) {
* console.log(result); //show error message in console
* });
*/
apply(method, args, atBeginning = false) {
return new Promise((resolve, reject) => {
const methodId = this.ddpConnection.method(method, args || [], atBeginning);
const _self = this;
let stoppingInterval;
function onMethodResult(message) {
if (message.id == methodId) {
clearTimeout(stoppingInterval);
if (!message.error) {
resolve(message.result);
}
else {
reject(message.error);
}
_self.ddpConnection.removeListener('result', onMethodResult);
}
}
this.ddpConnection.on('result', onMethodResult);
if (this.maxTimeout) {
stoppingInterval = setTimeout(() => {
this.ddpConnection.removeListener('result', onMethodResult);
reject(new Error('MAX_TIMEOUT_REACHED'));
}, this.maxTimeout);
}
});
}
/**
* Calls a remote method with arguments passed after the first argument.
* Syntactic sugar for @see apply.
*/
call(method, ...args) {
return this.apply(method, args);
}
/**
* Tries to subscribe to a specific publication on server.
* Starts the subscription if the same subscription exists.
*/
sub(pubname, args) {
const hasSuchSub = this.subs.find((sub) => sub.pubname == pubname && (0, isEqual_js_1.isEqual)(sub.args, Array.isArray(args) ? args : []));
if (!hasSuchSub) {
const i = this.subs.push(new ddpSubscription_js_1.ddpSubscription(pubname, Array.isArray(args) ? args : [], this));
return this.subs[i - 1];
}
if (hasSuchSub.isStopped())
hasSuchSub.start();
return hasSuchSub;
}
/**
* Tries to subscribe to a specific publication on server.
* Syntactic sugar for @see sub.
*/
subscribe(pubname, ...args) {
return this.sub(pubname, args);
}
/**
* Starts listening server for basic DDP event running f each time the message arrives.
* Default suppoted events @see PUBLIC_EVENTS const.
* @example
* server.on('connected', () => {
* // you can show a success message here
* });
*
* server.on('disconnected', () => {
* // you can show a reconnection message here
* });
*/
on(event, f) {
return (0, ddpEventListener_js_1.ddpEventListener)(event, f, this);
}
/**
* Stops all reactivity.
*/
stopChangeListeners() {
this.onChangeFuncs = [];
}
/**
* Removes all documents like if it was removed by the server publication.
*/
clearData() {
return new Promise((resolve, reject) => {
let totalDocuments = 0;
Object.keys(this.collections).forEach((collection) => {
totalDocuments += Array.isArray(this.collections[collection]) ? this.collections[collection].length : 0;
});
if (totalDocuments === 0) {
resolve();
}
else {
let counter = 0;
const uniqueId = `${this._id}-${this._opGenId()}`;
const listener = this.on('removed', (m, id) => {
if (id == uniqueId) {
counter++;
if (counter == totalDocuments) {
listener.stop();
resolve();
}
}
});
Object.keys(this.collections).forEach((collection) => {
this.collections[collection].forEach((doc) => {
this.ddpConnection.emit('removed', {
msg: 'removed',
id: doc.id,
collection
}, uniqueId);
});
});
}
});
}
/**
* Imports the data like if it was published by the server.
*/
importData(data) {
return new Promise((resolve, reject) => {
const c = typeof data === 'string' ? ejson_1.default.parse(data) : data;
let totalDocuments = 0;
Object.keys(c).forEach((collection) => {
totalDocuments += Array.isArray(c[collection]) ? c[collection].length : 0;
});
let counter = 0;
const uniqueId = `${this._id}-${this._opGenId()}`;
const listener = this.on('added', (m, id) => {
if (id == uniqueId) {
counter++;
if (counter == totalDocuments) {
listener.stop();
resolve();
}
}
});
Object.keys(c).forEach((collection) => {
c[collection].forEach((doc) => {
const docFields = { ...doc };
delete docFields.id;
this.ddpConnection.emit('added', {
msg: 'added',
id: doc.id,
collection,
fields: docFields
}, uniqueId);
});
});
});
}
/**
* Exports the data
*/
exportData(format) {
if (format === undefined || format == 'string') {
return ejson_1.default.stringify(this.collections);
}
if (format == 'raw') {
return (0, fullCopy_js_1.fullCopy)(this.collections);
}
return undefined;
}
/**
* Marks every passed @see ddpSubscription object as ready like if it was done by the server publication.
*/
markAsReady(subs) {
return new Promise((resolve, reject) => {
const uniqueId = `${this._id}-${this._opGenId()}`;
this.ddpConnection.emit('ready', {
msg: 'ready',
subs: subs.map((sub) => sub._getId())
}, uniqueId);
const listener = this.on('ready', (_m, id) => {
if (id == uniqueId) {
listener.stop();
resolve();
}
});
});
}
}
exports.default = DDPClient;