Repository: Gregivy/simpleddp Branch: master Commit: 4bd415ee52e4 Files: 74 Total size: 465.7 KB Directory structure: gitextract_hjf1yyvb/ ├── .babelrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── core/ │ ├── .babelrc │ ├── .eslintrc │ ├── .gitignore │ ├── .npmignore │ ├── .npmrc │ ├── .travis.yml │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src/ │ │ ├── ddp.js │ │ ├── queue.js │ │ ├── socket.js │ │ └── utils.js │ └── test/ │ ├── e2e/ │ │ ├── connection.js │ │ ├── methods.js │ │ └── subscriptions.js │ ├── server/ │ │ ├── .meteor/ │ │ │ ├── .finished-upgraders │ │ │ ├── .gitignore │ │ │ ├── .id │ │ │ ├── packages │ │ │ ├── platforms │ │ │ ├── release │ │ │ └── versions │ │ ├── methods.js │ │ └── publications.js │ └── unit/ │ ├── ddp.js │ ├── queue.js │ ├── socket.js │ └── utils.js ├── custom_ejson.md ├── docs/ │ ├── ddpCollection.html │ ├── ddpEventListener.html │ ├── ddpOnChange.html │ ├── ddpReactiveCollection.html │ ├── ddpReactiveDocument.html │ ├── ddpReducer.html │ ├── ddpSubscription.html │ ├── index.html │ ├── scripts/ │ │ ├── main.js │ │ └── prettify/ │ │ ├── Apache-License-2.0.txt │ │ ├── lang-css.js │ │ └── prettify.js │ ├── simpleDDP.html │ └── styles/ │ ├── main.css │ ├── prettify-jsdoc.css │ └── prettify-tomorrow.css ├── jsdoc_conf.json ├── notes.md ├── package.json ├── src/ │ ├── classes/ │ │ ├── ddpCollection.js │ │ ├── ddpEventListener.js │ │ ├── ddpOnChange.js │ │ ├── ddpReactiveCollection.js │ │ ├── ddpReactiveDocument.js │ │ ├── ddpReducer.js │ │ └── ddpSubscription.js │ ├── helpers/ │ │ ├── fullCopy.js │ │ └── isequal.js │ └── simpleddp.js └── test/ ├── test_call.js ├── test_collectionfetch.js ├── test_importexport.js ├── test_onchange.js ├── test_reactive.js ├── test_reactiveone.js └── test_sub.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": ["env", "stage-0"], "plugins": [ "add-module-exports" ] } ================================================ FILE: .gitignore ================================================ .DS_Store /lib/ /node_modules/ ================================================ FILE: .npmignore ================================================ simpleddp.png /src/ /docs/ ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - 8 script: - npm test ================================================ FILE: CHANGELOG.md ================================================ # CHANGELOG ## 2.3.0 * `simpleddp-core` is now a part of `simpleddp`. * Fixed bug with setting `sort` in `reactive()` method of `ddpCollection` class (see https://github.com/Gregivy/simpleddp/issues/13). * Added new methods `skip()`, `limit()` for `ddpReactiveCollection` class. Both methods are syntactic sugar for `settings()` method. * Updated `sub()` method. The returned subscription will be restarted if the same subscription exists and is stopped (see https://github.com/Gregivy/simpleddp/issues/11). * Updated `connect()` method. The method now supports `maxTimeout` (see https://github.com/Gregivy/simpleddp/issues/18). * New tests added. ## 2.2.4 * Fixed bug with auto re-subscribing when `clearDataOnReconnection=true` (default). Pseudo removing messages arrived later than the first subscription. It was causing possible data loss. * Fixex bug with resolving `clearData()`. * Updated `simpleddp-core` package. * Small changes in plugin system, added event `clientReady`. ## 2.2.3 * Fixed bug with `ddpSubscription.restart` and `ddpSubscription.nosub` when error comes from the server. ## 2.2.2 * Fixed bug with `maxTimeout`. ## 2.2.1 * Fixed bug with `ddpReactiveCollection` sorting. In some cases data array didn't recieve valid updates. ## 2.2.0 * `restartSubsOnConnect` method renamed to `restartSubs`. * Added property `clearDataOnReconnection` to `simpleDDP` class constructor. * Docs improvments. ## 2.1.1 * Fixed bug with `ddpSubscription` restart (loosing arguments). * Fixed rare situation with *ddp* message *removed* arriving before any other. * API fix. ## 2.1.0 * Fixed dependencies vulnerabilities. * Added documentation for custom EJSON types. * Added `maxTimeout` to support the maximum wait for a response from the server to the method call. ## 2.0.2 * Fixed dependencies vulnerabilities. ## 2.0.1 * Fix. If `change` message arrives and no collection is found `simpleddp` acts like it is an `added` message. ## 2.0.0 * Added semantic versioning. * `call` renamed to `apply`. * New `call` works like `apply` but accepts parameters for server method as a list of function arguments. * `subid` property of the subscription object renamed to `subscriptionId`. * Added `subscribe` method. Works like `sub` but accepts parameters for server publication as a list of function arguments. ## 1.2.3 * Updated `simpleddp-core` package. ## 1.2.2 * Fixed bug with EJSON types. ## 1.2.1 * Updated `simpleddp-core` package. * Added support for putting method call at the beginning of the requests queue. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018-2019 Plyushch Gregory (Gregivy) 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. ================================================ FILE: README.md ================================================ [![npm version](https://badge.fury.io/js/simpleddp.svg)](https://badge.fury.io/js/simpleddp) [![Build Status](https://travis-ci.org/Gregivy/simpleddp.svg?branch=master)](https://travis-ci.org/Gregivy/simpleddp) [![Dependency Status](https://david-dm.org/gregivy/simpleddp.svg)](https://david-dm.org/gregivy/simpleddp) [![devDependency Status](https://david-dm.org/gregivy/simpleddp/dev-status.svg)](https://david-dm.org/gregivy/simpleddp#info=devDependencies)

# SimpleDDP 🥚 The aim of this library is to simplify the process of working with Meteor.js server over DDP protocol using external JS environments (like Node.js, Cordova, Ionic, ReactNative, etc). It is battle tested 🏰 in production and ready to use 🔨. If you like this project ⭐ is always welcome. **Important** SimpleDDP is written in ES6 and uses modern features like *promises*. Though its precompiled with Babel, your js environment must support ES6 features. So if you are planning to use SimpleDDP be sure that your js environment supports ES6 features or include polyfills yourself (like Babel Polyfill). Project uses [semantic versioning 2.0.0](https://semver.org/spec/v2.0.0.html). DDP (protocol) [specification](https://github.com/meteor/meteor/blob/devel/packages/ddp/DDP.md). ## [CHANGE LOG](https://github.com/Gregivy/simpleddp/blob/master/CHANGELOG.md) ## Install `npm install simpleddp --save` ## [Documentation](https://gregivy.github.io/simpleddp/simpleDDP.html) ## Plugins * [simpleddp-plugin-login](https://github.com/Gregivy/simpleddp-plugin-login) ## [Adding custom EJSON types](https://github.com/Gregivy/simpleddp/blob/master/custom_ejson.md) ⭐ ## Example First of all you need WebSocket implementation for your node app. We will use [isomorphic-ws](https://www.npmjs.com/package/isomorphic-ws) package for this since it works on the client and serverside. `npm install isomorphic-ws ws --save` Import/require `simpleDDP`. ```javascript const simpleDDP = require("simpleddp"); // nodejs const ws = require("isomorphic-ws"); ``` or ```javascript import simpleDDP from 'simpleDDP'; // ES6 import ws from 'isomorphic-ws'; ``` Now you should make a new simpleDDP instance. ```javascript let opts = { endpoint: "ws://someserver.com/websocket", SocketConstructor: ws, reconnectInterval: 5000 }; const server = new simpleDDP(opts); ``` Connection is not going to be established immediately after you create a simpleDDP instance. If you need to check your connection simply use `server.connected` property which is `true` if you are connected to the server, otherwise it's `false`. You can also add some events for connection status. ```javascript server.on('connected', () => { // do something }); server.on('disconnected', () => { // for example show alert to user }); server.on('error', (e) => { // global errors from server }); ``` As an alternative you can use a *async/await* style (or `then(...)`). ```javascript (async ()=>{ await server.connect(); // connection is ready here })(); ``` The next thing we are going to do is subscribing to some publications. ```javascript let userSub = server.subscribe("user_pub"); let otherSub = server.subscribe("other_pub",'param1',2); // you can specify arguments for subscription (async ()=>{ await userSub.ready(); let nextSub = server.subscribe("next_pub"); // subscribing after userSub is ready await nextSub.ready(); //all subs are ready here })(); ``` You can fetch all things you've subscribed for using [server.collection](https://gregivy.github.io/simpleddp/simpleDDP.html#collection) method. Also you can get reactive data sources (plain js objects which will be automatically updated if something changes on the server). ```javascript (async ()=>{ // call some method await server.call('somemethod'); let userSub = server.subscribe("user",userId); await userSub.ready(); // get non-reactive user object let user = server.collection('users').filter(user=>user.id==userId).fetch()[0]; // get reactive user object let userReactiveCursor = server.collection('users').filter(user=>user.id==userId).reactive().one(); let userReactiveObject = userReactiveCursor.data(); // observing the changes server.collection('users').filter(user=>user.id==userId).onChange(({prev,next})=>{ console.log('previus user data',state.prev); console.log('next user data',state.next); }); // observing changes in reactive data source userReactiveCursor.onChange((newData)=>{ console.log('new user state', newData); }); let participantsSub = server.subscribe("participants"); await participantsSub.ready(); let reactiveCollection = server.collection('participants').reactive(); // reactive reduce let reducedReactive = reactiveCollection.reduce((acc,val,i,arr)=>{ if (i{ console.log('new user state', newData); }); })(); ``` ================================================ FILE: core/.babelrc ================================================ { "presets": ["es2015", "stage-0"], "env": { "test": { "plugins": ["istanbul"] } } } ================================================ FILE: core/.eslintrc ================================================ { "extends": "eslint:recommended", "parser": "babel-eslint", "env": { "browser": true, "mocha": true, "node": true, "meteor": true }, "rules": { "brace-style": [2, "1tbs"], "comma-spacing": [2, {"before": false, "after": true}], "computed-property-spacing": [2, "never"], "indent": [2, 4], "linebreak-style": [2, "unix"], "new-cap": [2, {"capIsNew": false}], "no-console": [0], "no-multi-spaces": [0], "no-underscore-dangle": [0], "object-curly-spacing": [2, "never"], "one-var": [2, "never"], "quotes": [2, "double"], "semi": [2, "always"], "keyword-spacing": [2], "space-before-blocks": [2, "always"], "space-before-function-paren": [2, "always"], "space-in-parens": [2, "never"], "strict": [2, "never"] } } ================================================ FILE: core/.gitignore ================================================ node_modules/ npm-debug.log coverage/ .nyc_output lib/ .DS_Store ================================================ FILE: core/.npmignore ================================================ node_modules/ npm-debug.log coverage/ ================================================ FILE: core/.npmrc ================================================ package-lock=false ================================================ FILE: core/.travis.yml ================================================ language: node_js node_js: - 8 script: - npm run lint - npm run coverage - npm run coveralls # - npm run e2e-test ================================================ FILE: core/CHANGELOG.md ================================================ # 1.0.6 * Pausing message queue prevents holds every out-coming message. # 1.0.5 * Now disconnect event won't fire on failed reconnecting events. # 1.0.4 * Now method calls can be put at the beginning of the call queue. * Pause/continue outcoming ddp messages queue. * `autoReconnect` value is being saved on disconnect. # 1.0.3 * Updated `wolfy87-eventemitter` to 5.2.5. # 1.0.2 * Replaced id checking inside sub method. # 1.0.1 * Added EJSON support. * Added message object to connect emit. * Added session id. * Added boolean parameter `cleanQueue` to the `DDP` constructor. Determine whether to clean ddp message queue on disconnect or not. ================================================ FILE: core/LICENSE ================================================ The MIT License (MIT) 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. ================================================ FILE: core/README.md ================================================ [![npm version](https://badge.fury.io/js/simpleddp-core.svg)](https://badge.fury.io/js/simpleddp-core.js) [![Build Status](https://travis-ci.org/gregivy/simpleddp-core.svg?branch=master)](https://travis-ci.org/gregivy/simpleddp-core) [![Coverage Status](https://img.shields.io/coveralls/gregivy/simpleddp-core.svg)](https://coveralls.io/r/gregivy/simpleddp-core?branch=master) [![Dependency Status](https://david-dm.org/gregivy/simpleddp-core.svg)](https://david-dm.org/gregivy/simpleddp-core) [![devDependency Status](https://david-dm.org/gregivy/simpleddp-core/dev-status.svg)](https://david-dm.org/gregivy/simpleddp-core#info=devDependencies) # simpleddp-core A javascript isomorphic/universal ddp client (successor of [ddp.js](https://github.com/mondora/ddp.js)). ## What is it for? The purpose of this library is: - to set up and maintain a ddp connection with a ddp server, freeing the developer from having to do it on their own - to give the developer a clear, consistent API to communicate with the ddp server ## Install To install ddp.js using `npm`: npm install ddp.js or using `yarn`: yarn add ddp.js ## Example usage ```js const DDP = require("ddp.js"); const options = { endpoint: "ws://localhost:3000/websocket", SocketConstructor: WebSocket }; const ddp = new DDP(options); ddp.on("connected", () => { console.log("Connected"); }); const subId = ddp.sub("mySubscription"); ddp.on("ready", message => { if (message.subs.includes(subId)) { console.log("mySubscription ready"); } }); ddp.on("added", message => { console.log(message.collection); }); const myLoginParams = { user: { email: "user@example.com" }, password: "hunter2" }; const methodId = ddp.method("login", [myLoginParams]); ddp.on("result", message => { if (message.id === methodId && !message.error) { console.log("Logged in!"); } }); ``` ## Developing After cloning the repository, install `npm` dependencies with `npm install`. Run `npm test` to run unit tests, or `npm run dev` to have `mocha` re-run your tests when source or test files change. To run e2e tests, first [install meteor](https://www.meteor.com/install). Then, start the meteor server with `npm run start-meteor`. Finally, run `npm run e2e-test` to run the e2e test suite, or `npm run e2e-dev` to have `mocha` re-run the suite when source or test files change. ## Public API ### new DDP(options) Creates a new DDP instance. After being constructed, the instance will establish a connection with the DDP server and will try to maintain it open. #### Arguments - `options` **object** *required* Available options are: - `cleanQueue` **boolean** *optional* [default: `false`]: whether to clean ddp message queue on disconnect or not. - `endpoint` **string** *required*: the location of the websocket server. Its format depends on the type of socket you are using. - `SocketConstructor` **function** *required*: the constructor function that will be used to construct the socket. Meteor (currently the only DDP server available) supports websockets and SockJS sockets. So, practically speaking, this means that on the browser you can use either the browser's native WebSocket constructor or the SockJS constructor provided by the SockJS library. On the server you can use whichever library implements the websocket protocol (e.g. faye-websocket). - `autoConnect` **boolean** *optional* [default: `true`]: whether to establish the connection to the server upon instantiation. When `false`, one can manually establish the connection with the `connect` method. - `autoReconnect` **boolean** *optional* [default: `true`]: whether to try to reconnect to the server when the socket connection closes, unless the closing was initiated by a call to the `disconnect` method. - `reconnectInterval` **number** *optional* [default: 10000]: the interval in ms between reconnection attempts. #### Returns A new DDP instance, which is also an `EventEmitter` instance. --- ### DDP.method(name, params) Calls a remote method. #### Arguments - `name` **string** *required*: name of the method to call. - `params` **array** *required*: array of parameters to pass to the remote method. Pass an empty array if you do not wish to pass any parameters. #### Returns The unique `id` (string) corresponding to the method call. #### Example usage Server code: ```js Meteor.methods({ myMethod (param_0, param_1, param_2) { /* ... */ } }); ``` Client code: ```js const methodCallId = ddp.method("myMethod", [param_0, param_1, param_2]); ``` --- ### DDP.sub(name, params) Subscribes to a server publication. #### Arguments - `name` **string** *required*: name of the server publication. - `params` **array** *required*: array of parameters to pass to the server publish function. Pass an empty array if you do not wish to pass any parameters. #### Returns The unique `id` (string) corresponding to the subscription call. #### Example usage Server code: ```js Meteor.publish("myPublication", (param_0, param_1, param_2) { /* ... */ }); ``` Client code: ```js const subscriptionId = ddp.sub("myPublication", [param_0, param_1, param_2]); ``` --- ### DDP.unsub(id) Unsubscribes to a previously-subscribed server publication. #### Arguments - `id` **string** *required*: id of the subscription. #### Returns The `id` corresponding to the subscription call (not of much use, but I return it for consistency). --- ### DDP.connect() Connects to the ddp server. The method is called automatically by the class constructor if the `autoConnect` option is set to `true` (default behaviour). So there generally should be no need for the developer to call the method themselves. #### Arguments None #### Returns None --- ### DDP.disconnect() Disconnects from the ddp server by closing the `WebSocket` connection. You can listen on the `disconnected` event to be notified of the disconnection. #### Arguments None #### Returns None ## Public events ### Connection events - `connected`: emitted with no arguments when the DDP connection is established. - `disconnected`: emitted with no arguments when the DDP connection drops. ### Subscription events All the following events are emitted with one argument, the parsed DDP message. Further details can be found [on the DDP spec page](https://github.com/meteor/meteor/blob/devel/packages/ddp/DDP.md). - `ready` - `nosub` - `added` - `changed` - `removed` ### Method events All the following events are emitted with one argument, the parsed DDP message. Further details can be found [on the DDP spec page](https://github.com/meteor/meteor/blob/devel/packages/ddp/DDP.md). - `result` - `updated` ================================================ FILE: core/package.json ================================================ { "name": "simpleddp-core", "version": "1.0.6", "description": "ddp javascript client", "main": "lib/ddp.js", "scripts": { "build": "babel src --out-dir lib", "clean": "rimraf lib coverage", "coverage": "nyc --require babel-register --reporter=lcov --include src --all npm test", "coveralls": "cat ./coverage/lcov.info | coveralls", "dev": "npm test -- --watch", "lint": "eslint src test", "prepare": "npm run clean && npm run build", "test": "mocha --require babel-register --recursive test/unit", "start-meteor": "cd test/server/ && meteor", "e2e-test": "mocha --require babel-register --recursive test/e2e", "e2e-dev": "npm run e2e-test -- --watch" }, "repository": { "type": "git", "url": "https://github.com/gregivy/simpleddp-core" }, "keywords": [ "ddp", "meteor", "simpleddp", "core" ], "author": "Plyushch Gregory ", "license": "MIT", "bugs": { "url": "https://github.com/gregivy/simpleddp-core/issues" }, "homepage": "https://github.com/gregivy/simpleddp-core", "devDependencies": { "ajv": "^5.5.2", "babel-cli": "^6.26.0", "babel-eslint": "^8.0.1", "babel-plugin-istanbul": "^4.1.5", "babel-preset-es2015": "^6.24.1", "babel-preset-stage-0": "^6.24.1", "babel-register": "^6.26.0", "chai": "^4.1.2", "coveralls": "^3.0.0", "eslint": "^4.9.0", "faye-websocket": "^0.11.1", "mocha": "^4.0.1", "nyc": "^11.2.1", "sinon": "^4.0.2", "sinon-chai": "^2.14.0" }, "dependencies": { "ejson": "^2.1.2", "wolfy87-eventemitter": "^5.2.5" } } ================================================ FILE: core/src/ddp.js ================================================ import EventEmitter from "wolfy87-eventemitter"; import Queue from "./queue"; import Socket from "./socket"; import {contains, uniqueId} from "./utils"; const DDP_VERSION = "1"; const PUBLIC_EVENTS = [ // Subscription messages "ready", "nosub", "added", "changed", "removed", // Method messages "result", "updated", // Error messages "error" ]; const DEFAULT_RECONNECT_INTERVAL = 10000; export default class DDP extends EventEmitter { emit () { setTimeout(super.emit.bind(this, ...arguments), 0); } constructor (options) { super(); this.status = "disconnected"; //DDP session id this.sessionId = null; //clean queue on disconnect or not, default to false this.cleanQueue = (options.cleanQueue === true); // Default `autoConnect` and `autoReconnect` to true this.autoConnect = (options.autoConnect !== false); this.autoReconnect = (options.autoReconnect !== false); this.autoReconnectUserValue = this.autoReconnect; this.reconnectInterval = options.reconnectInterval || DEFAULT_RECONNECT_INTERVAL; this.messageQueue = new Queue(message => { if (this.status === "connected") { this.socket.send(message); return true; } else { return false; } }); this.socket = new Socket(options.SocketConstructor, options.endpoint); this.socket.on("open", () => { // When the socket opens, send the `connect` message // to establish the DDP connection let params = { msg: "connect", version: DDP_VERSION, support: [DDP_VERSION] }; if (this.sessionId) params.session = this.sessionId; this.socket.send(params); }); this.socket.on("close", () => { let oldStatus = this.status; this.status = "disconnected"; if (this.cleanQueue) this.messageQueue.empty(); if (oldStatus != "disconnected") this.emit("disconnected"); if (this.autoReconnect) { // Schedule a reconnection setTimeout( this.socket.open.bind(this.socket), this.reconnectInterval ); } }); this.socket.on("message:in", message => { if (message.msg === "connected") { this.status = "connected"; this.sessionId = message.session ? message.session : null; this.messageQueue.process(); this.emit("connected", message); } else if (message.msg === "ping") { // Reply with a `pong` message to prevent the server from // closing the connection this.socket.send({msg: "pong", id: message.id}); } else if (contains(PUBLIC_EVENTS, message.msg)) { this.emit(message.msg, message); } }); if (this.autoConnect) { this.connect(); } } connect () { this.autoReconnect = this.autoReconnectUserValue; this.socket.open(); } disconnect () { /* * If `disconnect` is called, the caller likely doesn't want the * the instance to try to auto-reconnect. Therefore we set the * `autoReconnect` flag to false. * Also we should remember autoReconnect value to restore it on connect. */ this.autoReconnectUserValue = this.autoReconnect; this.autoReconnect = false; this.sessionId = null; this.socket.close(); } pauseQueue() { this.messageQueue.pause(); } continueQueue() { this.messageQueue.continue(); } method (name, params, atBeginning = false) { const id = uniqueId(); this.messageQueue[atBeginning?'unshift':'push']({ msg: "method", id: id, method: name, params: params }); return id; } sub (name, params, id = uniqueId()) { this.messageQueue.push({ msg: "sub", id: id, name: name, params: params }); return id; } unsub (id) { this.messageQueue.push({ msg: "unsub", id: id }); return id; } } ================================================ FILE: core/src/queue.js ================================================ export default class Queue { /* * As the name implies, `consumer` is the (sole) consumer of the queue. * It gets called with each element of the queue and its return value * serves as a ack, determining whether the element is removed or not from * the queue, allowing then subsequent elements to be processed. */ constructor (consumer) { this.consumer = consumer; this.paused = false; this.queue = []; } pause () { this.paused = true; } continue () { this.paused = false; this.process(); } push (element) { this.queue.push(element); this.process(); } unshift (element) { this.queue.unshift(element); this.process(); } process (opts) { if (!this.paused && this.queue.length !== 0) { const ack = this.consumer(this.queue[0]); if (ack) { this.queue.shift(); if (!this.paused) this.process(); } } } empty () { this.queue = []; } } ================================================ FILE: core/src/socket.js ================================================ import EventEmitter from "wolfy87-eventemitter"; import EJSON from "ejson"; export default class Socket extends EventEmitter { constructor (SocketConstructor, endpoint) { super(); this.SocketConstructor = SocketConstructor; this.endpoint = endpoint; this.rawSocket = null; } send (object) { const message = EJSON.stringify(object); this.rawSocket.send(message); // Emit a copy of the object, as the listener might mutate it. this.emit("message:out", EJSON.parse(message)); } open () { /* * Makes `open` a no-op if there's already a `rawSocket`. This avoids * memory / socket leaks if `open` is called twice (e.g. by a user * calling `ddp.connect` twice) without properly disposing of the * socket connection. `rawSocket` gets automatically set to `null` only * when it goes into a closed or error state. This way `rawSocket` is * disposed of correctly: the socket connection is closed, and the * object can be garbage collected. */ if (this.rawSocket) { return; } this.rawSocket = new this.SocketConstructor(this.endpoint); /* * Calls to `onopen` and `onclose` directly trigger the `open` and * `close` events on the `Socket` instance. */ this.rawSocket.onopen = () => this.emit("open"); this.rawSocket.onclose = () => { this.rawSocket = null; this.emit("close"); }; /* * Calls to `onerror` trigger the `close` event on the `Socket` * instance, and cause the `rawSocket` object to be disposed of. * Since it's not clear what conditions could cause the error and if * it's possible to recover from it, we prefer to always close the * connection (if it isn't already) and dispose of the socket object. */ this.rawSocket.onerror = () => { // It's not clear what the socket lifecycle is when errors occurr. // Hence, to avoid the `close` event to be emitted twice, before // manually closing the socket we de-register the `onclose` // callback. if (this.rawSocket && this.rawSocket.onclose) delete this.rawSocket.onclose; // Safe to perform even if the socket is already closed this.rawSocket.close(); this.rawSocket = null; this.emit("close"); }; /* * Calls to `onmessage` trigger a `message:in` event on the `Socket` * instance only once the message (first parameter to `onmessage`) has * been successfully parsed into a javascript object. */ this.rawSocket.onmessage = message => { var object; try { object = EJSON.parse(message.data); } catch (ignore) { // Simply ignore the malformed message and return return; } // Outside the try-catch block as it must only catch EJSON parsing // errors, not errors that may occur inside a "message:in" event // handler this.emit("message:in", object); }; } close () { /* * Avoid throwing an error if `rawSocket === null` */ if (this.rawSocket) { this.rawSocket.close(); } } } ================================================ FILE: core/src/utils.js ================================================ var i = 0; export function uniqueId () { return (i++).toString(); } export function contains (array, element) { return array.indexOf(element) !== -1; } ================================================ FILE: core/test/e2e/connection.js ================================================ import chai, {expect} from "chai"; import {Client} from "faye-websocket"; import sinon from "sinon"; import sinonChai from "sinon-chai"; import DDP from "../../src/ddp"; chai.use(sinonChai); const options = { endpoint: "ws://localhost:3000/websocket", SocketConstructor: Client }; describe("connection", () => { var ddp = null; afterEach(done => { if (ddp) { ddp.on("disconnected", () => done()); ddp.disconnect(); ddp = null; } else { done(); } }); describe("connecting", () => { it("a connection is established on instantiation unless `autoConnect === false`", done => { /* * The test suceeds when the `connected` event is fired, signaling * the establishment of the connection. * If the event is never fired, the test times out and fails. */ ddp = new DDP(options); ddp.on("connected", () => { done(); }); }); it("a connection is not established on instantiation when `autoConnect === false`", done => { /* * The test succeeds if, 1s after the creation of the DDP instance, * a `connected` event has not been fired. */ const ddp = new DDP({ ...options, autoConnect: false }); const connectedHandler = sinon.spy(); ddp.on("connected", connectedHandler); setTimeout(() => { try { expect(connectedHandler).to.have.callCount(0); done(); } catch (e) { done(e); } }, 1000); }); it("a connection can be established manually when `autoConnect === false`", done => { /* * The test suceeds when the `connected` event is fired, signaling * the establishment of the connection. * If the event is never fired, the test times out and fails. */ ddp = new DDP({ ...options, autoConnect: false }); ddp.connect(); ddp.on("connected", () => { done(); }); }); it("manually connecting several times doesn't causes multiple simultaneous connections [CASE: `autoConnect === true`]", done => { /* * The test suceeds if 1s after having called `connect` several * times only one connection has been established. */ ddp = new DDP(options); const connectedSpy = sinon.spy(); ddp.connect(); ddp.connect(); ddp.connect(); ddp.connect(); ddp.on("connected", connectedSpy); setTimeout(() => { try { expect(connectedSpy).to.have.callCount(1); done(); } catch (e) { done(e); } }, 1000); }); it("manually connecting several times doesn't causes multiple simultaneous connections [CASE: `autoConnect === false`]", done => { /* * The test suceeds if 1s after having called `connect` several * times only one connection has been established. */ ddp = new DDP({ ...options, autoConnect: false }); const connectedSpy = sinon.spy(); ddp.connect(); ddp.connect(); ddp.connect(); ddp.connect(); ddp.on("connected", connectedSpy); setTimeout(() => { try { expect(connectedSpy).to.have.callCount(1); done(); } catch (e) { done(e); } }, 1000); }); }); describe("disconnecting", () => { it("the connection is closed when calling `disconnect`", done => { /* * The test suceeds when the `disconnected` event is fired, * signaling the termination of the connection. * If the event is never fired, the test times out and fails. */ const ddp = new DDP(options); ddp.on("connected", () => { ddp.disconnect(); }); ddp.on("disconnected", () => { done(); }); }); it("calling `disconnect` several times causes no issues", done => { /* * The test suceeds if: * - calling `disconnect` several times doesn't throw, both before * and after the `disconnected` event has been received * - one and only one `disconnected` event is fired (check after * 1s) */ const ddp = new DDP(options); const disconnectSpy = sinon.spy(() => { try { ddp.disconnect(); ddp.disconnect(); ddp.disconnect(); ddp.disconnect(); } catch (e) { done(e); } }); ddp.on("connected", () => { try { ddp.disconnect(); ddp.disconnect(); ddp.disconnect(); ddp.disconnect(); } catch (e) { done(e); } }); ddp.on("disconnected", disconnectSpy); setTimeout(() => { try { expect(disconnectSpy).to.have.callCount(1); done(); } catch (e) { done(e); } }, 1000); }); it("the connection is closed when calling `disconnect` and it's not re-established", done => { /* * The test suceeds if, 1s after the `disconnected` event has been * fired, there hasn't been any reconnection. */ const ddp = new DDP({ ...options, reconnectInterval: 10 }); const disconnectOnConnection = sinon.spy(() => { ddp.disconnect(); }); ddp.on("connected", disconnectOnConnection); ddp.on("disconnected", () => { setTimeout(() => { try { expect(disconnectOnConnection).to.have.callCount(1); done(); } catch (e) { done(e); } }, 1000); }); }); it("the connection is closed and re-established when the server closes the connection, unless `autoReconnect === true`", done => { /* * The test suceeds when the `connect` event is fired a second time * after the client gets disconnected from the server (occurrence * marked by the `disconnected` event). * If the event is never fired a second time, the test times out * and fails. */ ddp = new DDP({ ...options, reconnectInterval: 10 }); var callCount = 0; ddp.on("connected", () => { callCount += 1; if (callCount === 1) { ddp.method("disconnectMe", []); } if (callCount === 2) { done(); } }); }); it("the connection is closed and _not_ re-established when the server closes the connection and `autoReconnect === false`", done => { /* * The test suceeds if, 1s after the `disconnected` event has been * fired, there hasn't been any reconnection. */ const ddp = new DDP({ ...options, reconnectInterval: 10, autoReconnect: false }); const disconnectMe = sinon.spy(() => { ddp.method("disconnectMe", []); }); ddp.on("connected", disconnectMe); ddp.on("disconnected", () => { setTimeout(() => { try { expect(disconnectMe).to.have.callCount(1); done(); } catch (e) { done(e); } }, 1000); }); }); describe("ddp.js issue #22", () => { /* * We need to test that no `uncaughtException`-s are raised. Since * mocha adds a listener to the `uncaughtException` event which * causes tests to fail in an unexpected manner, we first remove * that listener, and then we restore it. Since it's not clear * _what_ mocha does with that listener, we try to lower the * meddling impact by doing all of our work inside the `it` block. */ it("no issues when sending messages while disconnected / while disconnecting", done => { /* * The test suceeds if, 100ms after the `disconnected` event * has been fired, there haven't been any uncaught exceptions. */ const catcher = sinon.spy(); const listeners = process.listeners("uncaughtException"); process.removeAllListeners("uncaughtException"); process.on("uncaughtException", catcher); const ddp = new DDP({ ...options, autoReconnect: false }); ddp.on("connected", () => { ddp.disconnect(); }); const interval = setInterval(() => { ddp.method("echo", []); }, 1); ddp.on("disconnected", () => { setTimeout(runAssertions, 100); }); const runAssertions = () => { clearInterval(interval); process.removeAllListeners("uncaughtException"); listeners.forEach(listener => { process.on("uncaughtException", listener); }); try { expect(catcher).to.have.callCount(0); done(); } catch (e) { done(e); } }; }); }); }); }); ================================================ FILE: core/test/e2e/methods.js ================================================ import {expect} from "chai"; import {Client} from "faye-websocket"; import DDP from "../../src/ddp"; describe("methods", () => { describe("calling a method", () => { const ddp = new DDP({ endpoint: "ws://localhost:3000/websocket", SocketConstructor: Client }); after(done => { ddp.on("disconnected", () => done()); ddp.disconnect(); }); it("invokes the method on the server and gets a `result` message with the response", done => { /* * The test suceeds when the `result` message for the echo method * call is received, and assertions all succeed. * If the `result` message is never received, the test times out * and fails. Naturally, the test also fails if assertions fail. */ const methodId = ddp.method("echo", [0, 1, 2, 3, 4]); ddp.on("result", message => { if (message.id !== methodId || message.error) { return; } try { expect(message.result).to.deep.equal([0, 1, 2, 3, 4]); done(); } catch (e) { done(e); } }); }); it("gets an `updated` message", done => { /* * The test suceeds when the `updated` message for the echo method * call is received. * If the `updated` message is never received, the test times out * and fails. */ const methodId = ddp.method("echo", [0, 1, 2, 3, 4]); ddp.on("updated", message => { if (message.methods.indexOf(methodId) !== -1) { done(); } }); }); }); }); ================================================ FILE: core/test/e2e/subscriptions.js ================================================ import {expect} from "chai"; import {Client} from "faye-websocket"; import DDP from "../../src/ddp"; describe("subscriptions", () => { describe("subscribing to a publication", () => { const ddp = new DDP({ endpoint: "ws://localhost:3000/websocket", SocketConstructor: Client }); after(done => { ddp.on("disconnected", () => done()); ddp.disconnect(); }); it("sends a sub call to the server and receives server-sent scubscription events", done => { /* * The test suceeds when the `ready` message for the echo * subscription is received, and assertions all succeed. * If the `ready` message is never received, the test times out * and fails. Naturally, the test also fails if assertions fail. */ const subId = ddp.sub("echo", [0, 1, 2, 3, 4]); const collections = {}; ddp.on("added", message => { collections[message.collection] = { ...collections[message.collection], [message.id]: { _id: message.id, ...message.fields } }; }); ddp.on("ready", message => { if (message.subs.indexOf(subId) === -1) { return; } try { expect(collections).to.deep.equal({ echoParameters: { "id_0": {_id: "id_0", param: 0}, "id_1": {_id: "id_1", param: 1}, "id_2": {_id: "id_2", param: 2}, "id_3": {_id: "id_3", param: 3}, "id_4": {_id: "id_4", param: 4} } }); done(); } catch (e) { done(e); } }); }); }); describe("unsubscribing from a publication", () => { const ddp = new DDP({ endpoint: "ws://localhost:3000/websocket", SocketConstructor: Client }); after(done => { ddp.on("disconnected", () => done()); ddp.disconnect(); }); it("sends an unsub call to the server and receives a nosub unsubscriptions confirmation event", done => { /* * The test suceeds when the `nosub` message for the echo * subscription is received. If the `nosub` message is never * received, the test times out and fails. */ const subId = ddp.sub("echo", [0, 1, 2, 3, 4]); ddp.on("ready", message => { if (message.subs.indexOf(subId) === -1) { return; } ddp.unsub(subId); }); ddp.on("nosub", message => { if (message.id !== subId) { return; } done(); }); }); }); describe("getting unsubscribed from a publication", () => { const ddp = new DDP({ endpoint: "ws://localhost:3000/websocket", SocketConstructor: Client }); after(done => { ddp.on("disconnected", () => done()); ddp.disconnect(); }); it("receives a nosub unsubscriptions event", function (done) { /* * The test suceeds when the `nosub` message for the echo * subscription is received. If the `nosub` message is never * received, the test times out and fails. * The server will stop the subscription after about 1s, so there * is no need to terminate it with an `unsub`. We will however * increase the test timeout to 3s to account for the delay. */ this.timeout(3000); const subId = ddp.sub("autoTerminating"); var subReady = false; ddp.on("ready", message => { if (message.subs.indexOf(subId) !== -1) { subReady = true; } }); ddp.on("nosub", message => { if (message.id !== subId) { return; } try { // Ensure the subscription got marked as ready. expect(subReady).to.equal(true); done(); } catch (e) { done(e); } }); }); }); }); ================================================ FILE: core/test/server/.meteor/.finished-upgraders ================================================ # This file contains information which helps Meteor properly upgrade your # app when you run 'meteor update'. You should check it into version control # with your project. notices-for-0.9.0 notices-for-0.9.1 0.9.4-platform-file notices-for-facebook-graph-api-2 1.2.0-standard-minifiers-package 1.2.0-meteor-platform-split 1.2.0-cordova-changes 1.2.0-breaking-changes 1.3.0-split-minifiers-package 1.4.0-remove-old-dev-bundle-link 1.4.1-add-shell-server-package 1.4.3-split-account-service-packages 1.5-add-dynamic-import-package ================================================ FILE: core/test/server/.meteor/.gitignore ================================================ local ================================================ FILE: core/test/server/.meteor/.id ================================================ # This file contains a token that is unique to your project. # Check it into your repository along with the rest of this directory. # It can be used for purposes such as: # - ensuring you don't accidentally deploy one app on top of another # - providing package authors with aggregated statistics ydh6g2121jjwyqq7agh ================================================ FILE: core/test/server/.meteor/packages ================================================ # Meteor packages used by this project, one per line. # Check this file (and the other files in this directory) into your repository. # # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. meteor-base@1.1.0 # Packages every Meteor app needs to have mobile-experience@1.0.5 # Packages for a great mobile UX mongo@1.2.2 # The database Meteor supports right now blaze-html-templates # Compile .html files into Meteor Blaze views session@1.1.7 # Client-side reactive dictionary for your app jquery@1.11.10 # Helpful client-side library tracker@1.1.3 # Meteor's client-side reactive programming library es5-shim@4.6.15 # ECMAScript 5 compatibility for older browsers. standard-minifier-css standard-minifier-js shell-server dynamic-import ================================================ FILE: core/test/server/.meteor/platforms ================================================ server browser ================================================ FILE: core/test/server/.meteor/release ================================================ METEOR@1.5.2.2 ================================================ FILE: core/test/server/.meteor/versions ================================================ allow-deny@1.0.9 autoupdate@1.3.12 babel-compiler@6.20.0 babel-runtime@1.0.1 base64@1.0.10 binary-heap@1.0.10 blaze@2.3.2 blaze-html-templates@1.1.2 blaze-tools@1.0.10 boilerplate-generator@1.2.0 caching-compiler@1.1.9 caching-html-compiler@1.1.2 callback-hook@1.0.10 check@1.2.5 ddp@1.3.1 ddp-client@2.1.3 ddp-common@1.2.9 ddp-server@2.0.2 deps@1.0.12 diff-sequence@1.0.7 dynamic-import@0.1.3 ecmascript@0.8.3 ecmascript-runtime@0.4.1 ecmascript-runtime-client@0.4.3 ecmascript-runtime-server@0.4.1 ejson@1.0.14 es5-shim@4.6.15 geojson-utils@1.0.10 hot-code-push@1.0.4 html-tools@1.0.11 htmljs@1.0.11 http@1.2.12 id-map@1.0.9 jquery@1.11.10 launch-screen@1.1.1 livedata@1.0.18 logging@1.1.19 meteor@1.7.2 meteor-base@1.1.0 minifier-css@1.2.16 minifier-js@2.1.4 minimongo@1.3.3 mobile-experience@1.0.5 mobile-status-bar@1.0.14 modules@0.10.0 modules-runtime@0.8.0 mongo@1.2.2 mongo-dev-server@1.0.1 mongo-id@1.0.6 npm-mongo@2.2.33 observe-sequence@1.0.16 ordered-dict@1.0.9 promise@0.9.0 random@1.0.10 reactive-dict@1.1.9 reactive-var@1.0.11 reload@1.1.11 retry@1.0.9 routepolicy@1.0.12 session@1.1.7 shell-server@0.2.4 spacebars@1.0.15 spacebars-compiler@1.1.3 standard-minifier-css@1.3.5 standard-minifier-js@2.1.2 templating@1.3.2 templating-compiler@1.3.3 templating-runtime@1.3.2 templating-tools@1.1.2 tracker@1.1.3 ui@1.0.13 underscore@1.0.10 url@1.1.0 webapp@1.3.19 webapp-hashing@1.0.9 ================================================ FILE: core/test/server/methods.js ================================================ Meteor.methods({ echo: function () { return _.toArray(arguments); }, disconnectMe: function () { this.connection.close(); } }); ================================================ FILE: core/test/server/publications.js ================================================ Meteor.publish("echo", function () { var self = this; _.each(arguments, function (param, index) { self.added("echoParameters", "id_" + index, {param: param}); }); self.ready(); }); Meteor.publish("autoTerminating", function () { var self = this; self.added("autoTerminating", "id", {}); self.ready(); setTimeout(function () { self.stop(); }, 1000); }); ================================================ FILE: core/test/unit/ddp.js ================================================ import chai, {expect} from "chai"; import sinon from "sinon"; import sinonChai from "sinon-chai"; chai.use(sinonChai); import DDP from "../../src/ddp"; import Socket from "../../src/socket"; class SocketConstructorMock { send () {} close () {} } const options = { SocketConstructor: SocketConstructorMock }; describe("`DDP` class", () => { describe("`constructor` method", () => { beforeEach(() => { sinon.stub(Socket.prototype, "on"); sinon.stub(Socket.prototype, "open"); }); afterEach(() => { Socket.prototype.on.restore(); Socket.prototype.open.restore(); }); it("instantiates a `Socket`", () => { const ddp = new DDP(options); expect(ddp.socket).to.be.an.instanceOf(Socket); }); it("registers handlers for `socket` events", () => { const ddp = new DDP(options); expect(ddp.socket.on).to.have.always.been.calledWithMatch( sinon.match.string, sinon.match.func ); }); it("opens a connection to the server (by calling `socket.open`) unless `options.autoConnect === false`", () => { const ddp = new DDP(options); expect(ddp.socket.open).to.have.callCount(1); }); it("does not open a connection when `options.autoConnect === false`", () => { const ddp = new DDP({ ...options, autoConnect: false }); expect(ddp.socket.open).to.have.callCount(0); }); it("sets the instance `reconnectInterval` to `options.reconnectInterval` if specified", () => { const ddp = new DDP({ ...options, reconnectInterval: 1, autoConnect: false }); expect(ddp.reconnectInterval).to.equal(1); }); it("sets the instance `reconnectInterval` to a default value if `options.reconnectInterval` is not specified", () => { const ddp = new DDP({ ...options, autoConnect: false }); expect(ddp.reconnectInterval).to.equal(10000); }); }); describe("`method` method", () => { it("sends a DDP `method` message", () => { const ddp = new DDP(options); ddp.messageQueue.push = sinon.spy(); const id = ddp.method("name", ["param"]); expect(ddp.messageQueue.push).to.have.been.calledWith({ msg: "method", id: id, method: "name", params: ["param"] }); }); it("returns the method's `id`", () => { const ddp = new DDP(options); ddp.messageQueue.push = sinon.spy(); const id = ddp.method("name", ["param"]); expect(id).to.be.a("string"); }); }); describe("`sub` method", () => { it("sends a DDP `sub` message", () => { const ddp = new DDP(options); ddp.messageQueue.push = sinon.spy(); const id = ddp.sub("name", ["param"]); expect(ddp.messageQueue.push).to.have.been.calledWith({ msg: "sub", id: id, name: "name", params: ["param"] }); }); it("returns the sub's `id`", () => { const ddp = new DDP(options); ddp.messageQueue.push = sinon.spy(); const id = ddp.sub("name", ["param"]); expect(id).to.be.a("string"); }); it("generates unique id when not specified", () => { const ddp = new DDP(options); var ids = []; ids.push(ddp.sub("echo", [ 0 ])); ids.push(ddp.sub("echo", [ 0 ])); expect(ids[0]).to.be.a("string"); expect(ids[1]).to.be.a("string"); expect(ids[0]).not.to.equal(ids[1]); }); it("allows manually specifying sub's id", () => { const ddp = new DDP(options); const subId = ddp.sub("echo", [ 0 ], "12345"); expect(subId).to.equal("12345"); }); }); describe("`unsub` method", () => { it("sends a DDP `unsub` message", () => { const ddp = new DDP(options); ddp.messageQueue.push = sinon.spy(); const id = ddp.unsub("id"); expect(ddp.messageQueue.push).to.have.been.calledWith({ msg: "unsub", id: id }); }); it("returns the sub's `id`", () => { const ddp = new DDP(options); ddp.messageQueue.push = sinon.spy(); const id = ddp.unsub("id"); expect(id).to.be.a("string"); expect(id).to.equal("id"); }); }); describe("`connect` method", () => { beforeEach(() => { sinon.stub(Socket.prototype, "open"); }); afterEach(() => { Socket.prototype.open.restore(); }); it("opens the WebSocket connection", () => { const ddp = new DDP(options); Socket.prototype.open.reset(); ddp.connect(); expect(ddp.socket.open).to.have.callCount(1); }); }); describe("`disconnect` method", () => { beforeEach(() => { sinon.stub(Socket.prototype, "close"); }); afterEach(() => { Socket.prototype.close.restore(); }); it("closes the WebSocket connection", () => { const ddp = new DDP(options); ddp.disconnect(); expect(ddp.socket.close).to.have.callCount(1); }); it("sets the `autoReconnect` flag to false", () => { const ddp = new DDP(options); ddp.disconnect(); expect(ddp.autoReconnect).to.equal(false); }); }); describe("`socket` `open` handler", () => { beforeEach(() => { sinon.stub(global, "setTimeout").callsFake(fn => fn()); }); afterEach(() => { global.setTimeout.restore(); }); it("sends the `connect` DDP message", () => { const ddp = new DDP(options); ddp.socket.send = sinon.spy(); ddp.socket.emit("open"); expect(ddp.socket.send).to.have.been.calledWith({ msg: "connect", version: "1", support: ["1"] }); }); }); describe("`socket` `close` handler", () => { beforeEach(() => { sinon.stub(global, "setTimeout").callsFake(fn => fn()); }); afterEach(() => { global.setTimeout.restore(); }); it("emits the `disconnected` event if was 'connected'", () => { const ddp = new DDP(options); ddp.status = "connected"; ddp.emit = sinon.spy(); ddp.socket.emit("close"); expect(ddp.emit).to.have.been.calledWith("disconnected"); }); it("sets the status to `disconnected`", () => { const ddp = new DDP(options); ddp.status = "connected"; ddp.emit = sinon.spy(); ddp.socket.emit("close"); expect(ddp.status).to.equal("disconnected"); }); it("schedules a reconnection unless `options.autoReconnect === false`", () => { const ddp = new DDP(options); ddp.socket.open = sinon.spy(); ddp.socket.emit("close"); expect(ddp.socket.open).to.have.callCount(1); }); it("doesn't schedule a reconnection when `options.autoReconnect === false`", () => { const ddp = new DDP({ ...options, autoReconnect: false }); ddp.socket.open = sinon.spy(); ddp.socket.emit("close"); expect(ddp.socket.open).to.have.callCount(0); }); }); describe("`socket` `message:in` handler", () => { beforeEach(() => { sinon.stub(global, "setTimeout").callsFake(fn => fn()); }); afterEach(() => { global.setTimeout.restore(); }); it("responds to `ping` DDP messages", () => { const ddp = new DDP(options); ddp.socket.send = sinon.spy(); ddp.socket.emit("message:in", { id: "id", msg: "ping" }); expect(ddp.socket.send).to.have.been.calledWith({ id: "id", msg: "pong" }); }); it("triggers `messageQueue` processing upon connection", () => { const ddp = new DDP(options); ddp.emit = sinon.spy(); ddp.messageQueue.process = sinon.spy(); ddp.socket.emit("message:in", {msg: "connected"}); expect(ddp.messageQueue.process).to.have.callCount(1); }); it("sets the status to `connected` upon connection", () => { const ddp = new DDP(options); ddp.emit = sinon.spy(); ddp.socket.emit("message:in", {msg: "connected"}); expect(ddp.status).to.equal("connected"); }); it("emits public DDP messages as events", () => { const ddp = new DDP(options); ddp.emit = sinon.spy(); const message = { id: "id", msg: "result" }; ddp.socket.emit("message:in", message); expect(ddp.emit).to.have.been.calledWith("result", message); }); it("ignores unknown (or non public) DDP messages", () => { const ddp = new DDP(options); ddp.emit = sinon.spy(); const message = { id: "id", msg: "not-a-ddp-message" }; ddp.socket.emit("message:in", message); expect(ddp.emit).to.have.callCount(0); }); }); describe("`messageQueue` consumer", () => { it("acks if `status` is `connected`", () => { const ddp = new DDP(options); ddp.status = "connected"; const ack = ddp.messageQueue.consumer({}); expect(ack).to.equal(true); }); it("doesn't ack if `status` is `disconnected`", () => { const ddp = new DDP(options); ddp.status = "disconnected"; const ack = ddp.messageQueue.consumer({}); expect(ack).to.equal(false); }); }); }); ================================================ FILE: core/test/unit/queue.js ================================================ import chai, {expect} from "chai"; import sinon from "sinon"; import sinonChai from "sinon-chai"; chai.use(sinonChai); import Queue from "../../src/queue"; describe("`Queue` class", () => { describe("`push` method", () => { it("adds an element to the queue", () => { const q = new Queue(); q.process = sinon.spy(); const element = {}; q.push(element); expect(q.queue).to.include(element); }); it("triggers processing", () => { const q = new Queue(); q.process = sinon.spy(); const element = {}; q.push(element); expect(q.process).to.have.callCount(1); }); }); describe("`process` method", () => { it("calls the consumer on each element of the queue", () => { const consumer = sinon.spy(() => true); const q = new Queue(consumer); q.queue = [0, 1, 2]; q.process(); expect(consumer).to.have.been.calledWith(0); expect(consumer).to.have.been.calledWith(1); expect(consumer).to.have.been.calledWith(2); expect(consumer).to.have.callCount(3); }); it("removes elements from the queue", () => { const consumer = sinon.spy(() => true); const q = new Queue(consumer); q.queue = [0, 1, 2]; q.process(); expect(q.queue.length).to.equal(0); }); it("doesn't remove elements from the queue if the consumer doesn't ack", () => { const consumer = sinon.spy(() => false); const q = new Queue(consumer); q.queue = [0, 1, 2]; q.process(); expect(consumer).to.have.been.calledWith(0); expect(consumer).to.have.callCount(1); expect(q.queue.length).to.equal(3); }); }); describe("`empty` method", () => { it("empties the queue", () => { const q = new Queue(); q.process = sinon.spy(); const element = {}; q.push(element); expect(q.queue.length).to.equal(1); q.empty(); expect(q.queue.length).to.equal(0); }); }); }); ================================================ FILE: core/test/unit/socket.js ================================================ import chai, {expect} from "chai"; import sinon from "sinon"; import sinonChai from "sinon-chai"; chai.use(sinonChai); import Socket from "../../src/socket"; import EJSON from "ejson"; class SocketConstructorMock { close () {} send () {} } describe("`Socket` class", () => { describe("`send` method", () => { it("sends a message through the `rawSocket`", () => { const socket = new Socket(); socket.rawSocket = { send: sinon.spy() }; socket.send({}); expect(socket.rawSocket.send).to.have.callCount(1); }); it("stringifies the object to send", () => { const socket = new Socket(); socket.rawSocket = { send: sinon.spy() }; const object = { a: "a" }; const expectedMessage = EJSON.stringify(object); socket.send(object); expect(socket.rawSocket.send).to.have.been.calledWith(expectedMessage); }); it("emits a `message:out` event", () => { const socket = new Socket(); socket.rawSocket = { send: sinon.spy() }; socket.emit = sinon.spy(); const object = { a: "a" }; socket.send(object); expect(socket.emit).to.have.been.calledWith("message:out", object); }); }); describe("`open` method", () => { it("no-op if `rawSocket` is already defined", () => { const socket = new Socket(SocketConstructorMock); const rawSocket = {}; socket.rawSocket = rawSocket; socket.open(); // Test, for instance, that `rawSocket` has not been replaced. expect(socket.rawSocket).to.equal(rawSocket); }); it("instantiates a `SocketConstructor`", () => { const socket = new Socket(SocketConstructorMock); socket.open(); expect(socket.rawSocket).to.be.an.instanceOf(SocketConstructorMock); }); it("registers handlers for `rawSocket` events", () => { const socket = new Socket(SocketConstructorMock); socket.open(); expect(socket.rawSocket.onopen).to.be.a("function"); expect(socket.rawSocket.onclose).to.be.a("function"); expect(socket.rawSocket.onerror).to.be.a("function"); expect(socket.rawSocket.onmessage).to.be.a("function"); }); }); describe("`close` method", () => { it("closes the `rawSocket`", () => { const socket = new Socket(SocketConstructorMock); socket.open(); socket.rawSocket.close = sinon.spy(); socket.close(); expect(socket.rawSocket.close).to.have.callCount(1); }); it("doesn't throw if there's no `rawSocket`", () => { const socket = new Socket(SocketConstructorMock); const peacemaker = () => { socket.close(); }; expect(peacemaker).not.to.throw(); }); }); describe("`rawSocket` `onopen` handler", () => { it("emits an `open` event", () => { const socket = new Socket(SocketConstructorMock); const handler = sinon.spy(); socket.on("open", handler); socket.open(); socket.rawSocket.onopen(); expect(handler).to.have.callCount(1); }); }); describe("`rawSocket` `onclose` handler", () => { it("emits a `close` event", () => { const socket = new Socket(SocketConstructorMock); const handler = sinon.spy(); socket.on("close", handler); socket.open(); socket.rawSocket.onclose(); expect(handler).to.have.callCount(1); }); it("null-s the `rawSocket` property", () => { const socket = new Socket(SocketConstructorMock); socket.open(); socket.rawSocket.onclose(); expect(socket.rawSocket).to.equal(null); }); }); describe("`rawSocket` `onerror` handler", () => { it("closes `rawSocket` (by calling `rawSocket.close`)", () => { const socket = new Socket(SocketConstructorMock); socket.open(); const rawSocket = socket.rawSocket; rawSocket.close = sinon.spy(); socket.rawSocket.onerror(); expect(rawSocket.close).to.have.callCount(1); }); it("de-registers the `rawSocket.onclose` callback", () => { const socket = new Socket(SocketConstructorMock); socket.open(); const rawSocket = socket.rawSocket; expect(rawSocket).to.have.property("onclose"); socket.rawSocket.onerror(); expect(rawSocket).not.to.have.property("onclose"); }); it("emits a `close` event", () => { const socket = new Socket(SocketConstructorMock); const handler = sinon.spy(); socket.on("close", handler); socket.open(); socket.rawSocket.onerror(); expect(handler).to.have.callCount(1); }); it("null-s the `rawSocket` property", () => { const socket = new Socket(SocketConstructorMock); socket.open(); socket.rawSocket.onerror(); expect(socket.rawSocket).to.equal(null); }); }); describe("`rawSocket` `onmessage` handler", () => { it("parses message data into an object", () => { const socket = new Socket(SocketConstructorMock); sinon.stub(EJSON, "parse"); socket.open(); socket.rawSocket.onmessage({data: "message"}); expect(EJSON.parse).to.have.been.calledWith("message"); EJSON.parse.restore(); }); it("ignores malformed messages", () => { const socket = new Socket(SocketConstructorMock); sinon.stub(EJSON, "parse").throws(); socket.open(); expect(socket.rawSocket.onmessage).not.to.throw(); EJSON.parse.restore(); }); it("parses correctly EJSON-specific data types", function () { var socket = new Socket(SocketConstructorMock); var testDate = new Date(); sinon.stub(EJSON, "parse"); socket.open(); socket.rawSocket.onmessage({data: testDate}); expect(EJSON.parse).to.have.been.calledWith(testDate); EJSON.parse.restore(); }); it("emits `message:in` events", () => { const socket = new Socket(SocketConstructorMock); const handler = sinon.spy(); socket.on("message:in", handler); socket.open(); socket.rawSocket.onmessage({data: EJSON.stringify({a: "a"})}); expect(handler).to.have.callCount(1); expect(handler).to.have.been.calledWith({a: "a"}); }); }); }); ================================================ FILE: core/test/unit/utils.js ================================================ import {expect} from "chai"; import {contains, uniqueId} from "../../src/utils"; describe("`utils` object", () => { describe("`contains` function", () => { it("returns true if the first parameter contains the second parameter", () => { const array = ["element"]; const element = "element"; expect(contains(array, element)).to.equal(true); }); it("returns false if the first parameter doesn't contain the second parameter", () => { const array = ["element"]; const element = "different-element"; expect(contains(array, element)).to.equal(false); }); }); describe("`uniqueId` function", () => { it("should return a different string each time it's called", () => { const ret1 = uniqueId(); const ret2 = uniqueId(); expect(ret1).not.to.equal(ret2); }); }); }); ================================================ FILE: custom_ejson.md ================================================ # Adding custom EJSON types Adding custom EJSON types is as simple as it is in Meteor. First install `ejson` package: `npm install ejson --save` Import/require `EJSON`. ```javascript const EJSON = require("ejson"); // nodejs ``` or ```javascript import EJSON from 'ejson'; // ES6 ``` And use method `addType` ```javascript class Distance { constructor(value, unit) { this.value = value; this.unit = unit; } // Convert our type to JSON. toJSONValue() { return { value: this.value, unit: this.unit }; } // Unique type name. typeName() { return 'Distance'; } } EJSON.addType('Distance', function fromJSONValue(json) { return new Distance(json.value, json.unit); }); ``` *Don't forget to do the same as above in the server side code!* [Read more in Meteor Docs](https://docs.meteor.com/api/ejson.html) ## Example, adding Decimal support `npm install ejson decimal.js --save` ```javascript // ejson_decimal.js import EJSON from 'ejson'; import Decimal from 'decimal.js'; Decimal.prototype.typeName = function() { return 'Decimal'; }; Decimal.prototype.toJSONValue = function () { return this.toJSON(); }; Decimal.prototype.clone = function () { return Decimal(this.toString()); }; EJSON.addType('Decimal', function (str) { return Decimal(str); }); export { Decimal }; // now you can use Decimal in your method calls, subscriptions // and all the 'Decimal' data from server will be converted to Decimal object on client ``` ## Example, adding MongoId support `npm install ejson decimal.js --save` ```javascript // ejson_decimal.js import EJSON from 'ejson'; class MongoObjectId { constructor(str) { this.str = str; } // Convert our type to JSON. toJSONValue() { return this.value(); } // Unique type name. typeName() { return 'oid'; } value() { return this.str; } } EJSON.addType('oid', function fromJSONValue(str) { return new MongoObjectId(str); }); export { MongoObjectId }; ``` ================================================ FILE: docs/ddpCollection.html ================================================ SimpleDDP Docs

Class: ddpCollection

ddpCollection

DDP collection class.

new ddpCollection (name, server)

Name Type Description
name String

Collection name.

server simpleDDP

simpleDDP instance.

Classes

ddpCollection

Methods

exportData (format)string | Object

Exports data from the collection.

Name Type Default Description
format string 'string' optional

If 'string' then returns EJSON string, if 'raw' returns js object.

Returns:
Type Description
string | Object

fetch (settings)Object

Returns collection data based on filter and on passed settings. Supports skip, limit and sort. Order is 'filter' then 'sort' then 'skip' then 'limit'.

Name Type Default Description
settings Object {skip:0,limit:Infinity,sort:null} optional

Skip and limit are numbers or Infinity, sort is a standard js array sort function.

Returns:
Type Description
Object

filter (f)this

Allows to specify specific documents inside the collection for reactive data and fetching.

Name Type Description
f function

Filter function, recieves as arguments object, index and array.

Returns:
Type Description
this

importData (data)

Imports data inside the collection and emits all relevant events. Both string and JS object types are supported.

Name Type Description
data string | Object

EJSON string or EJSON or js object.

onChange (f, filter)Object

Returns change observer.

Name Type Description
f function
filter function
See:
Returns:
Type Description
Object
  • @see ddpOnChange

reactive (settings)Object

Returns reactive collection object.

Name Type Default Description
settings Object {skip:0,limit:Infinity,sort:null} optional
See:
Returns:
Type Description
Object
  • @see ddpReactiveCollection
================================================ FILE: docs/ddpEventListener.html ================================================ SimpleDDP Docs

Class: ddpEventListener

ddpEventListener

DDP event listener class.

new ddpEventListener (eventname, f, ddplink)

Name Type Description
eventname String

Event name.

f function

Function to run when event is fired.

ddplink simpleDDP

simpleDDP instance.

Classes

ddpEventListener

Methods

start ()

Usually you won't need this unless you stopped the @see ddpEventListener.

See:
  • ddpEventListener starts on creation.

stop ()

Stops listening for server event messages. You can start any stopped @see ddpEventListener at any time using ddpEventListener.start().

================================================ FILE: docs/ddpOnChange.html ================================================ SimpleDDP Docs

Class: ddpOnChange

ddpOnChange

DDP change listener class.

new ddpOnChange (obj, inst, listenersArray)

Name Type Default Description
obj Object

Describes changes of interest.

inst *

Event handler instance.

listenersArray simpleDDP 'onChangeFuncs' optional

Property name of event handler instance, array of listeners.

Classes

ddpOnChange

Methods

start ()

Start change listener. This method is being called on instance creation.

stop ()

Stops change listener.

================================================ FILE: docs/ddpReactiveCollection.html ================================================ SimpleDDP Docs

Class: ddpReactiveCollection

ddpReactiveCollection

A reactive collection class.

new ddpReactiveCollection (ddpCollection, skiplimit)

Name Type Default Description
ddpCollection ddpCollection

Instance of @see ddpCollection class.

skiplimit Object {skip:0,limit:Infinity} optional

Object for declarative reactive collection slicing.

Classes

ddpReactiveCollection

Methods

_updateReactiveObjects ()

Sends new object state for every associated reactive object.

count ()Object

Reactive length of the local collection.

Returns:
Type Description
Object
  • Object with reactive length of the local collection. {result}

data ()Array

Returns reactive local collection with applied sorting, skip and limit. This returned array is being mutated within this class instance.

Returns:
Type Description
Array
  • Local collection with applied sorting, skip and limit.

map (f)ddpReducer

Maps reactive local collection to another reactive array. Specified function form https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/map.

Name Type Description
f function

Function that produces an element of the new Array.

Returns:
Type Description
ddpReducer
  • Object that allows to get reactive mapped data @see ddpReducer.

onChange (f)

Runs a function every time a change occurs.

Name Type Description
f function

Function which recieves new collection at each change.

one (settings)ddpReactiveDocument

Returns a reactive object which fields are always the same as the first object in the collection.

Name Type Default Description
settings Object {preserve:false} optional

Settings for reactive object. Use {preserve:true} if you want to keep object on remove.

Returns:
Type Description
ddpReactiveDocument
  • Object that allows to get reactive object based on reduced reactive local collection @see ddpReactiveDocument.

reduce (f, initialValue)ddpReducer

Reduces reactive local collection. Specified function form https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce.

Name Type Description
f function

Function to execute on each element in the array.

initialValue *

Value to use as the first argument to the first call of the function.

Returns:
Type Description
ddpReducer
  • Object that allows to get reactive object based on reduced reactive local collection @see ddpReducer.

settings (skiplimit)

Update ddpReactiveCollection settings.

Name Type Default Description
skiplimit Object {skip:0,limit:Infinity} optional

Object for declarative reactive collection slicing.

sort (f)this

Sorts local collection according to specified function. Specified function form https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/sort.

Name Type Description
f function

A function used for sorting.

Returns:
Type Description
this

start ()

Start reactivity. This method is being called on instance creation. Also starts every associated reactive object.

stop ()

Stops reactivity. Also stops associated reactive objects.

================================================ FILE: docs/ddpReactiveDocument.html ================================================ SimpleDDP Docs

Class: ddpReactiveDocument

ddpReactiveDocument

A reactive document class.

new ddpReactiveDocument (ddpReactiveCollectionInstance, settings)

Name Type Default Description
ddpReactiveCollectionInstance ddpReactiveCollection

Instance of @see ddpReactiveCollection class.

settings Object {preserve:false} optional

Settings for reactive object. When preserve is true, reactive object won't change when corresponding object is being deleted.

Classes

ddpReactiveDocument

Methods

data ()Object

Returns reactive document.

Returns:
Type Description
Object

onChange (f)

Runs a function every time a change occurs.

Name Type Description
f function

Function which recieves a new value at each change.

settings (settings)

Change reactivity settings.

Name Type Description
settings Object
Name Type Description
preserve boolean

When preserve is true,reactive object won't change when corresponding object is being deleted.

start ()

Starts reactiveness for the document. This method is being called on instance creation.

stop ()

Stops reactiveness for the document.

================================================ FILE: docs/ddpReducer.html ================================================ SimpleDDP Docs

Class: ddpReducer

ddpReducer

A reducer class for a reactive document.

new ddpReducer (ddpReactiveCollectionInstance, reducer, initialValue)

Name Type Description
ddpReactiveCollectionInstance ddpReactiveCollection

Instance of @see ddpReactiveCollection class.

reducer function

Function for a reduction.

initialValue *

Initial value for a reduction function.

Classes

ddpReducer

Methods

data ()Object

Returns reactive reduce.

Returns:
Type Description
Object
  • {result:reducedValue}

doReduce ()

Forcibly reduces reactive data.

onChange (f)

Runs a function every time a change occurs.

Name Type Description
f function

Function which recieves a reduced value at each change.

start ()

Starts reactiveness for the reduced value of the collection. This method is being called on instance creation.

stop ()

Stops reactiveness.

================================================ FILE: docs/ddpSubscription.html ================================================ SimpleDDP Docs

Class: ddpSubscription

ddpSubscription

DDP subscription class.

new ddpSubscription (pubname, args, ddplink)

Name Type Description
pubname String

Publication name.

args Array

Subscription arguments.

ddplink simpleDDP

simpleDDP instance.

Classes

ddpSubscription

Methods

isOn ()Promise

Returns true if subscription is active otherwise false.

Returns:
Type Description
Promise

isReady ()boolean

Returns true if subsciprtion is ready otherwise false.

Returns:
Type Description
boolean

isStopped ()boolean

Returns true if subscription is stopped otherwise false.

Returns:
Type Description
boolean

nosub ()Promise

Returns a promise which resolves when corresponding nosub message arrives. Rejects when nosub comes with error.

Returns:
Type Description
Promise

onNosub (f)ddpEventListener

Runs everytime when nosub message corresponding to the subscription comes from the server.

Name Type Description
f function

Function, event handler.

Returns:
Type Description
ddpEventListener

onReady (f)ddpEventListener

Runs everytime when ready message corresponding to the subscription comes from the server.

Name Type Description
f function

Function, event handler.

Returns:
Type Description
ddpEventListener

ready ()Promise

Returns a promise which resolves when subscription is ready or rejects when nosub message arrives.

Returns:
Type Description
Promise

remove ()

Completly removes subscription.

restart (args)Promise

Restart the subscription. You can also change subscription arguments. Returns a promise which resolves when subscription is ready.

Name Type Description
args Array optional

Subscription arguments.

Returns:
Type Description
Promise

start (args)Promise

Start the subscription. Runs on class creation. Returns a promise which resolves when subscription is ready.

Name Type Description
args Array

Subscription arguments.

Returns:
Type Description
Promise

stop ()Promise

Stops subscription and return a promise which resolves when subscription is stopped.

Returns:
Type Description
Promise
================================================ FILE: docs/index.html ================================================ SimpleDDP Docs

Index

npm version Build Status Dependency Status devDependency Status

SimpleDDP 🥚

The aim of this library is to simplify the process of working with Meteor.js server over DDP protocol using external JS environments (like Node.js, Cordova, Ionic, ReactNative, etc).

It is battle tested 🏰 in production and ready to use 🔨.

If you like this project ⭐ is always welcome.

Important

SimpleDDP is written in ES6 and uses modern features like promises. Though its precompiled with Babel, your js environment must support ES6 features. So if you are planning to use SimpleDDP be sure that your js environment supports ES6 features or include polyfills yourself (like Babel Polyfill).

Project uses semantic versioning 2.0.0.

DDP (protocol) specification.

CHANGE LOG

Install

npm install simpleddp --save

Documentation

Plugins

Adding custom EJSON types

Example

First of all you need WebSocket implementation for your node app. We will use isomorphic-ws package for this since it works on the client and serverside.

npm install isomorphic-ws ws --save

Import/require simpleDDP.

const simpleDDP = require("simpleddp"); // nodejs
const ws = require("isomorphic-ws");

or

import simpleDDP from 'simpleDDP'; // ES6
import ws from 'isomorphic-ws';

Now you should make a new simpleDDP instance.

let opts = {
    endpoint: "ws://someserver.com/websocket",
    SocketConstructor: ws,
    reconnectInterval: 5000
};
const server = new simpleDDP(opts);

Connection is not going to be established immediately after you create a simpleDDP instance. If you need to check your connection simply use server.connected property which is true if you are connected to the server, otherwise it's false.

You can also add some events for connection status.

server.on('connected', () => {
    // do something
});

server.on('disconnected', () => {
    // for example show alert to user
});

server.on('error', (e) => {
    // global errors from server
});

As an alternative you can use a async/await style (or then(...)).

(async ()=>{
  await server.connect();
  // connection is ready here
})();

The next thing we are going to do is subscribing to some publications.

let userSub = server.subscribe("user_pub");
let otherSub = server.subscribe("other_pub",'param1',2); // you can specify arguments for subscription

(async ()=>{
  await userSub.ready();
  let nextSub = server.subscribe("next_pub"); // subscribing after userSub is ready
  await nextSub.ready();
  //all subs are ready here
})();

You can fetch all things you've subscribed for using server.collection method. Also you can get reactive data sources (plain js objects which will be automatically updated if something changes on the server).

(async ()=>{

  // call some method
  await server.call('somemethod');

  let userSub = server.subscribe("user",userId);
  await userSub.ready();

  // get non-reactive user object
  let user = server.collection('users').filter(user=>user.id==userId).fetch()[0];

  // get reactive user object
  let userReactiveCursor = server.collection('users').filter(user=>user.id==userId).reactive().one();
  let userReactiveObject = userReactiveCursor.data();

  // observing the changes
  server.collection('users').filter(user=>user.id==userId).onChange(({prev,next})=>{
    console.log('previus user data',state.prev);
    console.log('next user data',state.next);
  });

  // observing changes in reactive data source
  userReactiveCursor.onChange((newData)=>{
    console.log('new user state', newData);
  });

  let participantsSub = server.subscribe("participants");

  await participantsSub.ready();

  let reactiveCollection = server.collection('participants').reactive();

  // reactive reduce
  let reducedReactive = reactiveCollection.reduce((acc,val,i,arr)=>{
    if (i<arr.length-1)  {
      return acc + val.age;
    } else {
      return (acc + val.age)/arr.length;
    }
  },0);

  // reactive mean age of all participants
  let meanAge = reducedReactive.data();

  // observing changes in reactive data source
  userReactiveCursor.onChange((newData)=>{
    console.log('new user state', newData);
  });
})();
================================================ FILE: docs/scripts/main.js ================================================ (function(){var e=0;var a;var t=document.getElementById("source-code");if(t){var n=config.linenums;if(n){t=t.getElementsByTagName("ol")[0];a=Array.prototype.slice.apply(t.children);a=a.map(function(a){e++;a.id="line"+e})}else{t=t.getElementsByTagName("code")[0];a=t.innerHTML.split("\n");a=a.map(function(a){e++;return''+a});t.innerHTML=a.join("\n")}}})();$(function(){var e=$(".navigation");var a=e.find(".list");var t=$(".search");$("#search").on("keyup",function(t){var n=this.value.trim();if(n){var s=new RegExp(n,"i");e.addClass("searching").removeClass("not-searching").find("li, .itemMembers").removeClass("match");e.find("li").each(function(e,a){var t=$(a);if(t.data("name")&&s.test(t.data("name"))){t.addClass("match");t.closest(".itemMembers").addClass("match");t.closest(".item").addClass("match")}})}else{e.removeClass("searching").addClass("not-searching").find(".item, .itemMembers").removeClass("match")}a.scrollTop(0)});$("#menuToggle").click(function(){a.toggleClass("show");t.toggleClass("show")});e.addClass("not-searching");var n=$(".page-title").data("filename").replace(/\.[a-z]+$/,"");var s=e.find('.item[data-name*="'+n+'"]:eq(0)');if(s.length){s.remove().prependTo(a).addClass("current")}if(config.disqus){$(window).on("load",function(){var e=config.disqus;var a=document.createElement("script");a.type="text/javascript";a.async=true;a.src="http://"+e+".disqus.com/embed.js";(document.getElementsByTagName("head")[0]||document.getElementsByTagName("body")[0]).appendChild(a);var t=document.createElement("script");t.async=true;t.type="text/javascript";t.src="http://"+e+".disqus.com/count.js";document.getElementsByTagName("BODY")[0].appendChild(t)})}}); ================================================ FILE: docs/scripts/prettify/Apache-License-2.0.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "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. ================================================ FILE: docs/scripts/prettify/lang-css.js ================================================ PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); ================================================ FILE: docs/scripts/prettify/prettify.js ================================================ var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; (function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= [],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, "");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), ["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", /^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), ["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= !k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p SimpleDDP Docs

Class: simpleDDP

simpleDDP

Creates an instance of simpleDDP class. After being constructed, the instance will establish a connection with the DDP server and will try to maintain it open.

new simpleDDP (options, plugins)simpleDDP

Name Type Description
options Object
Name Type Default Description
endpoint string

The location of the websocket server. Its format depends on the type of socket you are using. If you are using https connection you have to use wss:// protocol.

SocketConstructor function

The constructor function that will be used to construct the socket. Meteor (currently the only DDP server available) supports websockets and SockJS sockets. So, practically speaking, this means that on the browser you can use either the browser's native WebSocket constructor or the SockJS constructor provided by the SockJS library. On the server you can use whichever library implements the websocket protocol (e.g. faye-websocket).

autoConnect boolean true optional

Whether to establish the connection to the server upon instantiation. When false, one can manually establish the connection with the connect method.

autoReconnect boolean true optional

Whether to try to reconnect to the server when the socket connection closes, unless the closing was initiated by a call to the disconnect method.

reconnectInterval number 1000 optional

The interval in ms between reconnection attempts.

clearDataOnReconnection boolean true optional

Whether to clear all collections data after a reconnection. This invokes fake removed messages on every document.

maxTimeout number optional

Maximum wait for a response from the server to the method call. Default no maxTimeout.

plugins Array optional

Array of plugins.

Version:
  • 2.2.4
Returns:
Type Description
simpleDDP
  • A new simpleDDP instance.
Example
var opts = {
   endpoint: "ws://someserver.com/websocket",
   SocketConstructor: WebSocket,
   reconnectInterval: 5000
};
var server = new simpleDDP(opts);

Members

collections Object

All collections data recieved from server.

connected Boolean

Whether the client is connected to server.

Methods

apply (method, arguments, atBeginning)Promise

Calls a remote method with arguments passed in array.

Name Type Default Description
method string

Name of the server publication.

arguments Array optional

Array of parameters to pass to the remote method. Pass an empty array or don't pass anything if you do not wish to pass any parameters.

atBeginning boolean false optional

If true puts method call at the beginning of the requests queue.

Returns:
Type Description
Promise
  • Promise object, which resolves when receives a result send by server and rejects when receives an error send by server.
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
});

call (method, args)Promise

Calls a remote method with arguments passed after the first argument. Syntactic sugar for @see apply.

Name Type Description
method string

Name of the server publication.

args any optional repeatable

List of parameters to pass to the remote method. Parameters are passed as function arguments.

Returns:
Type Description
Promise
  • Promise object, which resolves when receives a result send by server and rejects when receives an error send by server.

clearData ()Promise

Removes all documents like if it was removed by the server publication.

Returns:
Type Description
Promise
  • Resolves when data is successfully removed.

collection (name)ddpCollection

Use this for fetching the subscribed data and for reactivity inside the collection.

Name Type Description
name string

Collection name.

Returns:
Type Description
ddpCollection

connect ()Promise

Connects to the ddp server. The method is called automatically by the class constructor if the autoConnect option is set to true (default behavior).

Returns:
Type Description
Promise
  • Promise which resolves when connection is established.

disconnect ()Promise

Disconnects from the ddp server by closing the WebSocket connection. You can listen on the disconnected event to be notified of the disconnection.

Returns:
Type Description
Promise
  • Promise which resolves when connection is closed.

exportData (format)Object | string

Exports the data

Name Type Default Description
format string 'string' optional

Possible values are 'string' (EJSON string) and 'raw' (EJSON).

Returns:
Type Description
Object | string
  • EJSON string or EJSON.

importData (data)Promise

Imports the data like if it was published by the server.

Name Type Description
data Object | string

ESJON string or EJSON.

Returns:
Type Description
Promise
  • Resolves when data is successfully imported.

markAsReady (subs)Promise

Marks every passed @see ddpSubscription object as ready like if it was done by the server publication.

Name Type Description
subs Array

Array of @see ddpSubscription objects.

Returns:
Type Description
Promise
  • Resolves when all passed subscriptions are marked as ready.

on (event, f)ddpEventListener

Starts listening server for basic DDP event running f each time the message arrives.

Name Type Description
event string

Any event name from DDP specification. Default suppoted events: connected, disconnected, added, changed, removed, ready, nosub, error, ping, pong.

f function

Function which receives a message from a DDP server as a first argument each time server is invoking event.

Returns:
Type Description
ddpEventListener
Example
server.on('connected', () => {
    // you can show a success message here
});

server.on('disconnected', () => {
    // you can show a reconnection message here
});

stopChangeListeners ()

Stops all reactivity.

sub (pubname, arguments)ddpSubscription

Tries to subscribe to a specific publication on server.

Name Type Description
pubname string

Name of the publication on server.

arguments Array optional

Array of parameters to pass to the remote method. Pass an empty array or don't pass anything if you do not wish to pass any parameters.

Returns:
Type Description
ddpSubscription
  • Subscription.

subscribe (pubname, args)ddpSubscription

Tries to subscribe to a specific publication on server. Syntactic sugar for @see sub.

Name Type Description
pubname string

Name of the publication on server.

args any optional repeatable

List of parameters to pass to the remote method. Parameters are passed as function arguments.

Returns:
Type Description
ddpSubscription
  • Subscription.
================================================ FILE: docs/styles/main.css ================================================ body,html{font-family:'Libre Franklin',sans-serif;background-color:#ecedf1;color:#333}ol,ul{margin:0;padding:0}li{list-style-type:none}#wrap{position:relative}.list::-webkit-scrollbar{width:8px;background-color:transparent}.list::-webkit-scrollbar-thumb{background-color:#647086;border-radius:4px}.navigation{position:fixed;overflow:hidden;min-width:250px;width:25%;top:0;left:0;bottom:0;background-color:#272d37}.navigation .menu-toggle{display:none}@media screen and (max-width:768px){.navigation{left:0;position:relative;width:100%;overflow:auto}.navigation .list,.navigation .search{display:none}.navigation .list.show,.navigation .search.show{display:block;position:static}.navigation .menu-toggle{display:block;position:absolute;top:10px;right:10px}}.navigation .applicationName{margin:0;padding:20px;font-weight:700;white-space:nowrap;color:#fff}.navigation .applicationName a{color:#fff}.navigation .search{padding:0 20px}.navigation .search input{background-color:#14171d;color:#fff;border-color:#3d495a}.navigation .list{padding:20px;position:absolute;overflow:auto;-webkit-overflow-scrolling:touch;width:100%;top:100px;bottom:0}.navigation li.item{margin-bottom:8px;padding-bottom:8px;border-bottom:1px solid #3d495a;overflow:hidden}.navigation li.item a{color:#647086}.navigation li.item a:hover{color:#fff}.navigation li.item .title{display:block}.navigation li.item .title a{display:block;color:#cfd4db}.navigation li.item .title a:hover{color:#fff}.navigation li.item .title.namespace a{color:#fff}.navigation li.item .title .namespaceTag{display:inline-block;border-radius:3px;background-color:#c2185b;color:#fff;font-size:70%;padding:2px 6px;float:left;margin-right:10px;pointer-events:none}.navigation li.item .subtitle{margin:10px 0;font-weight:700;color:#c2185b;display:block;letter-spacing:.05em}.navigation li.item ul>li{padding-left:10px;font-size:.9em}.navigation li.item .itemMembers li.parent a{color:#a9b3c3}.navigation .item,.navigation .itemMembers,.navigation .itemMembers li{display:none}.navigation.not-searching .item{display:block}.navigation.not-searching .item.current .itemMembers,.navigation.not-searching .item.current .itemMembers li{display:block}.navigation.searching .item.match{display:block}.navigation.searching .item.match .itemMembers li.match,.navigation.searching .item.match .itemMembers.match{display:block}.content-size{max-width:1000px;min-width:300px;margin-left:auto;margin-right:auto}.status-deprecated{text-decoration:line-through;opacity:.4}.status-deprecated a,.status-deprecated:hover{text-decoration:none}.main{left:25%;position:fixed;height:100%;right:0;overflow:auto;-webkit-overflow-scrolling:touch;word-break:break-word}.main .summary-list dt{width:100%;margin-bottom:4px;float:left;font-size:110%}@media screen and (min-width:768px){.main .summary-list dt{width:50%}}@media screen and (min-width:991px){.main .summary-list dt{width:33.33%}}@media screen and (min-width:1200px){.main .summary-list dt{width:25%}}@media screen and (max-width:1000px){.main{left:250px}}@media screen and (max-width:768px){.main{left:0;position:static}}.main img{max-width:100%}.main article{padding:20px}.main header{background:#fff}.main header .class-description{font-size:120%}.main header .header{padding:20px}.main header .header h2{font-weight:700}.main .page-title{display:none}.main .access-signature{font-weight:400;display:inline-block;border-radius:3px;background-color:#79859a;color:#fff;font-size:.7em;padding:2px 6px;margin-left:6px}.main .access-signature.deprecated{background-color:#e91e63;font-weight:700}.main .access-signature.deprecated .deprecated-info{font-weight:400;margin-left:5px}.main .access-signature a{color:#fff}.main h4.name .type-signature{font-weight:400;font-size:.8em;color:#79859a}.main h4.name .type-signature:before{content:' : ';opacity:.6}.main h4.name .return-symbol{margin:0 6px;color:#79859a;font-size:80%}.main h4.name .share-icon{font-size:70%;color:#79859a}.main .subsection-title{color:#e91e63}.main .description{margin-top:10px}.main .description ol,.main .description ul{margin-bottom:15px}.main .description h2{font-weight:700;margin-top:30px;margin-bottom:10px;padding-bottom:10px;border-bottom:1px solid #efefef}.main .description pre{margin:10px 0}.main .tag-source{font-size:85%}.main dt.tag-source{margin-top:5px}.main dt.tag-default{color:#79859a}.main .nameContainer{position:relative}.main .nameContainer .tag-source{position:absolute;top:0;right:0;padding:2px 6px;border-bottom-left-radius:4px;border-bottom-right-radius:4px;background-color:#b3b7c3}.main .nameContainer .tag-source a{color:#fff;font-weight:400}.main .nameContainer h4{font-weight:700;padding:20px 0 0;border-top:1px solid #c8c9cc}.main .nameContainer h4 .signature{font-weight:400;font-size:.8em;padding-left:.4em}.main table{width:100%;margin-bottom:15px;margin-top:5px}.main table th{padding:3px 3px;color:#fff;font-weight:400;background:#79859a}.main table td{vertical-align:top;padding:5px 3px;word-break:normal}.main table tbody tr:nth-child(odd){background-color:#fff}.main table tbody tr:nth-child(even){background-color:#f5f5fb}.main table .type{color:#79859a}.main table .attributes{color:#79859a}.main table .description p{margin:0}.main table .optional{float:left;border-radius:3px;background-color:#b3b7c3;padding:2px 4px;margin-right:5px;color:#fff;font-size:80%}.main .readme p{margin-top:15px}.main .readme h1{font-weight:700}.main .readme h2{font-weight:700;margin-top:30px;margin-bottom:10px;padding-bottom:10px;border-bottom:1px solid #79859a}.main .readme h3{color:#e91e63}.main .readme li{margin-bottom:10px}.main article ol,.main article ul{margin-left:25px}.main article ol>li{list-style-type:decimal;margin-bottom:5px}.main article ul>li{margin-bottom:5px;list-style-type:disc}.footer{margin:0 20px 20px;padding-top:20px;text-align:right;font-size:.9em;color:#79859a;border-top:1px solid #c8c9cc} ================================================ FILE: docs/styles/prettify-jsdoc.css ================================================ /* JSDoc prettify.js theme */ /* plain text */ .pln { color: #000000; font-weight: normal; font-style: normal; } /* string content */ .str { color: #006400; font-weight: normal; font-style: normal; } /* a keyword */ .kwd { color: #000000; font-weight: bold; font-style: normal; } /* a comment */ .com { font-weight: normal; font-style: italic; } /* a type name */ .typ { color: #000000; font-weight: normal; font-style: normal; } /* a literal value */ .lit { color: #006400; font-weight: normal; font-style: normal; } /* punctuation */ .pun { color: #000000; font-weight: bold; font-style: normal; } /* lisp open bracket */ .opn { color: #000000; font-weight: bold; font-style: normal; } /* lisp close bracket */ .clo { color: #000000; font-weight: bold; font-style: normal; } /* a markup tag name */ .tag { color: #006400; font-weight: normal; font-style: normal; } /* a markup attribute name */ .atn { color: #006400; font-weight: normal; font-style: normal; } /* a markup attribute value */ .atv { color: #006400; font-weight: normal; font-style: normal; } /* a declaration */ .dec { color: #000000; font-weight: bold; font-style: normal; } /* a variable name */ .var { color: #000000; font-weight: normal; font-style: normal; } /* a function name */ .fun { color: #000000; font-weight: bold; font-style: normal; } /* Specify class=linenums on a pre to get line numbering */ ol.linenums { margin-top: 0; margin-bottom: 0; } ================================================ FILE: docs/styles/prettify-tomorrow.css ================================================ /* Tomorrow Theme */ /* Original theme - https://github.com/chriskempson/tomorrow-theme */ /* Pretty printing styles. Used with prettify.js. */ /* SPAN elements with the classes below are added by prettyprint. */ /* plain text */ .pln { color: #4d4d4c; } @media screen { /* string content */ .str { color: #718c00; } /* a keyword */ .kwd { color: #8959a8; } /* a comment */ .com { color: #8e908c; } /* a type name */ .typ { color: #4271ae; } /* a literal value */ .lit { color: #f5871f; } /* punctuation */ .pun { color: #4d4d4c; } /* lisp open bracket */ .opn { color: #4d4d4c; } /* lisp close bracket */ .clo { color: #4d4d4c; } /* a markup tag name */ .tag { color: #c82829; } /* a markup attribute name */ .atn { color: #f5871f; } /* a markup attribute value */ .atv { color: #3e999f; } /* a declaration */ .dec { color: #f5871f; } /* a variable name */ .var { color: #c82829; } /* a function name */ .fun { color: #4271ae; } } /* Use higher contrast and text-weight for printable form. */ @media print, projection { .str { color: #060; } .kwd { color: #006; font-weight: bold; } .com { color: #600; font-style: italic; } .typ { color: #404; font-weight: bold; } .lit { color: #044; } .pun, .opn, .clo { color: #440; } .tag { color: #006; font-weight: bold; } .atn { color: #404; } .atv { color: #060; } } /* Style */ /* pre.prettyprint { background: white; font-family: Menlo, Monaco, Consolas, monospace; font-size: 12px; line-height: 1.5; border: 1px solid #ccc; padding: 10px; } */ /* Specify class=linenums on a pre to get line numbering */ ol.linenums { margin-top: 0; margin-bottom: 0; } /* IE indents via margin-left */ li.L0, li.L1, li.L2, li.L3, li.L4, li.L5, li.L6, li.L7, li.L8, li.L9 { /* */ } /* Alternate shading for lines */ li.L1, li.L3, li.L5, li.L7, li.L9 { /* */ } ================================================ FILE: jsdoc_conf.json ================================================ { "plugins": [ "plugins/markdown" ], "recurseDepth": 10, "source": { "include": [ "./src/simpleddp.js", "./src/classes", "./README.md"] }, "sourceType": "module", "tags": { "allowUnknownTags": true, "dictionaries": ["jsdoc","closure"] }, "templates": { "applicationName": "SimpleDDP", "disqus": "", "googleAnalytics": "", "openGraph": { "title": "SimpleDDP Docs", "type": "website", "image": "https://github.com/Gregivy/simpleddp/raw/2.x.x/simpleddp.png", "site_name": "SimpleDDP Docs", "url": "" }, "meta": { "title": "SimpleDDP Docs", "description": "SimpleDDP Docs", "keyword": "ddp, simpleddp, websocket, meteor" }, "linenums": true }, "opts": { "encoding": "utf8", "recurse": true, "private": false, "lenient": true, "destination": "./docs", "template": "./node_modules/@pixi/jsdoc-template" } } ================================================ FILE: notes.md ================================================ # Notes ## Can websocket messages arrive out-of-order? [link](https://stackoverflow.com/questions/11804721/can-websocket-messages-arrive-out-of-order) Short answer: No. Long answer: WebSocket runs over TCP, so on that level @EJP 's answer applies. WebSocket can be "intercepted" by intermediaries (like WS proxies): those are allowed to reorder WebSocket control frames (i.e. WS pings/pongs), but not message frames when no WebSocket extension is in place. If there is a neogiated extension in place that in principle allows reordering, then an intermediary may only do so if it understands the extension and the reordering rules that apply. ## Meteor sessionId Meteor (tested v1.4-1.8) does not use sessionId for storing subscription data. ## Meter subId Meteor (tested v1.4-1.8) does not use store subscription data associated with subscription id. This means that if you subscribe to some publication, close socket connection, make some changes on server in data being published and then reconnect to server and subscribe with the same id that previous subscription had, you won't receive any `changed` or `removed` messages. ================================================ FILE: package.json ================================================ { "name": "simpleddp", "version": "2.3.0", "description": "The aim of this library is to simplify the process of working with meteor server over DDP protocol using external JS environments", "keywords": [ "ddp", "simpleddp", "simple", "ddpjs", "ddp.js", "DDP", "WebSocket", "client", "meteor" ], "main": "lib/simpleddp.js", "scripts": { "test": "npm run build && mocha", "docs": "jsdoc -c jsdoc_conf.json", "build": "rm -rf lib && babel src --out-dir lib --no-comments", "prepare": "npm run build", "prepublish": "npm run build" }, "repository": { "type": "git", "url": "git+https://github.com/Gregivy/simpleddp.git" }, "author": "Plyusch Gregory (aka Gregivy)", "license": "MIT", "bugs": { "url": "https://github.com/Gregivy/simpleddp/issues" }, "homepage": "https://gregivy.github.io/simpleddp/", "dependencies": { "clone-deep": "^4.0.1", "ejson": "^2.2.0" }, "devDependencies": { "@pixi/jsdoc-template": "^2.4.2", "babel-cli": "^6.26.0", "babel-core": "^6.26.3", "babel-plugin-add-module-exports": "^1.0.2", "babel-preset-env": "^1.7.0", "babel-preset-stage-0": "^6.24.1", "chai": "^4.2.0", "jsdoc": "^3.6.3", "mocha": "^5.2.0", "ws": "^6.2.1" } } ================================================ FILE: src/classes/ddpCollection.js ================================================ import { fullCopy } from '../helpers/fullCopy.js'; import { ddpOnChange } from './ddpOnChange.js'; import { ddpReactiveCollection } from './ddpReactiveCollection.js'; /** * DDP collection class. * @constructor * @param {String} name - Collection name. * @param {simpleDDP} server - simpleDDP instance. */ export class ddpCollection { constructor(name,server) { this._name = name; this._server = server; this._filter = false; } /** * Allows to specify specific documents inside the collection for reactive data and fetching. * Important: if you change filter function it won't change for the already created reactive objects. * @public * @param {Function} f - Filter function, recieves as arguments object, index and array. * @return {this} */ filter(f) { this._filter = f; return this; } /** * Imports data inside the collection and emits all relevant events. * Both string and JS object types are supported. * @public * @param {string|Object} data - EJSON string or EJSON or js object. */ importData(data) { let c = typeof data === 'string' ? EJSON.parse(data) : data; if (c[this._name]) { c[this._name].forEach((doc,i,arr)=>{ if (!this._filter || (this._filter && this._filter(doc,i,arr))) { this.ddpConnection.emit('added',{ msg: 'added', id: doc.id, collection: this._name, fields: doc.fields }); } }); } } /** * Exports data from the collection. * @public * @param {string} [format='string'] - If 'string' then returns EJSON string, if 'raw' returns js object. * @return {string|Object} */ exportData(format) { let collectionCopy = {[this._name]:this.fetch()}; if (format === undefined || format == 'string') { return EJSON.stringify(collectionCopy); } else if (format == 'raw') { return collectionCopy; } } /** * Returns collection data based on filter and on passed settings. Supports skip, limit and sort. * Order is 'filter' then 'sort' then 'skip' then 'limit'. * @public * @param {Object} [settings={skip:0,limit:Infinity,sort:null}] - Skip and limit are numbers or Infinity, * sort is a standard js array sort function. * @return {Object} */ fetch(settings) { let skip, limit, sort; if (settings) { skip = settings.skip; limit = settings.limit; sort = settings.sort; } let c = this._server.collections[this._name]; let collectionCopy = c ? fullCopy(c) : []; if (this._filter) collectionCopy = collectionCopy.filter(this._filter); if (sort) collectionCopy.sort(sort); if (typeof skip === 'number') collectionCopy.splice(0,skip); if (typeof limit === 'number' || limit == Infinity) collectionCopy.splice(limit); return collectionCopy; } /** * Returns reactive collection object. * @see ddpReactiveCollection * @public * @param {Object} [settings={skip:0,limit:Infinity,sort:null}] * @return {ddpReactiveCollection} */ reactive(settings) { return new ddpReactiveCollection(this,settings,this._filter); } /** * Returns change observer. * @see ddpOnChange * @public * @param {Function} f * @param {Function} filter * @return {ddpOnChange} */ onChange(f,filter) { let obj = { collection: this._name, f: f }; if (this._filter) obj.filter = this._filter; if (filter) obj.filter = filter; return new ddpOnChange(obj,this._server); } } ================================================ FILE: src/classes/ddpEventListener.js ================================================ /** * DDP event listener class. * @constructor * @param {String} eventname - Event name. * @param {Function} f - Function to run when event is fired. * @param {simpleDDP} ddplink - simpleDDP instance. */ export class ddpEventListener { constructor(eventname, f, ddplink) { this._ddplink = ddplink; this._eventname = eventname; this._f = f; this._started = false; this.start(); } /** * Stops listening for server `event` messages. * You can start any stopped @see ddpEventListener at any time using `ddpEventListener.start()`. * @public */ stop() { if (this._started) { this._ddplink.ddpConnection.removeListener(this._eventname,this._f); this._started = false; } } /** * Usually you won't need this unless you stopped the @see ddpEventListener. * @see ddpEventListener starts on creation. * @public */ start() { if (!this._started) { this._ddplink.ddpConnection.on(this._eventname,this._f); this._started = true; } } } ================================================ FILE: src/classes/ddpOnChange.js ================================================ /** * DDP change listener class. * @constructor * @param {Object} obj - Describes changes of interest. * @param {*} inst - Event handler instance. * @param {simpleDDP} [listenersArray = 'onChangeFuncs'] - Property name of event handler instance, array of listeners. */ export class ddpOnChange { constructor(obj,inst,listenersArray = 'onChangeFuncs') { this._obj = obj; this._inst = inst; this._isStopped = true; this._listenersArray = listenersArray; this.start(); } /** * Stops change listener. * @public */ stop() { let i = this._inst[this._listenersArray].indexOf(this._obj); if (i>-1) { this._isStopped = true; this._inst[this._listenersArray].splice(i,1); } } /** * Start change listener. This method is being called on instance creation. * @public */ start() { if (this._isStopped) { this._inst[this._listenersArray].push(this._obj); this._isStopped = false; } } } ================================================ FILE: src/classes/ddpReactiveCollection.js ================================================ import { ddpReducer } from './ddpReducer.js'; import { ddpReactiveDocument } from './ddpReactiveDocument.js'; import { ddpOnChange } from './ddpOnChange.js'; /** * A reactive collection class. * @constructor * @param {ddpCollection} ddpCollection - Instance of @see ddpCollection class. * @param {Object} [settings={skip:0,limit:Infinity,sort:false}] - Object for declarative reactive collection slicing. * @param {Function} [filter=undefined] - Filter function. */ export class ddpReactiveCollection { constructor(ddpCollectionInstance,settings,filter) { this._skip = settings && typeof settings.skip === 'number' ? settings.skip : 0; this._limit = settings && typeof settings.limit === 'number' ? settings.limit : Infinity; this._sort = settings && typeof settings.sort === 'function' ? settings.sort : false; this._length = {result:0}; this._data = []; this._rawData = []; this._reducers = []; this._tickers = []; this._ones = []; this._first = {}; this._syncFunc = function (skip,limit,sort) { let options = {}; if (typeof skip === 'number') options.skip = skip; if (typeof limit === 'number') options.limit = limit; if (sort) options.sort = sort; return ddpCollectionInstance.fetch.call(ddpCollectionInstance,options); }; this._changeHandler = ddpCollectionInstance.onChange(({prev,next,predicatePassed})=>{ if (prev && next) { if (predicatePassed[0]==0 && predicatePassed[1]==1) { // prev falling, next passing filter, adding new element with sort this._smartUpdate(next); } else if (predicatePassed[0]==1 && predicatePassed[1]==0) { // prev passing, next falling filter, removing old element let i = this._rawData.findIndex((obj)=>{ return obj.id == prev.id; }); this._removeItem(i); } else if (predicatePassed[0]==1 && predicatePassed[1]==1) { // both passing, should delete previous and add new let i = this._rawData.findIndex((obj)=>{ return obj.id == prev.id; }); this._smartUpdate(next,i); } } else if (!prev && next) { // element was added and is passing the filter // adding new element with sort this._smartUpdate(next); } else if (prev && !next) { // element was removed and is passing the filter, so it was in newCollection // removing old element let i = this._rawData.findIndex((obj)=>{ return obj.id == prev.id; }); this._removeItem(i); } this._length.result = this._data.length; this._reducers.forEach((reducer)=>{ reducer.doReduce(); }); if (this._data[0]!==this._first) { this._updateReactiveObjects(); } this._first = this._data[0]; this._tickers.forEach((ticker)=>{ ticker(this.data()); }); },filter?filter:(_)=>true); this.started = false; this.start(); } /** * Removes document from the local collection copies. * @private * @param {number} i - Document index in this._rawData array. */ _removeItem(i) { this._rawData.splice(i,1); if (i >= this._skip && i=this._skip+this._limit) { this._data.push(this._rawData[this._skip+this._limit-1]); } } else if (i=this._skip+this._limit) { this._data.push(this._rawData[this._skip+this._limit-1]); } } } /** * Adds document to local the collection this._rawData according to used sorting if specified. * @private * @param {Object} newEl - Document to be added to the local collection. * @return {boolean} - The first element in the collection was changed */ _smartUpdate(newEl,j) { let placement; if (!this._rawData.length) { placement = this._rawData.push(newEl) - 1; if (placement>=this._skip && placement=this._skip && j=this._skip && i=this._skip && placement=this._skip && j=this._skip && placement-1) { this._reducers.splice(i,1); } } /** * Removes reactive object. * @private * @param {ddpReactiveDocument} o - A ddpReducer object that does not need to be updated on changes. */ _deactivateReactiveObject(o) { let i = this._ones.indexOf(o); if (i>-1) { this._ones.splice(i,1); } } /** * Sends new object state for every associated reactive object. * @public */ _updateReactiveObjects() { this._ones.forEach((ro)=>{ ro._update(this.data()[0]); }); } /** * Updates ddpReactiveCollection settings. * @public * @param {Object} [settings={skip:0,limit:Infinity,sort:false}] - Object for declarative reactive collection slicing. * @return {this} */ settings(settings) { let skip, limit, sort; if (settings) { skip = settings.skip; limit = settings.limit; sort = settings.sort; } this._skip = skip !== undefined ? skip : this._skip; this._limit = limit !== undefined ? limit : this._limit; this._sort = sort !== undefined ? sort : this._sort; this._data.splice(0,this._data.length,...this._syncFunc(this._skip,this._limit,this._sort)); this._updateReactiveObjects(); return this; } /** * Updates the skip parameter only. * @public * @param {number} n - A number of documents to skip. * @return {this} */ skip(n) { return this.settings({skip:n}); } /** * Updates the limit parameter only. * @public * @param {number} n - A number of documents to observe. * @return {this} */ limit(n) { return this.settings({limit:n}); } /** * Stops reactivity. Also stops associated reactive objects. * @public */ stop() { if (this.started) { this._changeHandler.stop(); this.started = false; } } /** * Starts reactivity. This method is being called on instance creation. * Also starts every associated reactive object. * @public */ start() { if (!this.started) { this._rawData.splice(0,this._rawData.length,...this._syncFunc(false,false,this._sort)); this._data.splice(0,this._data.length,...this._syncFunc(this._skip,this._limit,this._sort)); this._updateReactiveObjects(); this._changeHandler.start(); this.started = true; } } /** * Sorts local collection according to specified function. * Specified function form {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/sort}. * @public * @param {Function} f - A function used for sorting. * @return {this} */ sort(f) { this._sort = f; if (this._sort) { this._rawData.splice(0,this._rawData.length,...this._syncFunc(false,false,this._sort)); this._data.splice(0,this._data.length,...this._syncFunc(this._skip,this._limit,this._sort)); this._updateReactiveObjects(); } return this; } /** * Returns reactive local collection with applied sorting, skip and limit. * This returned array is being mutated within this class instance. * @public * @return {Array} - Local collection with applied sorting, skip and limit. */ data() { return this._data; } /** * Runs a function every time a change occurs. * @param {Function} f - Function which recieves new collection at each change. * @public */ onChange(f) { return new ddpOnChange(f,this,'_tickers'); } /** * Maps reactive local collection to another reactive array. * Specified function form {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/map}. * @public * @param {Function} f - Function that produces an element of the new Array. * @return {ddpReducer} - Object that allows to get reactive mapped data @see ddpReducer. */ map(f) { return new ddpReducer(this,function (accumulator,el,i,a) { return accumulator.concat(f(el,i,a)); },[]); } /** * Reduces reactive local collection. * Specified function form {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce}. * @public * @param {Function} f - Function to execute on each element in the array. * @param {*} initialValue - Value to use as the first argument to the first call of the function. * @return {ddpReducer} - Object that allows to get reactive object based on reduced reactive local collection @see ddpReducer. */ reduce(f,initialValue) { return new ddpReducer(this,f,initialValue); } /** * Reactive length of the local collection. * @public * @return {Object} - Object with reactive length of the local collection. {result} */ count() { return this._length; } /** * Returns a reactive object which fields are always the same as the first object in the collection. * @public * @param {Object} [settings={preserve:false}] - Settings for reactive object. Use {preserve:true} if you want to keep object on remove. * @return {ddpReactiveDocument} - Object that allows to get reactive object based on reduced reactive local collection @see ddpReactiveDocument. */ one(settings) { return new ddpReactiveDocument(this,settings); } } ================================================ FILE: src/classes/ddpReactiveDocument.js ================================================ import { ddpOnChange } from './ddpOnChange.js'; /** * A reactive document class. * @constructor * @param {ddpReactiveCollection} ddpReactiveCollectionInstance - Instance of @see ddpReactiveCollection class. * @param {Object} [settings={preserve:false}] - Settings for reactive object. When preserve is true, * reactive object won't change when corresponding object is being deleted. */ export class ddpReactiveDocument{ constructor(ddpReactiveCollectionInstance,settings) { this._ddpReactiveCollectionInstance = ddpReactiveCollectionInstance; this._started = false; this._data = {}; this._tickers = []; this._preserve = false; if (typeof settings === 'object' && settings !== null) this.settings(settings); this.start(); } /** * Updates reactive object from local collection copies. * @private * @param {Object} newState - Document's new state. */ _update(newState) { if (newState) { //clean object Object.keys(this._data).forEach((key) => { delete this._data[key]; }); //assign new state Object.assign(this._data,newState); } else { // no object clean if not preserved if (!this._preserve) { Object.keys(this._data).forEach((key) => { delete this._data[key]; }); } } this._tickers.forEach((ticker)=>{ ticker(this.data()); }); } /** * Starts reactiveness for the document. This method is being called on instance creation. * @public */ start() { if (!this._started) { this._update(this._ddpReactiveCollectionInstance.data()[0]); this._ddpReactiveCollectionInstance._activateReactiveObject(this); this._started = true; } } /** * Stops reactiveness for the document. * @public */ stop() { if (this._started) { this._ddpReactiveCollectionInstance._deactivateReactiveObject(this); this._started = false; } } /** * Returns reactive document. * @public * @return {Object} */ data() { return this._data; } /** * Runs a function every time a change occurs. * @param {Function} f - Function which recieves a new value at each change. * @public */ onChange(f) { return new ddpOnChange(f,this,'_tickers'); } /** * Change reactivity settings. * @param {Object} settings * @param {boolean} settings.preserve - When preserve is true,reactive object won't change when corresponding object is being deleted. * @public */ settings({preserve}) { this._preserve = !!preserve; } } ================================================ FILE: src/classes/ddpReducer.js ================================================ import { ddpOnChange } from './ddpOnChange.js'; /** * A reducer class for a reactive document. * @constructor * @param {ddpReactiveCollection} ddpReactiveCollectionInstance - Instance of @see ddpReactiveCollection class. * @param {Function} reducer - Function for a reduction. * @param {*} initialValue - Initial value for a reduction function. */ export class ddpReducer { constructor(ddpReactiveCollectionInstance,reducer,initialValue) { this._ddpReactiveCollectionInstance = ddpReactiveCollectionInstance; this._reducer = reducer; this._started = false; this._data = {result:null}; this._tickers = []; this._initialValue = initialValue; this.start(); } /** * Forcibly reduces reactive data. * @public */ doReduce() { if (this._started) { this._data.result = this._ddpReactiveCollectionInstance.data().reduce(this._reducer,this._initialValue); this._tickers.forEach((ticker)=>{ ticker(this.data().result); }); } } /** * Starts reactiveness for the reduced value of the collection. * This method is being called on instance creation. * @public */ start() { if (!this._started) { this.doReduce(); this._ddpReactiveCollectionInstance._activateReducer(this); this._started = true; } } /** * Stops reactiveness. * @public */ stop() { if (this._started) { this._ddpReactiveCollectionInstance._deactivateReducer(this); this._started = false; } } /** * Returns reactive reduce. * @public * @return {Object} - {result:reducedValue} */ data() { return this._data; } /** * Runs a function every time a change occurs. * @param {Function} f - Function which recieves a reduced value at each change. * @public */ onChange(f) { return new ddpOnChange(f,this,'_tickers'); } } ================================================ FILE: src/classes/ddpSubscription.js ================================================ /** * DDP subscription class. * @constructor * @param {String} pubname - Publication name. * @param {Array} args - Subscription arguments. * @param {simpleDDP} ddplink - simpleDDP instance. */ export class ddpSubscription { constructor(pubname, args, ddplink) { this._ddplink = ddplink; this.pubname = pubname; this.args = args; this._nosub = false; this._started = false; this._ready = false; this.selfReadyEvent = ddplink.on('ready', (m) => { if (m.subs.includes(this.subscriptionId)) { this._ready = true; this._nosub = false; } }); this.selfNosubEvent = ddplink.on('nosub', (m) => { if (m.id==this.subscriptionId) { this._ready = false; this._nosub = true; this._started = false; } }); this.start(); } /** * Runs everytime when `nosub` message corresponding to the subscription comes from the server. * @public * @param {Function} f - Function, event handler. * @return {ddpEventListener} */ onNosub(f) { if (this.isStopped()) { f(); } else { let onNs = this._ddplink.on('nosub', (m) => { if (m.id==this.subscriptionId) { f(m.error || m); } }); return onNs; } } /** * Runs everytime when `ready` message corresponding to the subscription comes from the server. * @public * @param {Function} f - Function, event handler. * @return {ddpEventListener} */ onReady(f) { // может приходить несколько раз, нужно ли сохранять куда-то? if (this.isReady()) { f(); } else { let onReady = this._ddplink.on('ready', (m) => { if (m.subs.includes(this.subscriptionId)) { f(); } }); return onReady; } } /** * Returns true if subsciprtion is ready otherwise false. * @public * @return {boolean} */ isReady() { return this._ready; } /** * Returns true if subscription is stopped otherwise false. * @public * @return {boolean} */ isStopped() { return this._nosub; } /** * Returns a promise which resolves when subscription is ready or rejects when `nosub` message arrives. * @public * @return {Promise} */ ready() { return new Promise((resolve, reject) => { if (this.isReady()) { resolve(); } else { let onReady = this._ddplink.on('ready', (m) => { if (m.subs.includes(this.subscriptionId)) { onReady.stop(); onNosub.stop(); resolve(); } }); let onNosub = this._ddplink.on('nosub', (m) => { if (m.id == this.subscriptionId) { onNosub.stop(); onReady.stop(); reject(m.error || m); } }); } }); } /** * Returns a promise which resolves when corresponding `nosub` message arrives. * Rejects when `nosub` comes with error. * @public * @return {Promise} */ nosub() { return new Promise((resolve, reject) => { if (this.isStopped()) { resolve(); } else { let onNosub = this._ddplink.on('nosub', (m) => { if (m.id==this.subscriptionId) { this._nosub = true; onNosub.stop(); if (m.error) { reject(m.error); } else { resolve(); } } }); } }); } /** * Returns true if subscription is active otherwise false. * @public * @return {Promise} */ isOn() { return this._started; } /** * Completly removes subscription. * @public */ remove() { // stopping nosub listener this.selfNosubEvent.stop(); // stopping the subscription and ready listener this.stop(); // removing from sub list inside simpleDDP instance let i = this._ddplink.subs.indexOf(this); if (i>-1) { this._ddplink.subs.splice(i,1); } } /** * Stops subscription and return a promise which resolves when subscription is stopped. * @public * @return {Promise} */ stop() { if (this._started) { // stopping ready listener this.selfReadyEvent.stop(); // unsubscribing if (!this._nosub) this._ddplink.ddpConnection.unsub(this.subscriptionId); this._started = false; this._ready = false; } return this.nosub(); } /** * Returns subscription id. * @private * @return {Promise} */ _getId() { return this.subscriptionId; } /** * Start the subscription. Runs on class creation. * Returns a promise which resolves when subscription is ready. * @public * @param {Array} args - Subscription arguments. * @return {Promise} */ start(args) { if (!this._started) { // starting ready listener this.selfReadyEvent.start(); // subscribing if (Array.isArray(args)) this.args = args; this.subscriptionId = this._ddplink.ddpConnection.sub(this.pubname,this.args); this._started = true; } return this.ready(); } /** * Restart the subscription. You can also change subscription arguments. * Returns a promise which resolves when subscription is ready. * @public * @param {Array} [args] - Subscription arguments. * @return {Promise} */ restart(args) { return new Promise((resolve, reject) => { this.stop().then(()=>{ this.start(args).then(()=>{ resolve(); }).catch((e)=>{reject(e)}); }).catch((e)=>{reject(e)}); }); } } ================================================ FILE: src/helpers/fullCopy.js ================================================ import cloneDeep from 'clone-deep'; export const fullCopy = cloneDeep; ================================================ FILE: src/helpers/isequal.js ================================================ export const isEqual = function (value, other) { // Get the value type var type = Object.prototype.toString.call(value); // If the two objects are not the same type, return false if (type !== Object.prototype.toString.call(other)) return false; // If items are not an object or array, return false if (['[object Array]', '[object Object]'].indexOf(type) < 0) return false; // Compare the length of the length of the two items var valueLen = type === '[object Array]' ? value.length : Object.keys(value).length; var otherLen = type === '[object Array]' ? other.length : Object.keys(other).length; if (valueLen !== otherLen) return false; // Compare two items var compare = function (item1, item2) { // Get the object type var itemType = Object.prototype.toString.call(item1); // If an object or array, compare recursively if (['[object Array]', '[object Object]'].indexOf(itemType) >= 0) { if (!isEqual(item1, item2)) return false; } // Otherwise, do a simple comparison else { // If the two items are not the same type, return false if (itemType !== Object.prototype.toString.call(item2)) return false; // Else if it's a function, convert to a string and compare // Otherwise, just compare if (itemType === '[object Function]') { if (item1.toString() !== item2.toString()) return false; } else { if (item1 !== item2) return false; } } }; // Compare properties if (type === '[object Array]') { for (var i = 0; i < valueLen; i++) { if (compare(value[i], other[i]) === false) return false; } } else { for (var key in value) { if (value.hasOwnProperty(key)) { if (compare(value[key], other[key]) === false) return false; } } } // If nothing failed, return true return true; }; ================================================ FILE: src/simpleddp.js ================================================ import DDP from '../core'; import EJSON from "ejson"; import { isEqual } from './helpers/isequal.js'; import { fullCopy } from './helpers/fullCopy.js'; import { ddpEventListener } from './classes/ddpEventListener.js'; import { ddpSubscription } from './classes/ddpSubscription.js'; import { ddpCollection } from './classes/ddpCollection.js'; function uniqueIdFuncGen() { let idCounter = 0; return function () { return idCounter++; } } const simpleDDPcounter = uniqueIdFuncGen(); function connectPlugins(plugins,...places) { if (Array.isArray(plugins)) { plugins.forEach((p)=>{ places.forEach((place)=>{ if (p[place]) { p[place].call(this); } }); }); } } /** * Creates an instance of simpleDDP class. After being constructed, the instance will * establish a connection with the DDP server and will try to maintain it open. * @version 2.2.4 */ class simpleDDP { /** * @param {Object} options * @param {string} options.endpoint - The location of the websocket server. Its format depends on the type of socket you are using. If you are using https connection you have to use wss:// protocol. * @param {Function} options.SocketConstructor - The constructor function that will be used to construct the socket. Meteor (currently the only DDP server available) supports websockets and SockJS sockets. So, practically speaking, this means that on the browser you can use either the browser's native WebSocket constructor or the SockJS constructor provided by the SockJS library. On the server you can use whichever library implements the websocket protocol (e.g. faye-websocket). * @param {boolean} [options.autoConnect=true] - Whether to establish the connection to the server upon instantiation. When false, one can manually establish the connection with the connect method. * @param {boolean} [options.autoReconnect=true] - Whether to try to reconnect to the server when the socket connection closes, unless the closing was initiated by a call to the disconnect method. * @param {number} [options.reconnectInterval=1000] - The interval in ms between reconnection attempts. * @param {boolean} [options.clearDataOnReconnection=true] - Whether to clear all collections data after a reconnection. This invokes fake `removed` messages on every document. * @param {number} [options.maxTimeout=undefined] - Maximum wait for a response from the server to the method call. Default no maxTimeout. * @param {Array} [plugins] - Array of plugins. * @return {simpleDDP} - A new simpleDDP instance. * @example * var opts = { * endpoint: "ws://someserver.com/websocket", * SocketConstructor: WebSocket, * reconnectInterval: 5000 * }; * var server = new simpleDDP(opts); */ constructor(opts,plugins) { this._id = simpleDDPcounter(); this._opGenId = uniqueIdFuncGen(); this._opts = opts; this.ddpConnection = new DDP(opts); this.subs = []; /** All collections data recieved from server. @type Object */ this.collections = {}; this.onChangeFuncs = []; /** Whether the client is connected to server. @type Boolean */ this.connected = false; this.maxTimeout = opts.maxTimeout; this.clearDataOnReconnection = opts.clearDataOnReconnection === undefined ? true : opts.clearDataOnReconnection; this.tryingToConnect = opts.autoConnect === undefined ? true : opts.autoConnect; this.tryingToDisconnect = false; this.willTryToReconnect = opts.autoReconnect === undefined ? true : opts.autoReconnect; let pluginConnector = connectPlugins.bind(this,plugins); // plugin init section pluginConnector('init','beforeConnected'); this.connectedEvent = this.on('connected',(m)=>{ this.connected = true; this.tryingToConnect = false; }); pluginConnector('afterConnected', 'beforeSubsRestart'); this.connectedEventRestartSubs = this.on('connected', (m)=>{ 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',(m)=>{ 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'); } /** * Restarts all subs. * @private */ restartSubs() { this.subs.forEach((sub)=>{ if (sub.isOn()) { sub.restart(); } }); } /** * Use this for fetching the subscribed data and for reactivity inside the collection. * @public * @param {string} name - Collection name. * @return {ddpCollection} */ collection(name) { return new ddpCollection(name,this); } /** * Dispatcher for ddp added messages. * @private * @param {Object} m - DDP message. */ dispatchAdded(m) { //m везде одинаковое, стоит наверное копировать if (this.collections.hasOwnProperty(m.collection)) { let i = this.collections[m.collection].findIndex((obj)=>{ return 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] = []; let newObj = Object.assign({id:m.id},m.fields); let i = this.collections[m.collection].push(newObj); let fields = {}; if (m.fields) { Object.keys(m.fields).map((p)=>{ fields[p] = 1; }); } this.onChangeFuncs.forEach((l)=>{ if (l.collection==m.collection) { let hasFilter = l.hasOwnProperty('filter'); let newObjFullCopy = fullCopy(newObj); if (!hasFilter) { l.f({changed:false,added:newObjFullCopy,removed:false}); } else if (hasFilter && l.filter(newObjFullCopy,i-1,this.collections[m.collection])) { l.f({prev:false,next:newObjFullCopy,fields,fieldsChanged:newObjFullCopy,fieldsRemoved:[]}); } } }); } /** * Dispatcher for ddp changed messages. * @private * @param {Object} m - DDP message. */ dispatchChanged(m) { if (!this.collections.hasOwnProperty(m.collection)) this.collections[m.collection] = []; let i = this.collections[m.collection].findIndex((obj)=>{ return obj.id == m.id; }); if (i>-1) { let prev = fullCopy(this.collections[m.collection][i]); let fields = {}, fieldsChanged = {}, 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]; }); } let 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 let hasFilter = l.hasOwnProperty('filter'); if (!hasFilter) { l.f({changed:{prev,next:fullCopy(next),fields,fieldsChanged,fieldsRemoved},added:false,removed:false}); } else { let fCopyNext = fullCopy(next); let prevFilter = l.filter(prev,i,this.collections[m.collection]); let nextFilter = l.filter(fCopyNext,i,this.collections[m.collection]); if (prevFilter || nextFilter) { l.f({prev,next:fCopyNext,fields,fieldsChanged,fieldsRemoved,predicatePassed:[prevFilter,nextFilter]}); } } } }); } else { this.dispatchAdded(m); } } /** * Dispatcher for ddp removed messages. * @private * @param {Object} m - DDP message. */ dispatchRemoved(m) { if (!this.collections.hasOwnProperty(m.collection)) this.collections[m.collection] = []; let i = this.collections[m.collection].findIndex((obj)=>{ return obj.id == m.id; }); if (i>-1) { let prevProps; let removedObj = this.collections[m.collection].splice(i,1)[0]; this.onChangeFuncs.forEach((l)=>{ if (l.collection==m.collection) { let hasFilter = l.hasOwnProperty('filter'); if (!hasFilter) { // возможно стоит сделать fullCopy, чтобы было как в случае dispatchAdded и dispatchChanged l.f({changed:false,added:false,removed:removedObj}); } else { if (l.filter(removedObj,i,this.collections[m.collection])) { 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; let 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. * @public * @return {Promise} - Promise which resolves when connection is closed. */ disconnect() { this.willTryToReconnect = false; return new Promise((resolve, reject) => { if (!this.tryingToDisconnect) { this.ddpConnection.disconnect(); this.tryingToDisconnect = true; } if (this.connected) { let connectionHandler = this.on('disconnected', () => { connectionHandler.stop(); this.tryingToDisconnect = false; resolve(); }); } else { resolve(); } }); } /** * Calls a remote method with arguments passed in array. * @public * @param {string} method - Name of the server publication. * @param {Array} [arguments] - Array of parameters to pass to the remote method. Pass an empty array or don't pass anything if you do not wish to pass any parameters. * @param {boolean} [atBeginning=false] - If true puts method call at the beginning of the requests queue. * @return {Promise} - Promise object, which resolves when receives a result send by server and rejects when receives an error send by server. * @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?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. * @public * @param {string} method - Name of the server publication. * @param {...any} [args] - List of parameters to pass to the remote method. Parameters are passed as function arguments. * @return {Promise} - Promise object, which resolves when receives a result send by server and rejects when receives an error send by server. */ 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. * @public * @param {string} pubname - Name of the publication on server. * @param {Array} [arguments] - Array of parameters to pass to the remote method. Pass an empty array or don't pass anything if you do not wish to pass any parameters. * @return {ddpSubscription} - Subscription. */ sub(pubname,args) { let hasSuchSub = this.subs.find((sub) => { return sub.pubname == pubname && isEqual(sub.args,Array.isArray(args)?args:[]); }); if (!hasSuchSub) { let i = this.subs.push(new ddpSubscription(pubname,Array.isArray(args)?args:[],this)); return this.subs[i-1]; } else { if (hasSuchSub.isStopped()) hasSuchSub.start(); return hasSuchSub; } } /** * Tries to subscribe to a specific publication on server. * Syntactic sugar for @see sub. * @public * @param {string} pubname - Name of the publication on server. * @param {...any} [args] - List of parameters to pass to the remote method. Parameters are passed as function arguments. * @return {ddpSubscription} - Subscription. */ subscribe(pubname, ...args) { return this.sub(pubname, args); } /** * Starts listening server for basic DDP event running f each time the message arrives. * @public * @param {string} event - Any event name from DDP specification. * Default suppoted events: `connected`, `disconnected`, `added`, `changed`, `removed`, `ready`, `nosub`, `error`, `ping`, `pong`. * @param {Function} f - Function which receives a message from a DDP server as a first argument each time server is invoking event. * @return {ddpEventListener} * @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 new ddpEventListener(event,f,this); } /** * Stops all reactivity. */ stopChangeListeners() { this.onChangeFuncs = []; } /** * Removes all documents like if it was removed by the server publication. * @public * @return {Promise} - Resolves when data is successfully removed. */ 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; let 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: collection }, uniqueId); }); }); } }); } /** * Imports the data like if it was published by the server. * @public * @param {Object|string} data - ESJON string or EJSON. * @return {Promise} - Resolves when data is successfully imported. */ importData(data) { return new Promise((resolve, reject) => { let c = typeof data === 'string' ? EJSON.parse(data) : data; let totalDocuments = 0; Object.keys(c).forEach((collection)=>{ totalDocuments += Array.isArray(c[collection]) ? c[collection].length : 0; }); let counter = 0; let 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)=>{ let docFields = Object.assign({},doc); delete docFields['id']; this.ddpConnection.emit('added',{ msg: 'added', id: doc.id, collection: collection, fields: docFields }, uniqueId); }); }); }); } /** * Exports the data * @public * @param {string} [format='string'] - Possible values are 'string' (EJSON string) and 'raw' (EJSON). * @return {Object|string} - EJSON string or EJSON. */ exportData(format) { if (format === undefined || format == 'string') { return EJSON.stringify(this.collections); } else if (format == 'raw') { return fullCopy(this.collections); } } /** * Marks every passed @see ddpSubscription object as ready like if it was done by the server publication. * @public * @param {Array} subs - Array of @see ddpSubscription objects. * @return {Promise} - Resolves when all passed subscriptions are marked as ready. */ markAsReady(subs) { return new Promise((resolve, reject) => { let 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(); } }); }); } } export default simpleDDP; ================================================ FILE: test/test_call.js ================================================ const assert = require('chai').assert; const simpleDDP = require('../lib/simpleddp'); const ws = require("ws"); const opts = { endpoint: "ws://someserver.com/websocket", SocketConstructor: ws, reconnectInterval: 5000, maxTimeout: 25 }; describe('simpleDDP', function(){ let server = new simpleDDP(opts); describe('#call', function (){ it('should return promise and afterwards then function should run', function (done) { server.call("somemethod").then(function() { done(); }); server.ddpConnection.emit('result',{ msg: 'result', id: '0', result: 'ok' }); }); }); describe('#apply', function (){ it('should return promise and afterwards then function should run', function (done) { server.apply("somemethod").then(function () { done(); }); server.ddpConnection.emit('result', { msg: 'result', id: '1', result: 'ok' }); }); it("a rejection should be fire if the max timeout has been exceeded", function (done) { this.timeout(100); server.apply("somemethod").then(function () { assert.fail(); }).catch(function (error) { assert.isNotNull(error) done(); }); const ddpConnection = server.ddpConnection; setTimeout(function () { ddpConnection.emit("result", { msg: "result", id: "1", result: "ok" }); }, 50); }); }); after(function() { // runs after all tests in this block server.disconnect(); server = null; }); }); ================================================ FILE: test/test_collectionfetch.js ================================================ const assert = require('chai').assert; const simpleDDP = require('../lib/simpleddp'); const ws = require("ws"); const opts = { endpoint: "ws://someserver.com/websocket", SocketConstructor: ws, reconnectInterval: 5000 }; describe('simpleDDP', function(){ let server = new simpleDDP(opts); describe('#collection->fetch', function (){ beforeEach(function() { // runs before each test in this block // turn the default collection to the initial state server.collections['foe'] = [{ id: 'abc', name: 'test', age: '1 month', quality: 'super' },{ id: 'def', name: 'striker', age: '100 years', quality: 'medium' },{ id: 'ghi', name: 'unusual', why: 'because' }]; //remove onChange handlers server.onChangeFuncs = []; }); it('should return filtered collection', function () { let collectionCut = server.collection('foe').filter((e,i,c)=>{ return e.id == 'abc' || e.quality; }).fetch(); assert.deepEqual(collectionCut,[{ id: 'abc', name: 'test', age: '1 month', quality: 'super' },{ id: 'def', name: 'striker', age: '100 years', quality: 'medium' }]); }); it('should return [] because no such collection', function () { let collectionCut = server.collection('abc').fetch(); assert.deepEqual(collectionCut,[]); }); it('should return [] because no such collection', function () { let collectionCut = server.collection('abc').filter((e,i,c)=>{ return e.id == 'abc' || e.quality; }).fetch(); assert.deepEqual(collectionCut,[]); }); }); after(function() { // runs after all tests in this block server.disconnect(); server = null; }); }); ================================================ FILE: test/test_importexport.js ================================================ const assert = require('chai').assert; const simpleDDP = require('../lib/simpleddp'); const ws = require("ws"); const EJSON = require("ejson"); const opts = { endpoint: "ws://someserver.com/websocket", SocketConstructor: ws, reconnectInterval: 5000 }; let onListener = null; describe('simpleDDP', function(){ let server = new simpleDDP(opts); describe('#collection->importData', function (){ beforeEach(function() { // runs before each test in this block // turn the default collection to the initial state server.collections = {}; }); it('should import raw data into storage', function (done) { let data = { foe: [{ id: 'abc', cat: 'a', name: 'test', age: '1 month', quality: 'super' },{ id: 'def', cat: 'a', name: 'striker', age: '100 years', quality: 'medium' }], bar: [{ id: 'ghi', cat: 'b', name: 'victory', why: 'because' },{ id: 'plu', cat: 'a', name: 'unusual', why: 'because' }] }; server.importData(data).then(function() { assert.deepEqual(server.collections,data); done(); }); }); it('should import string data into storage', function (done) { let dataRaw = { foe: [{ id: 'abc', cat: 'a', name: 'test', age: '1 month', quality: 'super' },{ id: 'def', cat: 'a', name: 'striker', age: '100 years', quality: 'medium' }], bar: [{ id: 'ghi', cat: 'b', name: 'victory', why: 'because' },{ id: 'plu', cat: 'a', name: 'unusual', why: 'because' }] }; dataJSON = EJSON.stringify(dataRaw); server.importData(dataJSON).then(function() { assert.deepEqual(server.collections,dataRaw); done(); }); }); }); describe('#collection->exportData', function (){ const data = { foe: [{ id: 'abc', cat: 'a', name: 'test', age: '1 month', quality: 'super' },{ id: 'def', cat: 'a', name: 'striker', age: '100 years', quality: 'medium' }], bar: [{ id: 'ghi', cat: 'b', name: 'victory', why: 'because' },{ id: 'plu', cat: 'a', name: 'unusual', why: 'because' }] }; before(function() { server.collections = data; }); it('should export raw data from the storage', function () { let exported = server.exportData('raw'); assert.deepEqual(exported,data); }); it('should export EJSON data from the storage', function () { let exported = EJSON.parse(server.exportData()); assert.deepEqual(exported,data); }); }); describe('#collection->markAsReady', function (){ let emulSub; after(function() { // runs after every test in this block emulSub.stop(); emulSub = undefined; }); it('should emulate subscription readiness', function (done) { emulSub = server.sub('testsub'); emulSub.ready().then(done); server.markAsReady([emulSub]); }); }); describe('#collection->clearData', function (){ const data = { foe: [{ id: 'abc', cat: 'a', name: 'test', age: '1 month', quality: 'super' },{ id: 'def', cat: 'a', name: 'striker', age: '100 years', quality: 'medium' }], bar: [{ id: 'ghi', cat: 'b', name: 'victory', why: 'because' },{ id: 'plu', cat: 'a', name: 'unusual', why: 'because' }] }; after(function() { //server.collections = {}; }); it('should clear all collections data', function (done) { server.importData(data).then(function () { assert.deepEqual(server.collections,data); server.clearData().then(function() { assert.deepEqual(server.collections,{ foe: [], bar: [] }); done(); }); }); }); }); after(function() { // runs after all tests in this block server.disconnect(); server = null; }); }); ================================================ FILE: test/test_onchange.js ================================================ const assert = require('chai').assert; const simpleDDP = require('../lib/simpleddp'); const ws = require("ws"); const opts = { endpoint: "ws://someserver.com/websocket", SocketConstructor: ws, reconnectInterval: 5000 }; describe('simpleDDP', function(){ let server = new simpleDDP(opts); describe('#onChange', function (){ beforeEach(function() { // runs before each test in this block // turn the default collection to the initial state server.collections['foe'] = [{ id: 'abc', name: 'test', age: '1 month', quality: 'super' },{ id: 'def', name: 'striker', age: '100 years', quality: 'medium' },{ id: 'ghi', name: 'unusual', why: 'because' }]; //remove onChange handlers server.onChangeFuncs = []; }); it('should detect adding doc to the collection', function (done) { server.collection('foe').onChange(function ({added,removed,changed}) { assert.deepEqual(added, {id: 'nby', name:'new boy', age:'1 minute'}); done(); }); server.ddpConnection.emit('added',{ msg: 'added', id: 'nby', fields: {name:'new boy', age:'1 minute'}, collection: 'foe' }); }); it('should detect changing doc in the collection', function (done) { server.collection('foe').onChange(function ({added,removed,changed}) { assert.deepEqual(changed, { prev: { id: 'abc', name: 'test', age: '1 month', quality: 'super' }, next: { id: 'abc', name: 'new boy', quality: 'medium' }, fields: { name: 1, quality: 1, age: 0 }, fieldsChanged: { name: 'new boy', quality: 'medium' }, fieldsRemoved: ['age'] }); done(); }); server.ddpConnection.emit('changed',{ msg: 'changed', id: 'abc', fields: {name:'new boy', quality:'medium'}, cleared: ['age'], collection: 'foe' }); }); it('should detect removing doc from the collection', function (done) { server.collection('foe').onChange(function ({added,removed,changed}) { assert.deepEqual(removed, { id: 'abc', name: 'test', age: '1 month', quality: 'super' }); done(); }); server.ddpConnection.emit('removed',{ msg: 'removed', id: 'abc', collection: 'foe' }); }); it('should detect changing the doc', function (done) { server.collection('foe').filter((e,i,c)=>i==0).onChange(function (st) { assert.deepEqual(st, { prev: { id: 'abc', name: 'test', age: '1 month', quality: 'super' }, next: { id: 'abc', name: 'new boy', quality: 'medium' }, fields: { name: 1, quality: 1, age: 0 }, fieldsChanged: { name: 'new boy', quality: 'medium' }, fieldsRemoved: ['age'], predicatePassed: [true,true] }); done(); }); server.ddpConnection.emit('changed',{ msg: 'changed', id: 'abc', fields: {name:'new boy', quality:'medium'}, cleared: ['age'], collection: 'foe' }); }); it('should detect removing the doc', function (done) { server.collection('foe').filter((e,i,c)=>i==0).onChange(function ({prev,next,fields,fieldsChanged,fieldsRemoved}) { assert.deepEqual(prev, { id: 'abc', name: 'test', age: '1 month', quality: 'super' }); assert.isNotOk(next); done(); }); server.ddpConnection.emit('removed',{ msg: 'removed', id: 'abc', collection: 'foe' }); }); it('should detect changing the doc\'s properties', function (done) { server.collection('foe').filter((e,i,c)=>i==0).onChange(function (st) { if ('name' in st.fields) { assert.strictEqual(st.prev.name, 'test'); assert.strictEqual(st.next.name, 'new boy'); done(); } }); server.ddpConnection.emit('changed',{ msg: 'changed', id: 'abc', fields: {name:'new boy', quality:'medium'}, cleared: ['age'], collection: 'foe' }); }); it('should NOT detect changing other doc\'s properties', function (done) { server.collection('foe').filter((e,i,c)=>i==0).onChange(function ({fields}) { if ('name' in fields) { done(new Error()); } }); server.ddpConnection.emit('changed',{ msg: 'changed', id: 'abc', fields: {quality:'medium'}, cleared: ['age'], collection: 'foe' }); setTimeout(done, 10); }); it('should NOT detect changing doc\'s properties because stopped and then should detect after rerun', function (done) { let trg = true; let handler = server.collection('foe').filter((e,i,c)=>i==0).onChange(function ({prev,next}) { if (trg) { done(new Error()); } else if (prev.quality=='medium' && next.quality=='normal') { done(); } }); handler.stop(); server.ddpConnection.emit('changed',{ msg: 'changed', id: 'abc', fields: {quality:'medium'}, cleared: ['age'], collection: 'foe' }); setTimeout(()=>{ trg = false; handler.start(); server.ddpConnection.emit('changed',{ msg: 'changed', id: 'abc', fields: {quality:'normal'}, cleared: [], collection: 'foe' }); },10); }); }); after(function() { // runs after all tests in this block server.disconnect(); server = null; }); }); ================================================ FILE: test/test_reactive.js ================================================ const assert = require('chai').assert; const simpleDDP = require('../lib/simpleddp'); const ws = require("ws"); const opts = { endpoint: "ws://someserver.com/websocket", SocketConstructor: ws, reconnectInterval: 5000 }; let onListener = null; describe('simpleDDP', function(){ let server = new simpleDDP(opts); describe('#collection->reactive', function (){ beforeEach(function() { // runs before each test in this block // turn the default collection to the initial state server.collections['foe'] = [{ id: 'abc', cat: 'a', name: 'test', age: '1 month', quality: 'super' },{ id: 'def', cat: 'a', name: 'striker', age: '100 years', quality: 'medium' },{ id: 'ghi', cat: 'b', name: 'victory', why: 'because' },{ id: 'plu', cat: 'a', name: 'unusual', why: 'because' }]; //remove onChange handlers server.onChangeFuncs = []; //stop stop listeners if (onListener) onListener.stop(); }); it('should return reactive filtered collection', function () { let collectionReactiveCut = server.collection('foe').filter(e=>e.cat=='a').reactive(); assert.deepEqual(collectionReactiveCut.data(),[{ id: 'abc', cat: 'a', name: 'test', age: '1 month', quality: 'super' },{ id: 'def', cat: 'a', name: 'striker', age: '100 years', quality: 'medium' },{ id: 'plu', cat: 'a', name: 'unusual', why: 'because' }]); }); it('should return sorted reactive filtered collection', function () { let collectionReactiveCut = server.collection('foe').filter(e=>e.cat=='a').reactive(); collectionReactiveCut.sort((a,b)=>{ if (a.name <= b.name) { return -1; } else { return 1; } }); assert.deepEqual(collectionReactiveCut.data(),[{ id: 'def', cat: 'a', name: 'striker', age: '100 years', quality: 'medium' },{ id: 'abc', cat: 'a', name: 'test', age: '1 month', quality: 'super' },{ id: 'plu', cat: 'a', name: 'unusual', why: 'because' }]); }); it('should return sorted (sort via settings) reactive filtered collection', function () { const sortFunction = (a,b)=>{ if (a.name <= b.name) { return -1; } else { return 1; } }; let collectionReactiveCut = server.collection('foe').filter(e=>e.cat=='a').reactive({sort:sortFunction}); assert.deepEqual(collectionReactiveCut.data(),[{ id: 'def', cat: 'a', name: 'striker', age: '100 years', quality: 'medium' },{ id: 'abc', cat: 'a', name: 'test', age: '1 month', quality: 'super' },{ id: 'plu', cat: 'a', name: 'unusual', why: 'because' }]); }); it('should reactively remove element from filtered collection cut when element changes', function (done) { let collectionReactiveCut = server.collection('foe').filter(e=>e.cat=='a').reactive(); let collectionLength1 = collectionReactiveCut.data().length; server.ddpConnection.emit('changed',{ msg: 'changed', id: 'abc', fields: {cat:'b'}, cleared: [], collection: 'foe' }); setTimeout(()=>{ let collectionLength2 = collectionReactiveCut.data().length; assert.equal(collectionLength1,3); assert.equal(collectionLength2,2); done(); },10); }); it('should reactively remove element from filtered collection cut when element is removed', function (done) { let collectionReactiveCut = server.collection('foe').filter(e=>e.cat=='a').reactive(); let collectionLength1 = collectionReactiveCut.data().length; server.ddpConnection.emit('removed',{ msg: 'removed', id: 'abc', collection: 'foe' }); setTimeout(()=>{ let collectionLength2 = collectionReactiveCut.data().length; assert.equal(collectionLength1,3); assert.equal(collectionLength2,2); done(); },10); }); it('should reactively add element to filtered collection cut when element changes', function (done) { let collectionReactiveCut = server.collection('foe').filter(e=>e.cat=='a').reactive(); let collectionLength1 = collectionReactiveCut.data().length; server.ddpConnection.emit('changed',{ msg: 'changed', id: 'ghi', fields: {cat:'a'}, cleared: [], collection: 'foe' }); setTimeout(()=>{ let collectionLength2 = collectionReactiveCut.data().length; assert.equal(collectionLength1,3); assert.equal(collectionLength2,4); done(); },10); }); it('should reactively add element to filtered collection cut when element is added', function (done) { let collectionReactiveCut = server.collection('foe').filter(e=>e.cat=='a').reactive(); let collectionLength1 = collectionReactiveCut.data().length; server.ddpConnection.emit('added',{ msg: 'added', id: 'new', fields: {cat:'a',name:'newElement'}, collection: 'foe' }); setTimeout(()=>{ let collectionLength2 = collectionReactiveCut.data().length; assert.equal(collectionLength1,3); assert.equal(collectionLength2,4); done(); },10); }); it('should reactively re-sort filtered collection cut when element changes', function (done) { let collectionReactiveCut = server.collection('foe').filter(e=>e.cat=='a').reactive(); collectionReactiveCut.sort((a,b)=>{ if (a.name <= b.name) { return -1; } else { return 1; } }); server.ddpConnection.emit('changed',{ msg: 'changed', id: 'abc', fields: {name:'prime'}, cleared: [], collection: 'foe' }); setTimeout(()=>{ assert.deepEqual(collectionReactiveCut.data(),[{ id: 'abc', cat: 'a', name: 'prime', age: '1 month', quality: 'super' },{ id: 'def', cat: 'a', name: 'striker', age: '100 years', quality: 'medium' },{ id: 'plu', cat: 'a', name: 'unusual', why: 'because' }]); done(); },10); }); it('should reactively re-sort filtered collection cut when element added to filtered cut by being changed', function (done) { let collectionReactiveCut = server.collection('foe').filter(e=>e.cat=='a').reactive(); collectionReactiveCut.sort((a,b)=>{ if (a.name <= b.name) { return -1; } else { return 1; } }); server.ddpConnection.emit('changed',{ msg: 'changed', id: 'ghi', fields: {cat:'a'}, cleared: [], collection: 'foe' }); setTimeout(()=>{ assert.deepEqual(collectionReactiveCut.data(),[{ id: 'def', cat: 'a', name: 'striker', age: '100 years', quality: 'medium' },{ id: 'abc', cat: 'a', name: 'test', age: '1 month', quality: 'super' },{ id: 'plu', cat: 'a', name: 'unusual', why: 'because' },{ id: 'ghi', cat: 'a', name: 'victory', why: 'because' }]); done(); },10); }); it('should reactively re-sort filtered collection cut when element added to filtered cut by being added', function (done) { let collectionReactiveCut = server.collection('foe').filter(e=>e.cat=='a').reactive(); collectionReactiveCut.sort((a,b)=>{ if (a.name <= b.name) { return -1; } else { return 1; } }); server.ddpConnection.emit('added',{ msg: 'added', id: 'new', fields: {cat:'a', name:'tast'}, cleared: [], collection: 'foe' }); setTimeout(()=>{ assert.deepEqual(collectionReactiveCut.data(),[{ id: 'def', cat: 'a', name: 'striker', age: '100 years', quality: 'medium' },{ id: 'new', cat: 'a', name: 'tast', },{ id: 'abc', cat: 'a', name: 'test', age: '1 month', quality: 'super' },{ id: 'plu', cat: 'a', name: 'unusual', why: 'because' }]); done(); },10); }); it('should not add to reactive collection an object that does not pass the filter', function (done) { let collectionReactiveCut = server.collection('foe').filter(e=>e.cat=='a').reactive(); server.ddpConnection.emit('added',{ msg: 'added', id: 'new', fields: {cat:'b', name:'tast'}, cleared: [], collection: 'foe' }); setTimeout(()=>{ assert.deepEqual(collectionReactiveCut.data(),[{ id: 'abc', cat: 'a', name: 'test', age: '1 month', quality: 'super' },{ id: 'def', cat: 'a', name: 'striker', age: '100 years', quality: 'medium' },{ id: 'plu', cat: 'a', name: 'unusual', why: 'because' }]); done(); },10); }); it('should add to reactive collection an object', function (done) { let collectionReactiveCut = server.collection('foe').reactive(); server.ddpConnection.emit('added',{ msg: 'added', id: 'new', fields: {cat:'b', name:'tast'}, cleared: [], collection: 'foe' }); setTimeout(()=>{ assert.deepEqual(collectionReactiveCut.data(),[{ id: 'abc', cat: 'a', name: 'test', age: '1 month', quality: 'super' },{ id: 'def', cat: 'a', name: 'striker', age: '100 years', quality: 'medium' },{ id: 'ghi', cat: 'b', name: 'victory', why: 'because' },{ id: 'plu', cat: 'a', name: 'unusual', why: 'because' },{ id: 'new', cat: 'b', name: 'tast' }]); done(); },10); }); it('should add to reactive collection an object and re-sort', function (done) { let collectionReactiveCut = server.collection('foe').reactive(); collectionReactiveCut.sort((a,b)=>{ if (a.name <= b.name) { return -1; } else { return 1; } }); server.ddpConnection.emit('added',{ msg: 'added', id: 'new', fields: {cat:'b', name:'tast'}, cleared: [], collection: 'foe' }); setTimeout(()=>{ assert.deepEqual(collectionReactiveCut.data(),[{ id: 'def', cat: 'a', name: 'striker', age: '100 years', quality: 'medium' },{ id: 'new', cat: 'b', name: 'tast' },{ id: 'abc', cat: 'a', name: 'test', age: '1 month', quality: 'super' },{ id: 'plu', cat: 'a', name: 'unusual', why: 'because' },{ id: 'ghi', cat: 'b', name: 'victory', why: 'because' }]); done(); },10); }); it('should return reactive collection slice based on skip', function (done) { let collectionReactiveCut = server.collection('foe').reactive({skip:2}); server.ddpConnection.emit('added',{ msg: 'added', id: 'new', fields: {cat:'b', name:'tast'}, cleared: [], collection: 'foe' }); onListener = server.on('added',function (m) { assert.deepEqual(collectionReactiveCut.data(),[{ id: 'ghi', cat: 'b', name: 'victory', why: 'because' },{ id: 'plu', cat: 'a', name: 'unusual', why: 'because' },{ id: 'new', cat: 'b', name: 'tast' }]); done(); }); }); it('should return reactive collection slice based on skip (via skip method)', function (done) { let collectionReactiveCut = server.collection('foe').reactive().skip(2); server.ddpConnection.emit('added',{ msg: 'added', id: 'new', fields: {cat:'b', name:'tast'}, cleared: [], collection: 'foe' }); onListener = server.on('added',function (m) { assert.deepEqual(collectionReactiveCut.data(),[{ id: 'ghi', cat: 'b', name: 'victory', why: 'because' },{ id: 'plu', cat: 'a', name: 'unusual', why: 'because' },{ id: 'new', cat: 'b', name: 'tast' }]); done(); }); }); it('should return reactive collection slice based on limit', function (done) { let collectionReactiveCut = server.collection('foe').reactive({limit:3}); server.ddpConnection.emit('added',{ msg: 'added', id: 'new', fields: {cat:'b', name:'tast'}, cleared: [], collection: 'foe' }); onListener = server.on('added',function (m) { assert.deepEqual(collectionReactiveCut.data(),[{ id: 'abc', cat: 'a', name: 'test', age: '1 month', quality: 'super' },{ id: 'def', cat: 'a', name: 'striker', age: '100 years', quality: 'medium' },{ id: 'ghi', cat: 'b', name: 'victory', why: 'because' }]); done(); }); }); it('should return reactive collection slice based on limit (via limit method)', function (done) { let collectionReactiveCut = server.collection('foe').reactive().limit(3); server.ddpConnection.emit('added',{ msg: 'added', id: 'new', fields: {cat:'b', name:'tast'}, cleared: [], collection: 'foe' }); onListener = server.on('added',function (m) { assert.deepEqual(collectionReactiveCut.data(),[{ id: 'abc', cat: 'a', name: 'test', age: '1 month', quality: 'super' },{ id: 'def', cat: 'a', name: 'striker', age: '100 years', quality: 'medium' },{ id: 'ghi', cat: 'b', name: 'victory', why: 'because' }]); done(); }); }); it('should return reactive collection slice based on skip and limit', function (done) { let collectionReactiveCut = server.collection('foe').reactive({skip:2,limit:2}); server.ddpConnection.emit('added',{ msg: 'added', id: 'new', fields: {cat:'b', name:'tast'}, cleared: [], collection: 'foe' }); onListener = server.on('added',function (m) { assert.deepEqual(collectionReactiveCut.data(),[{ id: 'ghi', cat: 'b', name: 'victory', why: 'because' },{ id: 'plu', cat: 'a', name: 'unusual', why: 'because' }]); done(); }); }); it('should return reactive collection slice based on skip and limit declared vis settings method', function (done) { let collectionReactiveCut = server.collection('foe').reactive().settings({skip:2,limit:2}); server.ddpConnection.emit('added',{ msg: 'added', id: 'new', fields: {cat:'b', name:'tast'}, cleared: [], collection: 'foe' }); onListener = server.on('added',function (m) { assert.deepEqual(collectionReactiveCut.data(),[{ id: 'ghi', cat: 'b', name: 'victory', why: 'because' },{ id: 'plu', cat: 'a', name: 'unusual', why: 'because' }]); done(); }); }); it('should reduce reactive collection slice with skip and limit to a string', function (done) { let collectionReactiveCut = server.collection('foe').reactive({skip:2,limit:2}); let allnames = collectionReactiveCut.reduce((acc,obj)=>acc+obj.name,''); server.ddpConnection.emit('added',{ msg: 'added', id: 'new', fields: {cat:'b', name:'tast'}, cleared: [], collection: 'foe' }); onListener = server.on('added',function (m) { assert.equal(allnames.data().result,'victoryunusual'); done(); }); }); it('should map reactive collection slice with skip and limit to an array of names', function (done) { let collectionReactiveCut = server.collection('foe').reactive({skip:2,limit:2}); let allnames = collectionReactiveCut.map((val)=>val.name); let tick_happened = false; allnames.onChange(function (arr) { tick_happened = true; }); server.ddpConnection.emit('added',{ msg: 'added', id: 'new', fields: {cat:'b', name:'tast'}, cleared: [], collection: 'foe' }); onListener1 = server.on('added',function (m) { assert.isTrue(tick_happened); assert.deepEqual(allnames.data().result,['victory','unusual']); onListener1.stop(); server.ddpConnection.emit('changed',{ msg: 'changed', id: 'ghi', fields: {name:'working'}, cleared: [], collection: 'foe' }); }); onListener = server.on('changed',function (m) { assert.deepEqual(allnames.data().result,['working','unusual']); done(); }); }); }); after(function() { // runs after all tests in this block server.disconnect(); server = null; }); }); ================================================ FILE: test/test_reactiveone.js ================================================ const assert = require('chai').assert; const simpleDDP = require('../lib/simpleddp'); const ws = require("ws"); const opts = { endpoint: "ws://someserver.com/websocket", SocketConstructor: ws, reconnectInterval: 5000 }; describe('simpleDDP', function(){ let server = new simpleDDP(opts); describe('#collection->reactive->one', function (){ beforeEach(function() { // runs before each test in this block // turn the default collection to the initial state server.collections['foe'] = [{ id: 'abc', cat: 'a', name: 'test', age: '1 month', quality: 'super' },{ id: 'def', cat: 'a', name: 'striker', age: '100 years', quality: 'medium' },{ id: 'ghi', cat: 'b', name: 'victory', why: 'because' },{ id: 'plu', cat: 'a', name: 'unusual', why: 'because' }]; //remove onChange handlers server.onChangeFuncs = []; }); it('should return reactive object from filtered collection', function () { let collectionReactiveObj = server.collection('foe').filter(e=>e.cat=='a').reactive().one(); assert.deepEqual(collectionReactiveObj.data(),{ id: 'abc', cat: 'a', name: 'test', age: '1 month', quality: 'super' }); }); it('should change reactive object data to another object because new object does not pass the filter', function (done) { let collectionReactiveObj = server.collection('foe').filter(e=>e.cat=='a').reactive().one(); server.ddpConnection.emit('changed',{ msg: 'changed', id: 'abc', fields: {cat:'b'}, cleared: [], collection: 'foe' }); setTimeout(()=>{ assert.deepEqual(collectionReactiveObj.data(),{ id: 'def', cat: 'a', name: 'striker', age: '100 years', quality: 'medium' }); done(); },10); }); it('should update the reactive object', function (done) { let collectionReactiveObj = server.collection('foe').filter(e=>e.cat=='a').reactive().one(); server.ddpConnection.emit('changed',{ msg: 'changed', id: 'abc', fields: {name:'not test'}, cleared: [], collection: 'foe' }); setTimeout(()=>{ assert.deepEqual(collectionReactiveObj.data(),{ id: 'abc', cat: 'a', name: 'not test', age: '1 month', quality: 'super' }); done(); },10); }); }); after(function() { // runs after all tests in this block server.disconnect(); server = null; }); }); ================================================ FILE: test/test_sub.js ================================================ const assert = require('chai').assert; const simpleDDP = require('../lib/simpleddp'); const ws = require("ws"); const opts = { endpoint: "ws://someserver.com/websocket", SocketConstructor: ws, reconnectInterval: 5000, clearDataOnReconnection: true }; describe('simpleDDP', function(){ let server = new simpleDDP(opts); describe('#sub', function (){ it('should subscribe and simpleDDP.collections should update', async function () { let subscriptionId = ""; setTimeout(function(){ server.ddpConnection.emit('added',{ msg: 'added', collection: "test", id: '0', fields: {isOk:true} }); server.ddpConnection.emit('ready',{ msg: 'ready', subs: [subscriptionId] }); },10); let sub = server.sub("testsub"); subscriptionId = sub.subscriptionId; await sub.ready(); console.log('sub', sub.isReady()) assert.deepEqual(server.collections['test'][0],{ id: '0', isOk: true }); }); it('should subscribe and simpleDDP.collections should update, await sub ready should work both times', async function () { let subscriptionId = ""; setTimeout(function(){ server.ddpConnection.emit('added',{ msg: 'changed', collection: "test", id: '0', fields: {isOk:false} }); server.ddpConnection.emit('ready',{ msg: 'ready', subs: [subscriptionId] }); },10); let sub = server.sub("testsub"); subscriptionId = sub.subscriptionId; await sub.ready(); assert.deepEqual(server.collections['test'][0],{ id: '0', isOk: true }); await sub.ready(); assert.deepEqual(server.collections['test'][0],{ id: '0', isOk: true }); }); }); describe('#subscribe', function (){ it('has the same functionanly as sub, but different syntax', async function () { let subscriptionId = ""; setTimeout(function(){ server.ddpConnection.emit('added',{ msg: 'added', collection: "test", id: '0', fields: {isOk:true} }); server.ddpConnection.emit('ready',{ msg: 'ready', subs: [subscriptionId] }); },10); let sub = server.subscribe("testsub"); subscriptionId = sub.subscriptionId; await sub.ready(); assert.deepEqual(server.collections['test'][0],{ id: '0', isOk: true }); }); it('check the behavior of starting the subscription if error comes from server', function (done) { let subscriptionId = ""; setTimeout(function(){ server.ddpConnection.emit('nosub',{id:subscriptionId,error:"test error"}); },10); let sub = server.subscribe("testsub"); subscriptionId = sub.subscriptionId; sub.ready().then(function () { assert.fail(); }).catch(function (error) { assert.isNotNull(error) done(); }); }); }); describe('#restart', function (){ it('check the behavior of restarting the subscription if error comes from server', function (done) { let subscriptionId = ""; setTimeout(function(){ server.ddpConnection.emit('ready',{ msg: 'ready', subs: [subscriptionId] }); },10); let sub = server.subscribe("testsub"); subscriptionId = sub.subscriptionId; sub.ready().then(function () { setTimeout(function(){ server.ddpConnection.emit('nosub',{id:subscriptionId,error:"test error"}); },10); sub.restart().then(function () { assert.fail(); }).catch(function (error) { assert.isNotNull(error) done(); }); }); }); }); describe('#clearData', function (){ it('should clearData when `clearDataOnReconnection=true` and only after all `removed` message resubscribe', function (done) { let checks = []; server.on('added',function () { checks.push('added'); }); server.on('removed',function () { checks.push('removed'); }); server.ddpConnection.emit('added',{id:0,collection:'test',fields:{test:0}}); server.ddpConnection.emit('disconnected'); server.ddpConnection.emit('connected'); setTimeout(function(){ server.ddpConnection.emit('added',{id:0,collection:'test',fields:{test:0}}); setTimeout(function(){ assert.deepEqual(checks, ['added','removed','added']); done(); },0); },10); }); }); after(function() { // runs after all tests in this block server.disconnect(); server = null; }); });