├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── src ├── index.ts ├── mqtt-pubsub.ts ├── pubsub-async-iterator.ts └── test │ ├── benchmark.ts │ ├── integration-tests.ts │ └── tests.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | dist 4 | coverage 5 | typings 6 | npm-debug.log 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | node_modules 3 | typings 4 | tsconfig.json 5 | typings.json 6 | tslint.json 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | - "8" 5 | - "6" 6 | 7 | cache: 8 | directories: 9 | - node_modules 10 | 11 | install: 12 | - npm install 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 David Yahalomi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-mqtt-subscriptions 2 | 3 | [![Greenkeeper badge](https://badges.greenkeeper.io/davidyaha/graphql-mqtt-subscriptions.svg)](https://greenkeeper.io/) 4 | 5 | This package implements the AsyncIterator Interface and PubSubEngine Interface from the [graphql-subscriptions](https://github.com/apollographql/graphql-subscriptions) package. 6 | It allows you to connect your subscriptions manager to an MQTT enabled Pub Sub broker to support 7 | 8 | horizontally scalable subscriptions setup. 9 | This package is an adapted version of my [graphql-redis-subscriptions](https://github.com/davidyaha/graphql-redis-subscriptions) package. 10 | 11 | ## Installation 12 | 13 | ``` 14 | npm install graphql-mqtt-subscriptions 15 | ``` 16 | 17 | ## Using the AsyncIterator Interface 18 | 19 | Define your GraphQL schema with a `Subscription` type. 20 | 21 | ```graphql 22 | schema { 23 | query: Query 24 | mutation: Mutation 25 | subscription: Subscription 26 | } 27 | 28 | type Subscription { 29 | somethingChanged: Result 30 | } 31 | 32 | type Result { 33 | id: String 34 | } 35 | ``` 36 | 37 | Now, create a `MQTTPubSub` instance. 38 | 39 | ```javascript 40 | import { MQTTPubSub } from 'graphql-mqtt-subscriptions'; 41 | const pubsub = new MQTTPubSub(); // connecting to mqtt://localhost by default 42 | ``` 43 | 44 | Now, implement the Subscriptions type resolver, using `pubsub.asyncIterator` to map the event you need. 45 | 46 | ```javascript 47 | const SOMETHING_CHANGED_TOPIC = 'something_changed'; 48 | 49 | export const resolvers = { 50 | Subscription: { 51 | somethingChanged: { 52 | subscribe: () => pubsub.asyncIterator(SOMETHING_CHANGED_TOPIC) 53 | } 54 | } 55 | } 56 | ``` 57 | 58 | > Subscriptions resolvers are not a function, but an object with `subscribe` method, that returns `AsyncIterable`. 59 | 60 | The `AsyncIterator` method will tell the MQTT client to listen for messages from the MQTT broker on the topic provided, and wraps that listener in an `AsyncIterator` object. 61 | 62 | When messages are received from the topic, those messages can be returned back to connected clients. 63 | 64 | `pubsub.publish` can be used to send messages to a given topic. 65 | 66 | ```js 67 | pubsub.publish(SOMETHING_CHANGED_TOPIC, { somethingChanged: { id: "123" }}); 68 | ``` 69 | 70 | ## Dynamically Create a Topic Based on Subscription Args Passed on the Query: 71 | 72 | ```javascript 73 | export const resolvers = { 74 | Subscription: { 75 | somethingChanged: { 76 | subscribe: (_, args) => pubsub.asyncIterator(`${SOMETHING_CHANGED_TOPIC}.${args.relevantId}`), 77 | }, 78 | }, 79 | } 80 | ``` 81 | 82 | ## Using Arguments and Payload to Filter Events 83 | 84 | ```javascript 85 | import { withFilter } from 'graphql-subscriptions'; 86 | 87 | export const resolvers = { 88 | Subscription: { 89 | somethingChanged: { 90 | subscribe: withFilter( 91 | (_, args) => pubsub.asyncIterator(`${SOMETHING_CHANGED_TOPIC}.${args.relevantId}`), 92 | (payload, variables) => payload.somethingChanged.id === variables.relevantId, 93 | ), 94 | }, 95 | }, 96 | } 97 | ``` 98 | 99 | ## Passing your own client object 100 | 101 | The basic usage is great for development and you will be able to connect to any mqtt enabled server running on your system seamlessly. 102 | For production usage, it is recommended you pass your own MQTT client. 103 | 104 | ```javascript 105 | import { connect } from 'mqtt'; 106 | import { MQTTPubSub } from 'graphql-mqtt-subscriptions'; 107 | 108 | const client = connect('mqtt://test.mosquitto.org', { 109 | reconnectPeriod: 1000, 110 | }); 111 | 112 | const pubsub = new MQTTPubSub({ 113 | client 114 | }); 115 | ``` 116 | 117 | You can learn more on the mqtt options object [here](https://github.com/mqttjs/MQTT.js#client). 118 | 119 | ## Changing QoS for publications or subscriptions 120 | 121 | As specified [here](https://github.com/mqttjs/MQTT.js#publish), the MQTT.js publish and subscribe functions takes an 122 | options object. This object can be defined per trigger with `publishOptions` and `subscribeOptions` resolvers. 123 | 124 | ```javascript 125 | const triggerToQoSMap = { 126 | 'comments.added': 1, 127 | 'comments.updated': 2, 128 | }; 129 | 130 | const pubsub = new MQTTPubSub({ 131 | publishOptions: trigger => Promise.resolve({ qos: triggerToQoSMap[trigger] }), 132 | 133 | subscribeOptions: (trigger, channelOptions) => Promise.resolve({ 134 | qos: Math.max(triggerToQoSMap[trigger], channelOptions.maxQoS), 135 | }), 136 | }); 137 | ``` 138 | 139 | ## Get Notified of the Actual QoS Assigned for a Subscription 140 | 141 | MQTT allows the broker to assign different QoS levels than the one requested by the client. 142 | In order to know what QoS was given to your subscription, you can pass in a callback called `onMQTTSubscribe` 143 | 144 | ```javascript 145 | const onMQTTSubscribe = (subId, granted) => { 146 | console.log(`Subscription with id ${subId} was given QoS of ${granted.qos}`); 147 | } 148 | 149 | const pubsub = new MQTTPubSub({onMQTTSubscribe}); 150 | ``` 151 | 152 | ## Change Encoding Used to Encode and Decode Messages 153 | 154 | Supported encodings available [here](https://nodejs.org/api/buffer.html#buffer_buffers_and_character_encodings) 155 | 156 | ```javascript 157 | const pubsub = new MQTTPubSub({ 158 | parseMessageWithEncoding: 'utf16le', 159 | }); 160 | ``` 161 | 162 | 163 | ## Basic Usage with SubscriptionManager (Deprecated) 164 | 165 | ```javascript 166 | import { MQTTPubSub } from 'graphql-mqtt-subscriptions'; 167 | const pubsub = new MQTTPubSub(); // connecting to mqtt://localhost on default 168 | const subscriptionManager = new SubscriptionManager({ 169 | schema, 170 | pubsub, 171 | setupFunctions: {}, 172 | }); 173 | ``` 174 | 175 | ## Using Trigger Transform (Deprecated) 176 | 177 | Similar to the [graphql-redis-subscriptions](https://github.com/davidyaha/graphql-redis-subscriptions) package, this package supports 178 | a trigger transform function. This trigger transform allows you to use the `channelOptions` object provided to the `SubscriptionManager` 179 | instance, and return a trigger string which is more detailed then the regular trigger. 180 | 181 | Here is an example of a generic trigger transform. 182 | 183 | ```javascript 184 | const triggerTransform = (trigger, { path }) => [trigger, ...path].join('.'); 185 | ``` 186 | 187 | > Note that a `path` field to be passed to the `channelOptions` but you can do whatever you want. 188 | 189 | Next, pass the `triggerTransform` to the `MQTTPubSub` constructor. 190 | 191 | ```javascript 192 | const pubsub = new MQTTPubSub({ 193 | triggerTransform, 194 | }); 195 | ``` 196 | 197 | Lastly, a setupFunction is provided for the `commentsAdded` subscription field. 198 | It specifies one trigger called `comments.added` and it is called with the `channelOptions` object that holds `repoName` path fragment. 199 | ```javascript 200 | const subscriptionManager = new SubscriptionManager({ 201 | schema, 202 | setupFunctions: { 203 | commentsAdded: (options, { repoName }) => ({ 204 | 'comments/added': { 205 | channelOptions: { path: [repoName] }, 206 | }, 207 | }), 208 | }, 209 | pubsub, 210 | }); 211 | ``` 212 | > Note that the `triggerTransform` dependency on the `path` field is satisfied here. 213 | 214 | When `subscribe` is called like this: 215 | 216 | ```javascript 217 | const query = ` 218 | subscription X($repoName: String!) { 219 | commentsAdded(repoName: $repoName) 220 | } 221 | `; 222 | const variables = {repoName: 'graphql-mqtt-subscriptions'}; 223 | subscriptionManager.subscribe({ query, operationName: 'X', variables, callback }); 224 | ``` 225 | 226 | The subscription string that MQTT will receive will be `comments.added.graphql-mqtt-subscriptions`. 227 | This subscription string is much more specific and means the the filtering required for this type of subscription is not needed anymore. 228 | This is one step towards lifting the load off of the graphql api server regarding subscriptions. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-mqtt-subscriptions", 3 | "version": "1.2.0", 4 | "description": "A graphql-subscriptions PubSub Engine using mqtt protocol", 5 | "main": "dist/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/davidyaha/graphql-mqtt-subscriptions.git" 9 | }, 10 | "keywords": [ 11 | "graphql", 12 | "mqtt", 13 | "apollo", 14 | "subscriptions" 15 | ], 16 | "author": "David Yahalomi", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/davidyaha/graphql-mqtt-subscriptions/issues" 20 | }, 21 | "homepage": "https://github.com/davidyaha/graphql-mqtt-subscriptions", 22 | "scripts": { 23 | "compile": "tsc --noUnusedParameters --noUnusedLocals", 24 | "pretest": "npm run compile", 25 | "test": "npm run testonly --", 26 | "posttest": "npm run lint", 27 | "lint": "tslint ./src/**/*.ts", 28 | "watch": "tsc -w", 29 | "testonly": "mocha --reporter spec --full-trace ./dist/test/tests.js ", 30 | "integration": "npm run compile && mocha --reporter spec --full-trace ./dist/test/integration-tests.js ", 31 | "benchmark": "npm run compile && mocha --reporter spec --full-trace ./dist/test/benchmark.js ", 32 | "coverage": "node ./node_modules/istanbul/lib/cli.js cover _mocha -- --full-trace ./dist/test/tests.js", 33 | "postcoverage": "remap-istanbul --input coverage/coverage.raw.json --type lcovonly --output coverage/lcov.info", 34 | "prepublish": "npm run test" 35 | }, 36 | "dependencies": { 37 | "graphql-subscriptions": "^0.4.2", 38 | "iterall": "^1.1.1", 39 | "mqtt": "^2.3.0" 40 | }, 41 | "devDependencies": { 42 | "@types/chai": "^3.4.34", 43 | "@types/chai-as-promised": "0.0.30", 44 | "@types/graphql": "^0.9.0", 45 | "@types/mocha": "^2.2.33", 46 | "@types/node": "7.0.18", 47 | "@types/simple-mock": "0.0.27", 48 | "chai": "^3.5.0", 49 | "chai-as-promised": "^6.0.0", 50 | "graphql": "^0.10.1", 51 | "istanbul": "1.0.0-alpha.2", 52 | "mocha": "^3.0.0", 53 | "remap-istanbul": "^0.9.5", 54 | "simple-mock": "^0.7.0", 55 | "tslint": "^5.2.0", 56 | "typescript": "^2.3.4" 57 | }, 58 | "typings": "dist/index.d.ts", 59 | "typescript": { 60 | "definition": "dist/index.d.ts" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export {MQTTPubSub} from './mqtt-pubsub'; 2 | -------------------------------------------------------------------------------- /src/mqtt-pubsub.ts: -------------------------------------------------------------------------------- 1 | import { PubSubEngine } from 'graphql-subscriptions/dist/pubsub-engine'; 2 | import { connect, Client, ISubscriptionGrant, IClientPublishOptions, IClientSubscribeOptions } from 'mqtt'; 3 | import { PubSubAsyncIterator } from './pubsub-async-iterator'; 4 | 5 | export interface PubSubMQTTOptions { 6 | brokerUrl?: string; 7 | client?: Client; 8 | connectionListener?: (err: Error) => void; 9 | publishOptions?: PublishOptionsResolver; 10 | subscribeOptions?: SubscribeOptionsResolver; 11 | onMQTTSubscribe?: (id: number, granted: ISubscriptionGrant[]) => void; 12 | triggerTransform?: TriggerTransform; 13 | parseMessageWithEncoding?: string; 14 | } 15 | 16 | export class MQTTPubSub implements PubSubEngine { 17 | 18 | private triggerTransform: TriggerTransform; 19 | private onMQTTSubscribe: SubscribeHandler; 20 | private subscribeOptionsResolver: SubscribeOptionsResolver; 21 | private publishOptionsResolver: PublishOptionsResolver; 22 | private mqttConnection: Client; 23 | private subscriptionMap: { [subId: number]: [string, Function] }; 24 | private subsRefsMap: { [trigger: string]: Array }; 25 | private currentSubscriptionId: number; 26 | private parseMessageWithEncoding: string; 27 | 28 | private static matches(pattern: string, topic: string) { 29 | const patternSegments = pattern.split('/'); 30 | const topicSegments = topic.split('/'); 31 | const patternLength = patternSegments.length; 32 | 33 | for (let i = 0; i < patternLength; i++) { 34 | const currentPattern = patternSegments[i]; 35 | const currentTopic = topicSegments[i]; 36 | if (!currentTopic && !currentPattern) { 37 | continue; 38 | } 39 | if (!currentTopic && currentPattern !== '#') { 40 | return false; 41 | } 42 | if (currentPattern[0] === '#') { 43 | return i === (patternLength - 1); 44 | } 45 | if (currentPattern[0] !== '+' && currentPattern !== currentTopic) { 46 | return false; 47 | } 48 | } 49 | return patternLength === (topicSegments.length); 50 | } 51 | 52 | constructor(options: PubSubMQTTOptions = {}) { 53 | this.triggerTransform = options.triggerTransform || (trigger => trigger as string); 54 | 55 | if (options.client) { 56 | this.mqttConnection = options.client; 57 | } else { 58 | const brokerUrl = options.brokerUrl || 'mqtt://localhost'; 59 | this.mqttConnection = connect(brokerUrl); 60 | } 61 | 62 | this.mqttConnection.on('message', this.onMessage.bind(this)); 63 | 64 | if (options.connectionListener) { 65 | this.mqttConnection.on('connect', options.connectionListener); 66 | this.mqttConnection.on('error', options.connectionListener); 67 | } else { 68 | this.mqttConnection.on('error', console.error); 69 | } 70 | 71 | this.subscriptionMap = {}; 72 | this.subsRefsMap = {}; 73 | this.currentSubscriptionId = 0; 74 | this.onMQTTSubscribe = options.onMQTTSubscribe || (() => null); 75 | this.publishOptionsResolver = options.publishOptions || (() => Promise.resolve({} as IClientPublishOptions)); 76 | this.subscribeOptionsResolver = options.subscribeOptions || (() => Promise.resolve({} as IClientSubscribeOptions)); 77 | this.parseMessageWithEncoding = options.parseMessageWithEncoding; 78 | } 79 | 80 | public publish(trigger: string, payload: any): boolean { 81 | this.publishOptionsResolver(trigger, payload).then(publishOptions => { 82 | const message = Buffer.from(JSON.stringify(payload), this.parseMessageWithEncoding); 83 | 84 | this.mqttConnection.publish(trigger, message, publishOptions); 85 | }); 86 | return true; 87 | } 88 | 89 | public subscribe(trigger: string, onMessage: Function, options?: Object): Promise { 90 | const triggerName: string = this.triggerTransform(trigger, options); 91 | const id = this.currentSubscriptionId++; 92 | this.subscriptionMap[id] = [triggerName, onMessage]; 93 | 94 | let refs = this.subsRefsMap[triggerName]; 95 | if (refs && refs.length > 0) { 96 | const newRefs = [...refs, id]; 97 | this.subsRefsMap[triggerName] = newRefs; 98 | return Promise.resolve(id); 99 | 100 | } else { 101 | return new Promise((resolve, reject) => { 102 | // 1. Resolve options object 103 | this.subscribeOptionsResolver(trigger, options).then(subscriptionOptions => { 104 | 105 | // 2. Subscribing using MQTT 106 | this.mqttConnection.subscribe(triggerName, { qos: 0, ...subscriptionOptions }, (err, granted) => { 107 | if (err) { 108 | reject(err); 109 | } else { 110 | 111 | // 3. Saving the new sub id 112 | const subscriptionIds = this.subsRefsMap[triggerName] || []; 113 | this.subsRefsMap[triggerName] = [...subscriptionIds, id]; 114 | 115 | // 4. Resolving the subscriptions id to the Subscription Manager 116 | resolve(id); 117 | 118 | // 5. Notify implementor on the subscriptions ack and QoS 119 | this.onMQTTSubscribe(id, granted); 120 | } 121 | }); 122 | }).catch(err => reject(err)); 123 | }); 124 | } 125 | } 126 | 127 | public unsubscribe(subId: number) { 128 | const [triggerName = null] = this.subscriptionMap[subId] || []; 129 | const refs = this.subsRefsMap[triggerName]; 130 | 131 | if (!refs) { 132 | throw new Error(`There is no subscription of id "${subId}"`); 133 | } 134 | 135 | let newRefs; 136 | if (refs.length === 1) { 137 | this.mqttConnection.unsubscribe(triggerName); 138 | newRefs = []; 139 | 140 | } else { 141 | const index = refs.indexOf(subId); 142 | if (index > -1) { 143 | newRefs = [...refs.slice(0, index), ...refs.slice(index + 1)]; 144 | } 145 | } 146 | 147 | this.subsRefsMap[triggerName] = newRefs; 148 | delete this.subscriptionMap[subId]; 149 | } 150 | 151 | public asyncIterator(triggers: string | string[]): AsyncIterator { 152 | return new PubSubAsyncIterator(this, triggers); 153 | } 154 | 155 | private onMessage(topic: string, message: Buffer) { 156 | const subscribers = [].concat( 157 | ...Object.keys(this.subsRefsMap) 158 | .filter((key) => MQTTPubSub.matches(key, topic)) 159 | .map((key) => this.subsRefsMap[key]), 160 | ); 161 | 162 | // Don't work for nothing.. 163 | if (!subscribers || !subscribers.length) { 164 | return; 165 | } 166 | const messageString = message.toString(this.parseMessageWithEncoding); 167 | let parsedMessage; 168 | try { 169 | parsedMessage = JSON.parse(messageString); 170 | } catch (e) { 171 | parsedMessage = messageString; 172 | } 173 | 174 | for (const subId of subscribers) { 175 | const listener = this.subscriptionMap[subId][1]; 176 | listener(parsedMessage); 177 | } 178 | } 179 | } 180 | 181 | export type Path = Array; 182 | export type Trigger = string | Path; 183 | export type TriggerTransform = (trigger: Trigger, channelOptions?: Object) => string; 184 | export type SubscribeOptionsResolver = (trigger: Trigger, channelOptions?: Object) => Promise; 185 | export type PublishOptionsResolver = (trigger: Trigger, payload: any) => Promise; 186 | export type SubscribeHandler = (id: number, granted: ISubscriptionGrant[]) => void; 187 | -------------------------------------------------------------------------------- /src/pubsub-async-iterator.ts: -------------------------------------------------------------------------------- 1 | import { $$asyncIterator } from 'iterall'; 2 | import { PubSubEngine } from 'graphql-subscriptions/dist/pubsub-engine'; 3 | 4 | /** 5 | * A class for digesting PubSubEngine events via the new AsyncIterator interface. 6 | * This implementation is a generic version of the one located at 7 | * https://github.com/apollographql/graphql-subscriptions/blob/master/src/event-emitter-to-async-iterator.ts 8 | * @class 9 | * 10 | * @constructor 11 | * 12 | * @property pullQueue @type {Function[]} 13 | * A queue of resolve functions waiting for an incoming event which has not yet arrived. 14 | * This queue expands as next() calls are made without PubSubEngine events occurring in between. 15 | * 16 | * @property pushQueue @type {any[]} 17 | * A queue of PubSubEngine events waiting for next() calls to be made. 18 | * This queue expands as PubSubEngine events arrice without next() calls occurring in between. 19 | * 20 | * @property eventsArray @type {string[]} 21 | * An array of PubSubEngine event names which this PubSubAsyncIterator should watch. 22 | * 23 | * @property allSubscribed @type {Promise} 24 | * A promise of a list of all subscription ids to the passed PubSubEngine. 25 | * 26 | * @property listening @type {boolean} 27 | * Whether or not the PubSubAsynIterator is in listening mode (responding to incoming PubSubEngine events and next() calls). 28 | * Listening begins as true and turns to false once the return method is called. 29 | * 30 | * @property pubsub @type {PubSubEngine} 31 | * The PubSubEngine whose events will be observed. 32 | */ 33 | export class PubSubAsyncIterator implements AsyncIterator { 34 | 35 | private pullQueue: Function[]; 36 | private pushQueue: any[]; 37 | private eventsArray: string[]; 38 | private allSubscribed: Promise; 39 | private listening: boolean; 40 | private pubsub: PubSubEngine; 41 | 42 | constructor(pubsub: PubSubEngine, eventNames: string | string[]) { 43 | this.pubsub = pubsub; 44 | this.pullQueue = []; 45 | this.pushQueue = []; 46 | this.listening = true; 47 | this.eventsArray = typeof eventNames === 'string' ? [eventNames] : eventNames; 48 | this.allSubscribed = this.subscribeAll(); 49 | } 50 | 51 | public async next() { 52 | await this.allSubscribed; 53 | return this.listening ? this.pullValue() : this.return(); 54 | } 55 | 56 | public async return() { 57 | this.emptyQueue(await this.allSubscribed); 58 | return { value: undefined, done: true }; 59 | } 60 | 61 | public async throw(error) { 62 | this.emptyQueue(await this.allSubscribed); 63 | return Promise.reject(error); 64 | } 65 | 66 | public [$$asyncIterator]() { 67 | return this; 68 | } 69 | 70 | private async pushValue(event) { 71 | await this.allSubscribed; 72 | if (this.pullQueue.length !== 0) { 73 | this.pullQueue.shift()({ value: event, done: false }); 74 | } else { 75 | this.pushQueue.push(event); 76 | } 77 | } 78 | 79 | private pullValue(): Promise> { 80 | return new Promise(resolve => { 81 | if (this.pushQueue.length !== 0) { 82 | resolve({ value: this.pushQueue.shift(), done: false }); 83 | } else { 84 | this.pullQueue.push(resolve); 85 | } 86 | }); 87 | } 88 | 89 | private emptyQueue(subscriptionIds: number[]) { 90 | if (this.listening) { 91 | this.listening = false; 92 | this.unsubscribeAll(subscriptionIds); 93 | this.pullQueue.forEach(resolve => resolve({ value: undefined, done: true })); 94 | this.pullQueue.length = 0; 95 | this.pushQueue.length = 0; 96 | } 97 | } 98 | 99 | private subscribeAll() { 100 | return Promise.all(this.eventsArray.map( 101 | eventName => this.pubsub.subscribe(eventName, this.pushValue.bind(this), {}), 102 | )); 103 | } 104 | 105 | private unsubscribeAll(subscriptionIds: number[]) { 106 | for (const subscriptionId of subscriptionIds) { 107 | this.pubsub.unsubscribe(subscriptionId); 108 | } 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/test/benchmark.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | import { 3 | GraphQLSchema, 4 | GraphQLObjectType, 5 | GraphQLString, 6 | GraphQLFloat, 7 | GraphQLInt, 8 | GraphQLID, 9 | } from 'graphql'; 10 | 11 | import {SubscriptionManager, PubSub} from 'graphql-subscriptions'; 12 | import {MQTTPubSub} from '../mqtt-pubsub'; 13 | 14 | const expect = chai.expect; 15 | 16 | const User = new GraphQLObjectType({ 17 | name: 'User', 18 | fields: { 19 | login: { 20 | type: GraphQLString, 21 | }, 22 | avatar_url: { 23 | type: GraphQLString, 24 | }, 25 | html_url: { 26 | type: GraphQLString, 27 | }, 28 | name: { 29 | type: GraphQLString, 30 | }, 31 | last_visit: { 32 | type: GraphQLFloat, 33 | }, 34 | }, 35 | }); 36 | 37 | const Comment = new GraphQLObjectType({ 38 | name: 'Comment', 39 | fields: { 40 | id: { 41 | type: GraphQLID, 42 | }, 43 | content: { 44 | type: GraphQLString, 45 | }, 46 | repoName: { 47 | type: GraphQLString, 48 | }, 49 | createdAt: { 50 | type: GraphQLFloat, 51 | }, 52 | attachedImage: { 53 | type: GraphQLString, 54 | }, 55 | likes: { 56 | type: GraphQLInt, 57 | }, 58 | postedBy: { 59 | type: User, 60 | }, 61 | }, 62 | }); 63 | 64 | const schema = new GraphQLSchema({ 65 | query: new GraphQLObjectType({ 66 | name: 'Query', 67 | fields: { 68 | testString: { 69 | type: GraphQLString, 70 | resolve: function () { 71 | return 'works'; 72 | }, 73 | }, 74 | }, 75 | }), 76 | subscription: new GraphQLObjectType({ 77 | name: 'Subscription', 78 | fields: { 79 | testSubscription: { 80 | type: GraphQLString, 81 | resolve: function (root) { 82 | return root; 83 | }, 84 | }, 85 | testSubscription2: { 86 | type: GraphQLString, 87 | resolve: function (root) { 88 | return root; 89 | }, 90 | }, 91 | commentAdded: { 92 | type: Comment, 93 | resolve: function (root) { 94 | return root; 95 | }, 96 | }, 97 | }, 98 | }), 99 | types: [Comment, User], 100 | }); 101 | 102 | describe('Benchmark EE PubSub', function () { 103 | const subManager = new SubscriptionManager({ 104 | schema, 105 | setupFunctions: {}, 106 | pubsub: new PubSub(), 107 | }); 108 | 109 | describe('multiple subscribers to channel', function () { 110 | 111 | const numberOfSubscribers = 30000; 112 | const subsPromises = []; 113 | let publishesCounter = 0; 114 | let subIds = []; 115 | 116 | before(`Subscribe to ${numberOfSubscribers}`, function (done) { 117 | this.timeout(10000); 118 | publishesCounter = 0; 119 | const query = 'subscription X{ testSubscription }'; 120 | const callback = () => publishesCounter++; 121 | 122 | for (let i = 0; i < numberOfSubscribers; i++) { 123 | const promise = subManager.subscribe({query, operationName: 'X', callback}); 124 | subsPromises.push(promise); 125 | } 126 | 127 | Promise.all(subsPromises).then(ids => { 128 | subIds = ids; 129 | done(); 130 | }).catch(done); 131 | }); 132 | 133 | after('Unsubscribe', function (done) { 134 | this.timeout(10000); 135 | subIds.forEach((subId, index) => { 136 | expect(subId).to.be.a('number'); 137 | subManager.unsubscribe(subId); 138 | 139 | if (index >= subIds.length - 1) { 140 | done(); 141 | } 142 | }); 143 | }); 144 | 145 | it(`should be able to publish to ${numberOfSubscribers} subscribers under a second`, function (done) { 146 | 147 | this.slow(1000); 148 | 149 | // Publish to all subscribers 150 | subManager.publish('testSubscription', 'small event'); 151 | setTimeout(() => { 152 | try { 153 | expect(publishesCounter).to.equals(numberOfSubscribers); 154 | done(); 155 | } catch (e) { 156 | done(e); 157 | } 158 | }, 10); 159 | 160 | }); 161 | }); 162 | 163 | describe('multiple events to channel', function () { 164 | this.timeout(10000); 165 | const smallEventsPerSec = 30000; 166 | const mediumEventsPerSec = 30000; 167 | const largeEventsPerSec = 30000; 168 | const mutationsPerSec = 30000; 169 | const smallQueriesPerSec = 19700; 170 | const mediumQueryPerSec = 16600; 171 | const fullQueryPerSec = 14600; 172 | 173 | it(`should be able to publish ${smallEventsPerSec} small events under a second`, function (done) { 174 | const query = 'subscription X{ testSubscription2 }'; 175 | const payload = 'small event'; 176 | testEventsPerSecond.call(this, smallEventsPerSec, payload, subManager, query, done); 177 | }); 178 | 179 | const mediumEventSize = 5000; 180 | let mediumMessage = ''; 181 | for (let i = 0; i < mediumEventSize; i++) { 182 | mediumMessage += 'e'; 183 | } 184 | 185 | it(`should be able to publish ${mediumEventsPerSec} medium events under a second`, function (done) { 186 | const query = 'subscription X{ testSubscription2 }'; 187 | testEventsPerSecond.call(this, mediumEventsPerSec, mediumMessage, subManager, query, done); 188 | }); 189 | 190 | const largeEventSize = 50000; 191 | let largeMessage = ''; 192 | for (let i = 0; i < largeEventSize; i++) { 193 | largeMessage += 'e'; 194 | } 195 | 196 | it(`should be able to publish ${largeEventsPerSec} large events under a second`, function (done) { 197 | const query = 'subscription X{ testSubscription2 }'; 198 | testEventsPerSecond.call(this, largeEventsPerSec, largeMessage, subManager, query, done); 199 | }); 200 | 201 | let mutationResult = { 202 | content: 'Very good example', 203 | repoName: 'graphql-redis-subscriptions', 204 | attachedImage: 'https://avatars1.githubusercontent.com/u/2580920?v=3&s=466', 205 | likes: 5, 206 | postedBy: { 207 | login: 'davidyaha', 208 | avatar_url: 'https://avatars1.githubusercontent.com/u/2580920?v=3&s=466', 209 | html_url: 'https://twitter.com/davidyahalomi', 210 | name: 'David Yahalomi', 211 | last_visit: Date.now(), 212 | }, 213 | }; 214 | 215 | it(`should be able to publish ${mutationsPerSec} empty query mutation results under a second`, function (done) { 216 | const query = `subscription X{ 217 | commentAdded { 218 | id 219 | } 220 | }`; 221 | testMutationsPerSecond.call(this, mutationsPerSec, mutationResult, subManager, query, done); 222 | }); 223 | 224 | it(`should be able to publish ${smallQueriesPerSec} small query mutation results under a second`, function (done) { 225 | const query = `subscription X{ 226 | commentAdded { 227 | id 228 | createdAt 229 | postedBy { 230 | login 231 | } 232 | } 233 | }`; 234 | testMutationsPerSecond.call(this, smallQueriesPerSec, mutationResult, subManager, query, done); 235 | }); 236 | 237 | it(`should be able to publish ${mediumQueryPerSec} medium query mutation results under a second`, function (done) { 238 | const query = `subscription X{ 239 | commentAdded { 240 | id 241 | createdAt 242 | content 243 | repoName 244 | postedBy { 245 | login 246 | avatar_url 247 | html_url 248 | } 249 | } 250 | }`; 251 | testMutationsPerSecond.call(this, mediumQueryPerSec, mutationResult, subManager, query, done); 252 | }); 253 | 254 | it(`should be able to publish ${fullQueryPerSec} full query mutation results under a second`, function (done) { 255 | const query = `subscription X{ 256 | commentAdded { 257 | id 258 | createdAt 259 | content 260 | repoName 261 | attachedImage 262 | likes 263 | postedBy { 264 | login 265 | avatar_url 266 | html_url 267 | name 268 | last_visit 269 | } 270 | } 271 | }`; 272 | testMutationsPerSecond.call(this, fullQueryPerSec, mutationResult, subManager, query, done); 273 | }); 274 | }); 275 | }); 276 | 277 | describe('Benchmark Redis PubSub', function () { 278 | const subManager = new SubscriptionManager({ 279 | schema, 280 | setupFunctions: {}, 281 | pubsub: new MQTTPubSub(), 282 | }); 283 | 284 | describe('multiple subscribers to channel', function () { 285 | 286 | const numberOfSubscribers = 30000; 287 | const subsPromises = []; 288 | let publishesCounter = 0; 289 | let subIds = []; 290 | 291 | before(`Subscribe to ${numberOfSubscribers}`, function (done) { 292 | this.timeout(10000); 293 | publishesCounter = 0; 294 | const query = 'subscription X{ testSubscription }'; 295 | const callback = () => publishesCounter++; 296 | 297 | for (let i = 0; i < numberOfSubscribers; i++) { 298 | const promise = subManager.subscribe({query, operationName: 'X', callback}); 299 | subsPromises.push(promise); 300 | } 301 | 302 | Promise.all(subsPromises).then(ids => { 303 | subIds = ids; 304 | done(); 305 | }).catch(done); 306 | }); 307 | 308 | after('Unsubscribe', function (done) { 309 | this.timeout(10000); 310 | subIds.forEach((subId, index) => { 311 | expect(subId).to.be.a('number'); 312 | subManager.unsubscribe(subId); 313 | 314 | if (index >= subIds.length - 1) { 315 | done(); 316 | } 317 | }); 318 | }); 319 | 320 | it(`should be able to publish to ${numberOfSubscribers} subscribers under a second`, function (done) { 321 | 322 | this.slow(1000); 323 | 324 | // Publish to all subscribers 325 | subManager.publish('testSubscription', 'small event'); 326 | setTimeout(() => { 327 | try { 328 | expect(publishesCounter).to.equals(numberOfSubscribers); 329 | done(); 330 | } catch (e) { 331 | done(e); 332 | } 333 | }, 10); 334 | 335 | }); 336 | }); 337 | 338 | describe('multiple events to channel', function () { 339 | this.timeout(10000); 340 | const smallEventsPerSec = 14000; 341 | const mediumEventsPerSec = 5000; 342 | const largeEventsPerSec = 340; 343 | const mutationsPerSec = 10500; 344 | const smallQueriesPerSec = 9500; 345 | const mediumQueryPerSec = 8700; 346 | const fullQueryPerSec = 7700; 347 | 348 | it(`should be able to publish ${smallEventsPerSec} small events under a second`, function (done) { 349 | const query = 'subscription X{ testSubscription2 }'; 350 | const payload = 'small event'; 351 | testEventsPerSecond.call(this, smallEventsPerSec, payload, subManager, query, done); 352 | }); 353 | 354 | const mediumEventSize = 5000; 355 | let mediumMessage = ''; 356 | for (let i = 0; i < mediumEventSize; i++) { 357 | mediumMessage += 'e'; 358 | } 359 | 360 | it(`should be able to publish ${mediumEventsPerSec} medium events under a second`, function (done) { 361 | const query = 'subscription X{ testSubscription2 }'; 362 | testEventsPerSecond.call(this, mediumEventsPerSec, mediumMessage, subManager, query, done); 363 | }); 364 | 365 | const largeEventSize = 50000; 366 | let largeMessage = ''; 367 | for (let i = 0; i < largeEventSize; i++) { 368 | largeMessage += 'e'; 369 | } 370 | 371 | it(`should be able to publish ${largeEventsPerSec} large events under a second`, function (done) { 372 | const query = 'subscription X{ testSubscription2 }'; 373 | testEventsPerSecond.call(this, largeEventsPerSec, largeMessage, subManager, query, done); 374 | }); 375 | 376 | let mutationResult = { 377 | content: 'Very good example', 378 | repoName: 'graphql-redis-subscriptions', 379 | attachedImage: 'https://avatars1.githubusercontent.com/u/2580920?v=3&s=466', 380 | likes: 5, 381 | postedBy: { 382 | login: 'davidyaha', 383 | avatar_url: 'https://avatars1.githubusercontent.com/u/2580920?v=3&s=466', 384 | html_url: 'https://twitter.com/davidyahalomi', 385 | name: 'David Yahalomi', 386 | last_visit: Date.now(), 387 | }, 388 | }; 389 | 390 | it(`should be able to publish ${mutationsPerSec} empty query mutation results under a second`, function (done) { 391 | const query = `subscription X{ 392 | commentAdded { 393 | id 394 | } 395 | }`; 396 | testMutationsPerSecond.call(this, mutationsPerSec, mutationResult, subManager, query, done); 397 | }); 398 | 399 | it(`should be able to publish ${smallQueriesPerSec} small query mutation results under a second`, function (done) { 400 | const query = `subscription X{ 401 | commentAdded { 402 | id 403 | createdAt 404 | postedBy { 405 | login 406 | } 407 | } 408 | }`; 409 | testMutationsPerSecond.call(this, smallQueriesPerSec, mutationResult, subManager, query, done); 410 | }); 411 | 412 | it(`should be able to publish ${mediumQueryPerSec} medium query mutation results under a second`, function (done) { 413 | const query = `subscription X{ 414 | commentAdded { 415 | id 416 | createdAt 417 | content 418 | repoName 419 | postedBy { 420 | login 421 | avatar_url 422 | html_url 423 | } 424 | } 425 | }`; 426 | testMutationsPerSecond.call(this, mediumQueryPerSec, mutationResult, subManager, query, done); 427 | }); 428 | 429 | it(`should be able to publish ${fullQueryPerSec} full query mutation results under a second`, function (done) { 430 | const query = `subscription X{ 431 | commentAdded { 432 | id 433 | createdAt 434 | content 435 | repoName 436 | attachedImage 437 | likes 438 | postedBy { 439 | login 440 | avatar_url 441 | html_url 442 | name 443 | last_visit 444 | } 445 | } 446 | }`; 447 | testMutationsPerSecond.call(this, fullQueryPerSec, mutationResult, subManager, query, done); 448 | }); 449 | }); 450 | }); 451 | 452 | function testEventsPerSecond(eventsPerSec: number, eventPayload: any, subManager: SubscriptionManager, query: string, done) { 453 | this.slow(1500); 454 | let start; 455 | 456 | let publishesCounter = 0; 457 | let subId; 458 | 459 | const callback = () => { 460 | if (++publishesCounter === eventsPerSec) { 461 | try { 462 | expect(Date.now() - start).to.below(1000); 463 | subManager.unsubscribe(subId); 464 | done(); 465 | } catch (e) { 466 | done(e); 467 | } 468 | } 469 | }; 470 | 471 | subManager.subscribe({query, operationName: 'X', callback}).then(id => { 472 | subId = id; 473 | start = Date.now(); 474 | for (let i = 0; i < eventsPerSec; i++) { 475 | subManager.publish('testSubscription2', eventPayload); 476 | } 477 | }).catch(done); 478 | } 479 | 480 | function testMutationsPerSecond(mutationsPerSec: number, mutationPayload: any, subManager: SubscriptionManager, query: string, done) { 481 | this.slow(1500); 482 | let start; 483 | 484 | let publishesCounter = 0; 485 | let subId; 486 | const callback = (err, event) => { 487 | if (err) { 488 | done(err); 489 | } 490 | 491 | if (++publishesCounter === mutationsPerSec) { 492 | try { 493 | expect(Date.now() - start).to.below(1000); 494 | 495 | const commentId = event.data.commentAdded.id; 496 | expect(commentId).to.equals(String(mutationsPerSec)); 497 | 498 | subManager.unsubscribe(subId); 499 | 500 | done(); 501 | } catch (e) { 502 | done(e); 503 | } 504 | } 505 | }; 506 | 507 | subManager.subscribe({query, operationName: 'X', callback}).then(id => { 508 | subId = id; 509 | start = Date.now(); 510 | for (let i = 0; i < mutationsPerSec; i++) { 511 | mutationPayload['id'] = i + 1; 512 | mutationPayload['createdAt'] = Date.now(); 513 | 514 | subManager.publish('commentAdded', mutationPayload); 515 | } 516 | }).catch(done); 517 | } 518 | -------------------------------------------------------------------------------- /src/test/integration-tests.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | import * as chaiAsPromised from 'chai-as-promised'; 3 | 4 | import { 5 | GraphQLSchema, 6 | GraphQLObjectType, 7 | GraphQLString, 8 | GraphQLInt, 9 | GraphQLBoolean, 10 | } from 'graphql'; 11 | 12 | import {SubscriptionManager} from 'graphql-subscriptions'; 13 | import {connect} from 'mqtt'; 14 | import {MQTTPubSub} from '../mqtt-pubsub'; 15 | 16 | chai.use(chaiAsPromised); 17 | const expect = chai.expect; 18 | const assert = chai.assert; 19 | 20 | const schema = new GraphQLSchema({ 21 | query: new GraphQLObjectType({ 22 | name: 'Query', 23 | fields: { 24 | testString: { 25 | type: GraphQLString, 26 | resolve: function () { 27 | return 'works'; 28 | }, 29 | }, 30 | }, 31 | }), 32 | subscription: new GraphQLObjectType({ 33 | name: 'Subscription', 34 | fields: { 35 | testSubscription: { 36 | type: GraphQLString, 37 | resolve: function (root) { 38 | return root; 39 | }, 40 | }, 41 | testFilter: { 42 | type: GraphQLString, 43 | resolve: function (_, {filterBoolean}) { 44 | return filterBoolean ? 'goodFilter' : 'badFilter'; 45 | }, 46 | args: { 47 | filterBoolean: {type: GraphQLBoolean}, 48 | }, 49 | }, 50 | testFilterMulti: { 51 | type: GraphQLString, 52 | resolve: function (_, {filterBoolean}) { 53 | return filterBoolean ? 'goodFilter' : 'badFilter'; 54 | }, 55 | args: { 56 | filterBoolean: {type: GraphQLBoolean}, 57 | a: {type: GraphQLString}, 58 | b: {type: GraphQLInt}, 59 | }, 60 | }, 61 | testChannelOptions: { 62 | type: GraphQLString, 63 | resolve: function (root) { 64 | return root; 65 | }, 66 | args: { 67 | repoName: {type: GraphQLString}, 68 | }, 69 | }, 70 | }, 71 | }), 72 | }); 73 | 74 | const mqttClient = connect('mqtt://localhost'); 75 | 76 | const subManager = new SubscriptionManager({ 77 | schema, 78 | setupFunctions: { 79 | 'testFilter': (_, {filterBoolean}) => { 80 | return { 81 | 'Filter1': {filter: (root) => root.filterBoolean === filterBoolean}, 82 | }; 83 | }, 84 | 'testFilterMulti': () => { 85 | return { 86 | 'Trigger1': {filter: () => true}, 87 | 'Trigger2': {filter: () => true}, 88 | }; 89 | }, 90 | }, 91 | pubsub: new MQTTPubSub({ 92 | client: mqttClient, 93 | }), 94 | }); 95 | 96 | describe('SubscriptionManager', function () { 97 | before('wait for connection', function (done) { 98 | mqttClient.on('connect', () => { 99 | done(); 100 | }); 101 | }); 102 | 103 | it('throws an error if query is not valid', function () { 104 | const query = 'query a{ testInt }'; 105 | const callback = () => null; 106 | return expect(subManager.subscribe({query, operationName: 'a', callback})) 107 | .to.eventually.be.rejectedWith('Subscription query has validation errors'); 108 | }); 109 | 110 | it('rejects subscriptions with more than one root field', function () { 111 | const query = 'subscription X{ a: testSubscription, b: testSubscription }'; 112 | const callback = () => null; 113 | return expect(subManager.subscribe({query, operationName: 'X', callback})) 114 | .to.eventually.be.rejectedWith('Subscription query has validation errors'); 115 | }); 116 | 117 | it('can subscribe with a valid query and gets a subId back', function () { 118 | const query = 'subscription X{ testSubscription }'; 119 | const callback = () => null; 120 | subManager.subscribe({query, operationName: 'X', callback}).then(subId => { 121 | expect(subId).to.be.a('number'); 122 | subManager.unsubscribe(subId); 123 | }); 124 | }); 125 | 126 | it.only('can subscribe with a valid query and get the root value', function (done) { 127 | const query = 'subscription X{ testSubscription }'; 128 | const callback = function (err, payload) { 129 | if (err) { 130 | done(err); 131 | } 132 | 133 | try { 134 | expect(payload.data.testSubscription).to.equals('good'); 135 | } catch (e) { 136 | done(e); 137 | return; 138 | } 139 | done(); 140 | }; 141 | 142 | subManager.subscribe({query, operationName: 'X', callback}).then(subId => { 143 | subManager.publish('testSubscription', 'good'); 144 | setTimeout(() => { 145 | subManager.unsubscribe(subId); 146 | }, 10000); 147 | }); 148 | }); 149 | 150 | it('can use filter functions properly', function (done) { 151 | const query = `subscription Filter1($filterBoolean: Boolean){ 152 | testFilter(filterBoolean: $filterBoolean) 153 | }`; 154 | const callback = function (err, payload) { 155 | if (err) { 156 | done(err); 157 | } 158 | 159 | try { 160 | expect(payload.data.testFilter).to.equals('goodFilter'); 161 | } catch (e) { 162 | done(e); 163 | return; 164 | } 165 | done(); 166 | }; 167 | subManager.subscribe({ 168 | query, 169 | operationName: 'Filter1', 170 | variables: {filterBoolean: true}, 171 | callback, 172 | }).then(subId => { 173 | subManager.publish('Filter1', {filterBoolean: false}); 174 | subManager.publish('Filter1', {filterBoolean: true}); 175 | setTimeout(() => { 176 | subManager.unsubscribe(subId); 177 | }, 20); 178 | }); 179 | }); 180 | 181 | it('can subscribe to more than one trigger', function (done) { 182 | // I also used this for testing arg parsing (with console.log) 183 | // args a and b can safely be removed. 184 | // TODO: write real tests for argument parsing 185 | let triggerCount = 0; 186 | const query = `subscription multiTrigger($filterBoolean: Boolean, $uga: String){ 187 | testFilterMulti(filterBoolean: $filterBoolean, a: $uga, b: 66) 188 | }`; 189 | const callback = function (err, payload) { 190 | if (err) { 191 | done(err); 192 | } 193 | 194 | try { 195 | expect(payload.data.testFilterMulti).to.equals('goodFilter'); 196 | triggerCount++; 197 | } catch (e) { 198 | done(e); 199 | return; 200 | } 201 | if (triggerCount === 2) { 202 | done(); 203 | } 204 | }; 205 | subManager.subscribe({ 206 | query, 207 | operationName: 'multiTrigger', 208 | variables: {filterBoolean: true, uga: 'UGA'}, 209 | callback, 210 | }).then(subId => { 211 | subManager.publish('NotATrigger', {filterBoolean: false}); 212 | subManager.publish('Trigger1', {filterBoolean: true}); 213 | subManager.publish('Trigger2', {filterBoolean: true}); 214 | setTimeout(() => { 215 | subManager.unsubscribe(subId); 216 | }, 30); 217 | }); 218 | }); 219 | 220 | it('can unsubscribe', function (done) { 221 | const query = 'subscription X{ testSubscription }'; 222 | const callback = (err) => { 223 | if (err) { 224 | done(err); 225 | } 226 | 227 | try { 228 | assert(false); 229 | } catch (e) { 230 | done(e); 231 | return; 232 | } 233 | done(); 234 | }; 235 | subManager.subscribe({query, operationName: 'X', callback}).then(subId => { 236 | subManager.unsubscribe(subId); 237 | subManager.publish('testSubscription', 'bad'); 238 | setTimeout(done, 30); 239 | }); 240 | }); 241 | 242 | it('throws an error when trying to unsubscribe from unknown id', function () { 243 | expect(() => subManager.unsubscribe(123)) 244 | .to.throw('undefined'); 245 | }); 246 | 247 | it('calls the error callback if there is an execution error', function (done) { 248 | const query = `subscription X($uga: Boolean!){ 249 | testSubscription @skip(if: $uga) 250 | }`; 251 | const callback = function (err, payload) { 252 | try { 253 | // tslint:disable-next-line no-unused-expression 254 | expect(payload).to.be.undefined; 255 | expect(err.message).to.equals( 256 | 'Variable "$uga" of required type "Boolean!" was not provided.', 257 | ); 258 | } catch (e) { 259 | done(e); 260 | return; 261 | } 262 | done(); 263 | }; 264 | 265 | subManager.subscribe({query, operationName: 'X', callback}).then(subId => { 266 | subManager.publish('testSubscription', 'good'); 267 | setTimeout(() => { 268 | subManager.unsubscribe(subId); 269 | }, 40); 270 | }); 271 | }); 272 | 273 | it('can use transform function to convert the trigger name given into more explicit channel name', function (done) { 274 | const triggerTransform = (trigger, {path}) => [trigger, ...path].join('.'); 275 | const pubsub = new MQTTPubSub({ 276 | triggerTransform, 277 | }); 278 | 279 | const subManager2 = new SubscriptionManager({ 280 | schema, 281 | setupFunctions: { 282 | testChannelOptions: (_, {repoName}) => ({ 283 | comments: { 284 | channelOptions: {path: [repoName]}, 285 | }, 286 | }), 287 | }, 288 | pubsub, 289 | }); 290 | 291 | const callback = (err, payload) => { 292 | if (err) { 293 | done(err); 294 | } 295 | 296 | try { 297 | expect(payload.data.testChannelOptions).to.equals('test'); 298 | done(); 299 | } catch (e) { 300 | done(e); 301 | } 302 | }; 303 | 304 | const query = ` 305 | subscription X($repoName: String!) { 306 | testChannelOptions(repoName: $repoName) 307 | } 308 | `; 309 | 310 | const variables = {repoName: 'graphql-redis-subscriptions'}; 311 | 312 | subManager2.subscribe({query, operationName: 'X', variables, callback}).then(subId => { 313 | pubsub.publish('comments.graphql-redis-subscriptions', 'test'); 314 | 315 | setTimeout(() => pubsub.unsubscribe(subId), 4); 316 | }); 317 | 318 | }); 319 | }); 320 | -------------------------------------------------------------------------------- /src/test/tests.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | import * as chaiAsPromised from 'chai-as-promised'; 3 | import { spy, restore } from 'simple-mock'; 4 | import { isAsyncIterable } from 'iterall'; 5 | import * as mqtt from 'mqtt'; 6 | import { QoS } from 'mqtt'; 7 | import { MQTTPubSub } from '../mqtt-pubsub'; 8 | 9 | chai.use(chaiAsPromised); 10 | const expect = chai.expect; 11 | 12 | // -------------- Mocking mqtt Client ------------------ 13 | 14 | let listener; 15 | 16 | const publishSpy = spy((channel, message) => listener && listener(channel, message)); 17 | const subscribeSpy = spy((topic, options, cb) => cb && cb(null, [{ ...options, topic }])); 18 | const unsubscribeSpy = spy((channel, _, cb) => cb && cb(channel)); 19 | 20 | const mqttPackage = mqtt as Object; 21 | 22 | const connect = function () { 23 | return { 24 | publish: publishSpy, 25 | subscribe: subscribeSpy, 26 | unsubscribe: unsubscribeSpy, 27 | on: (event, cb) => { 28 | if (event === 'message') { 29 | listener = cb; 30 | } 31 | }, 32 | }; 33 | }; 34 | 35 | mqttPackage['connect'] = connect; 36 | 37 | // -------------- Mocking mqtt Client ------------------ 38 | 39 | describe('MQTTPubSub', function () { 40 | 41 | const pubSub = new MQTTPubSub(); 42 | 43 | it('can subscribe to specific mqtt channel and called when a message is published on it', function (done) { 44 | let sub; 45 | const onMessage = message => { 46 | pubSub.unsubscribe(sub); 47 | 48 | try { 49 | expect(message).to.equals('test'); 50 | done(); 51 | } catch (e) { 52 | done(e); 53 | } 54 | }; 55 | 56 | pubSub.subscribe('Posts', onMessage).then(subId => { 57 | expect(subId).to.be.a('number'); 58 | pubSub.publish('Posts', 'test'); 59 | sub = subId; 60 | }).catch(err => done(err)); 61 | }); 62 | 63 | it('can unsubscribe from specific mqtt channel', function (done) { 64 | pubSub.subscribe('Posts', () => null).then(subId => { 65 | pubSub.unsubscribe(subId); 66 | 67 | try { 68 | expect(unsubscribeSpy.callCount).to.equals(1); 69 | const call = unsubscribeSpy.lastCall; 70 | expect(call.args).to.have.members(['Posts']); 71 | done(); 72 | 73 | } catch (e) { 74 | done(e); 75 | } 76 | }); 77 | }); 78 | 79 | it('cleans up correctly the memory when unsubscribing', function (done) { 80 | Promise.all([ 81 | pubSub.subscribe('Posts', () => null), 82 | pubSub.subscribe('Posts', () => null), 83 | ]) 84 | .then(([subId, secondSubId]) => { 85 | try { 86 | // This assertion is done against a private member, if you change the internals, you may want to change that 87 | expect((pubSub as any).subscriptionMap[subId]).not.to.be.an('undefined'); 88 | pubSub.unsubscribe(subId); 89 | 90 | // This assertion is done against a private member, if you change the internals, you may want to change that 91 | expect((pubSub as any).subscriptionMap[subId]).to.be.an('undefined'); 92 | expect(() => pubSub.unsubscribe(subId)).to.throw(`There is no subscription of id "${subId}"`); 93 | pubSub.unsubscribe(secondSubId); 94 | done(); 95 | } catch (e) { 96 | done(e); 97 | } 98 | }); 99 | }); 100 | 101 | it('will not unsubscribe from the mqtt channel if there is another subscriber on it\'s subscriber list', function (done) { 102 | let lastSubId; 103 | const onMessage = msg => { 104 | // Check onMessage support 105 | pubSub.unsubscribe(lastSubId); 106 | expect(unsubscribeSpy.callCount).to.equals(1); 107 | 108 | try { 109 | expect(msg).to.equals('test'); 110 | done(); 111 | } catch (e) { 112 | done(e); 113 | } 114 | }; 115 | 116 | const subscriptionPromises = [ 117 | pubSub.subscribe('Posts', () => { 118 | done('Not supposed to be triggered'); 119 | }), 120 | pubSub.subscribe('Posts', onMessage), 121 | ]; 122 | 123 | Promise.all(subscriptionPromises).then(subIds => { 124 | try { 125 | expect(subIds.length).to.equals(2); 126 | 127 | pubSub.unsubscribe(subIds[0]); 128 | expect(unsubscribeSpy.callCount).to.equals(0); 129 | 130 | pubSub.publish('Posts', 'test'); 131 | lastSubId = subIds[1]; 132 | } catch (e) { 133 | done(e); 134 | } 135 | }); 136 | }); 137 | 138 | it('will subscribe to mqtt channel only once', function (done) { 139 | const onMessage = () => null; 140 | 141 | pubSub.subscribe('Posts', onMessage).then(id1 => { 142 | return pubSub.subscribe('Posts', onMessage) 143 | .then(id2 => [id1, id2]); 144 | }).then(subIds => { 145 | try { 146 | expect(subIds.length).to.equals(2); 147 | expect(subscribeSpy.callCount).to.equals(1); 148 | 149 | pubSub.unsubscribe(subIds[0]); 150 | pubSub.unsubscribe(subIds[1]); 151 | done(); 152 | } catch (e) { 153 | done(e); 154 | } 155 | }); 156 | }); 157 | 158 | it('can have multiple subscribers and all will be called when a message is published to this channel', function (done) { 159 | let unSubIds = []; 160 | let callCount = 0; 161 | const onMessageSpy = spy(() => { 162 | callCount++; 163 | 164 | if (callCount === 2) { 165 | pubSub.unsubscribe(unSubIds[0]); 166 | pubSub.unsubscribe(unSubIds[1]); 167 | 168 | expect(onMessageSpy.callCount).to.equals(2); 169 | onMessageSpy.calls.forEach(call => { 170 | expect(call.args).to.have.members(['test']); 171 | }); 172 | 173 | done(); 174 | } 175 | }); 176 | const subscriptionPromises = [ 177 | pubSub.subscribe('Posts', onMessageSpy as Function), 178 | pubSub.subscribe('Posts', onMessageSpy as Function), 179 | ]; 180 | 181 | Promise.all(subscriptionPromises).then(subIds => { 182 | try { 183 | expect(subIds.length).to.equals(2); 184 | 185 | pubSub.publish('Posts', 'test'); 186 | 187 | unSubIds = subIds; 188 | } catch (e) { 189 | done(e); 190 | } 191 | }); 192 | }); 193 | 194 | it('can publish objects as well', function (done) { 195 | let unSubId; 196 | const onMessage = message => { 197 | pubSub.unsubscribe(unSubId); 198 | 199 | try { 200 | expect(message).to.have.property('comment', 'This is amazing'); 201 | done(); 202 | } catch (e) { 203 | done(e); 204 | } 205 | }; 206 | 207 | pubSub.subscribe('Posts', onMessage).then(subId => { 208 | try { 209 | pubSub.publish('Posts', { comment: 'This is amazing' }); 210 | unSubId = subId; 211 | } catch (e) { 212 | done(e); 213 | } 214 | }); 215 | }); 216 | 217 | it('throws if you try to unsubscribe with an unknown id', function () { 218 | return expect(() => pubSub.unsubscribe(123)) 219 | .to.throw('There is no subscription of id "123"'); 220 | }); 221 | 222 | it('can use transform function to convert the trigger name given into more explicit channel name', function (done) { 223 | const triggerTransform = (trigger, { repoName }) => `${trigger}.${repoName}`; 224 | const pubsub = new MQTTPubSub({ 225 | triggerTransform, 226 | }); 227 | 228 | let unSubId; 229 | const validateMessage = message => { 230 | pubsub.unsubscribe(unSubId); 231 | 232 | try { 233 | expect(message).to.equals('test'); 234 | done(); 235 | } catch (e) { 236 | done(e); 237 | } 238 | }; 239 | 240 | pubsub.subscribe('comments', validateMessage, { repoName: 'graphql-mqtt-subscriptions' }).then(subId => { 241 | pubsub.publish('comments.graphql-mqtt-subscriptions', 'test'); 242 | unSubId = subId; 243 | }); 244 | 245 | }); 246 | 247 | it('allows to change encodings of messages passed through MQTT broker', function (done) { 248 | const pubsub = new MQTTPubSub({ 249 | parseMessageWithEncoding: 'base64', 250 | }); 251 | 252 | let unSubId; 253 | const validateMessage = message => { 254 | pubsub.unsubscribe(unSubId); 255 | 256 | try { 257 | expect(message).to.equals('test'); 258 | done(); 259 | } catch (e) { 260 | done(e); 261 | } 262 | }; 263 | 264 | pubsub.subscribe('comments', validateMessage).then(subId => { 265 | pubsub.publish('comments', 'test'); 266 | unSubId = subId; 267 | }); 268 | }); 269 | 270 | it('allows to QoS for each publish topic', function (done) { 271 | const pubsub = new MQTTPubSub({ 272 | publishOptions: topic => { 273 | const qos: QoS = topic === 'comments' ? 2 : undefined; 274 | return Promise.resolve({ qos }); 275 | }, 276 | }); 277 | 278 | let unSubId; 279 | const validateMessage = message => { 280 | pubsub.unsubscribe(unSubId); 281 | 282 | try { 283 | expect(publishSpy.calls[0].args[2].qos).to.equals(2); 284 | expect(message).to.equals('test'); 285 | done(); 286 | } catch (e) { 287 | done(e); 288 | } 289 | }; 290 | 291 | pubsub.subscribe('comments', validateMessage).then(subId => { 292 | pubsub.publish('comments', 'test'); 293 | unSubId = subId; 294 | }); 295 | }); 296 | 297 | it('allows to set QoS for each topic subscription', function (done) { 298 | const pubsub = new MQTTPubSub({ 299 | subscribeOptions: topic => { 300 | const qos: QoS = topic === 'comments' ? 2 : undefined; 301 | return Promise.resolve({ qos }); 302 | }, 303 | onMQTTSubscribe: (id, granted) => { 304 | pubsub.unsubscribe(id); 305 | try { 306 | expect(granted[0].topic).to.equals('comments'); 307 | expect(granted[0].qos).to.equals(2); 308 | done(); 309 | } catch (e) { 310 | done(e); 311 | } 312 | }, 313 | }); 314 | 315 | pubsub.subscribe('comments', () => null).catch(done); 316 | }); 317 | 318 | afterEach('Reset spy count', () => { 319 | publishSpy.reset(); 320 | subscribeSpy.reset(); 321 | unsubscribeSpy.reset(); 322 | }); 323 | 324 | after('Restore mqtt client', () => { 325 | restore(); 326 | }); 327 | 328 | }); 329 | 330 | describe('PubSubAsyncIterator', function () { 331 | 332 | it('should expose valid asyncItrator for a specific event', () => { 333 | const pubSub = new MQTTPubSub(); 334 | const eventName = 'test'; 335 | const iterator = pubSub.asyncIterator(eventName); 336 | // tslint:disable-next-line:no-unused-expression 337 | expect(iterator).to.exist; 338 | // tslint:disable-next-line:no-unused-expression 339 | expect(isAsyncIterable(iterator)).to.be.true; 340 | }); 341 | 342 | it('should trigger event on asyncIterator when published', done => { 343 | const pubSub = new MQTTPubSub(); 344 | const eventName = 'test'; 345 | const iterator = pubSub.asyncIterator(eventName); 346 | 347 | iterator.next().then(result => { 348 | // tslint:disable-next-line:no-unused-expression 349 | expect(result).to.exist; 350 | // tslint:disable-next-line:no-unused-expression 351 | expect(result.value).to.exist; 352 | // tslint:disable-next-line:no-unused-expression 353 | expect(result.done).to.exist; 354 | done(); 355 | }); 356 | 357 | pubSub.publish(eventName, { test: true }); 358 | }); 359 | 360 | it('should not trigger event on asyncIterator when publishing other event', () => { 361 | const pubSub = new MQTTPubSub(); 362 | const eventName = 'test2'; 363 | const iterator = pubSub.asyncIterator('test'); 364 | const triggerSpy = spy(() => undefined); 365 | 366 | iterator.next().then(triggerSpy); 367 | pubSub.publish(eventName, { test: true }); 368 | expect(triggerSpy.callCount).to.equal(0); 369 | }); 370 | 371 | it('register to multiple events', done => { 372 | const pubSub = new MQTTPubSub(); 373 | const eventName = 'test2'; 374 | const iterator = pubSub.asyncIterator(['test', 'test2']); 375 | const triggerSpy = spy(() => undefined); 376 | 377 | iterator.next().then(() => { 378 | triggerSpy(); 379 | expect(triggerSpy.callCount).to.be.gte(1); 380 | done(); 381 | }); 382 | pubSub.publish(eventName, { test: true }); 383 | }); 384 | 385 | it('should not trigger event on asyncIterator already returned', done => { 386 | const pubSub = new MQTTPubSub(); 387 | const eventName = 'test'; 388 | const iterator = pubSub.asyncIterator(eventName); 389 | 390 | iterator.next().then(result => { 391 | // tslint:disable-next-line:no-unused-expression 392 | expect(result).to.exist; 393 | // tslint:disable-next-line:no-unused-expression 394 | expect(result.value).to.exist; 395 | expect(result.value.test).to.equal('word'); 396 | // tslint:disable-next-line:no-unused-expression 397 | expect(result.done).to.be.false; 398 | }); 399 | 400 | pubSub.publish(eventName, { test: 'word' }); 401 | 402 | iterator.next().then(result => { 403 | // tslint:disable-next-line:no-unused-expression 404 | expect(result).to.exist; 405 | // tslint:disable-next-line:no-unused-expression 406 | expect(result.value).not.to.exist; 407 | // tslint:disable-next-line:no-unused-expression 408 | expect(result.done).to.be.true; 409 | done(); 410 | }); 411 | 412 | iterator.return(); 413 | pubSub.publish(eventName, { test: true }); 414 | }); 415 | 416 | }); 417 | 418 | describe('Wildcards in subscription topic', function () { 419 | 420 | it('should receive message while subscribing to topic containing wildcard', done => { 421 | const pubSub = new MQTTPubSub(); 422 | let unSubIds = []; 423 | let callCount = 0; 424 | const onMessageSpy = spy(() => { 425 | callCount++; 426 | if (callCount === 3) { 427 | pubSub.unsubscribe(unSubIds[0]); 428 | pubSub.unsubscribe(unSubIds[1]); 429 | 430 | expect(onMessageSpy.callCount).to.equals(3); 431 | onMessageSpy.calls.forEach(call => { 432 | expect(call.args).to.have.members(['test']); 433 | }); 434 | 435 | done(); 436 | } 437 | }); 438 | const subscriptionPromises = [ 439 | pubSub.subscribe('Posts/#', onMessageSpy as Function), 440 | pubSub.subscribe('Posts/CategoryA', onMessageSpy as Function), 441 | ]; 442 | 443 | Promise.all(subscriptionPromises).then(subIds => { 444 | try { 445 | expect(subIds.length).to.equals(2); 446 | pubSub.publish('Posts/CategoryA', 'test'); 447 | pubSub.publish('Posts/CategoryB', 'test'); 448 | unSubIds = subIds; 449 | } catch (e) { 450 | done(e); 451 | } 452 | }); 453 | }); 454 | 455 | it('can subscribe to everything with "#" topic', function (done) { 456 | const pubSub = new MQTTPubSub(); 457 | let sub; 458 | let expectedMessages = ['test0', 'test1', 'test2', 'test3']; 459 | let messages = []; 460 | const onMessage = message => { 461 | try { 462 | if (messages.length === 3) { 463 | messages.push(message); 464 | expect(messages).to.deep.equal(expectedMessages); 465 | pubSub.unsubscribe(sub); 466 | done(); 467 | } else { 468 | messages.push(message); 469 | } 470 | } catch (e) { 471 | done(e); 472 | } 473 | }; 474 | 475 | pubSub.subscribe('#', onMessage).then(subId => { 476 | expect(subId).to.be.a('number'); 477 | pubSub.publish('Posts', 'test0'); 478 | pubSub.publish('Posts/A', 'test1'); 479 | pubSub.publish('Posts/A/B', 'test2'); 480 | pubSub.publish('Posts/A/D/C', 'test3'); 481 | sub = subId; 482 | }).catch(err => done(err)); 483 | }); 484 | 485 | it('can subscribe to only specific subset', function (done) { 486 | const pubSub = new MQTTPubSub(); 487 | let sub; 488 | let expectedMessages = ['test2', 'test3', 'test4']; 489 | let messages = []; 490 | const onMessage = message => { 491 | try { 492 | if (expectedMessages.indexOf(message) > -1) { 493 | if (messages.length === 2) { 494 | messages.push(message); 495 | expect(messages).to.deep.equal(expectedMessages); 496 | pubSub.unsubscribe(sub); 497 | done(); 498 | } else { 499 | messages.push(message); 500 | } 501 | } 502 | } catch (e) { 503 | done(e); 504 | } 505 | }; 506 | 507 | pubSub.subscribe('Posts/+/D', onMessage).then(subId => { 508 | expect(subId).to.be.a('number'); 509 | pubSub.publish('Posts/A', 'test1'); 510 | pubSub.publish('Posts/B/D', 'test2'); 511 | pubSub.publish('Posts/C/D', 'test3'); 512 | pubSub.publish('Posts/E/D', 'test4'); 513 | pubSub.publish('Posts/F/G', 'test5'); 514 | pubSub.publish('Posts/H/D/I', 'test6'); 515 | pubSub.publish('Posts', 'test7'); 516 | sub = subId; 517 | }).catch(err => done(err)); 518 | }); 519 | }); 520 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "noImplicitAny": false, 8 | "rootDir": "./src", 9 | "outDir": "./dist", 10 | "allowSyntheticDefaultImports": true, 11 | "pretty": true, 12 | "removeComments": true, 13 | "declaration": true, 14 | "lib": ["es6","esnext"] 15 | }, 16 | "exclude": [ 17 | "node_modules", 18 | "dist" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "align": [ 4 | false, 5 | "parameters", 6 | "arguments", 7 | "statements" 8 | ], 9 | "ban": false, 10 | "class-name": true, 11 | "curly": true, 12 | "eofline": true, 13 | "forin": true, 14 | "indent": [ 15 | true, 16 | "spaces" 17 | ], 18 | "interface-name": false, 19 | "jsdoc-format": true, 20 | "label-position": true, 21 | "max-line-length": [ 22 | true, 23 | 140 24 | ], 25 | "member-access": true, 26 | "member-ordering": [ 27 | true, 28 | "public-before-private", 29 | "static-before-instance", 30 | "variables-before-functions" 31 | ], 32 | "no-any": false, 33 | "no-arg": true, 34 | "no-bitwise": true, 35 | "no-conditional-assignment": true, 36 | "no-consecutive-blank-lines": false, 37 | "no-console": [ 38 | true, 39 | "log", 40 | "debug", 41 | "info", 42 | "time", 43 | "timeEnd", 44 | "trace" 45 | ], 46 | "no-construct": true, 47 | "no-parameter-properties": true, 48 | "no-debugger": true, 49 | "no-duplicate-variable": true, 50 | "no-empty": true, 51 | "no-eval": true, 52 | "no-inferrable-types": false, 53 | "no-internal-module": true, 54 | "no-null-keyword": false, 55 | "no-require-imports": false, 56 | "no-shadowed-variable": true, 57 | "no-switch-case-fall-through": true, 58 | "no-trailing-whitespace": true, 59 | "no-unused-expression": true, 60 | "no-use-before-declare": true, 61 | "no-var-keyword": true, 62 | "no-var-requires": true, 63 | "object-literal-sort-keys": false, 64 | "one-line": [ 65 | true, 66 | "check-open-brace", 67 | "check-catch", 68 | "check-else", 69 | "check-finally", 70 | "check-whitespace" 71 | ], 72 | "quotemark": [ 73 | true, 74 | "single", 75 | "avoid-escape" 76 | ], 77 | "radix": true, 78 | "semicolon": [ 79 | true, 80 | "always" 81 | ], 82 | "switch-default": true, 83 | "trailing-comma": [ 84 | true, 85 | { 86 | "multiline": "always", 87 | "singleline": "never" 88 | } 89 | ], 90 | "triple-equals": [ 91 | true, 92 | "allow-null-check" 93 | ], 94 | "typedef": [ 95 | false, 96 | "call-signature", 97 | "parameter", 98 | "arrow-parameter", 99 | "property-declaration", 100 | "variable-declaration", 101 | "member-variable-declaration" 102 | ], 103 | "typedef-whitespace": [ 104 | true, 105 | { 106 | "call-signature": "nospace", 107 | "index-signature": "nospace", 108 | "parameter": "nospace", 109 | "property-declaration": "nospace", 110 | "variable-declaration": "nospace" 111 | }, 112 | { 113 | "call-signature": "space", 114 | "index-signature": "space", 115 | "parameter": "space", 116 | "property-declaration": "space", 117 | "variable-declaration": "space" 118 | } 119 | ], 120 | "variable-name": [ 121 | true, 122 | "check-format", 123 | "allow-leading-underscore", 124 | "ban-keywords", 125 | "allow-pascal-case" 126 | ], 127 | "whitespace": [ 128 | true, 129 | "check-branch", 130 | "check-decl", 131 | "check-operator", 132 | "check-separator", 133 | "check-type" 134 | ] 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/chai-as-promised@0.0.30": 6 | version "0.0.30" 7 | resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-0.0.30.tgz#2341321cc796c6c3544a949a063e7609a222f303" 8 | dependencies: 9 | "@types/chai" "*" 10 | 11 | "@types/chai@*": 12 | version "4.0.1" 13 | resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.0.1.tgz#37fea779617cfec3fd2b19a0247e8bbdd5133bf6" 14 | 15 | "@types/chai@^3.4.34": 16 | version "3.5.2" 17 | resolved "https://registry.yarnpkg.com/@types/chai/-/chai-3.5.2.tgz#c11cd2817d3a401b7ba0f5a420f35c56139b1c1e" 18 | 19 | "@types/graphql@^0.9.0", "@types/graphql@^0.9.1": 20 | version "0.9.3" 21 | resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.9.3.tgz#4a2a888e110c796eb7671bf8f9ecef229ad67749" 22 | 23 | "@types/mocha@^2.2.33": 24 | version "2.2.41" 25 | resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-2.2.41.tgz#e27cf0817153eb9f2713b2d3f6c68f1e1c3ca608" 26 | 27 | "@types/node@7.0.18": 28 | version "7.0.18" 29 | resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.18.tgz#cd67f27d3dc0cfb746f0bdd5e086c4c5d55be173" 30 | 31 | "@types/simple-mock@0.0.27": 32 | version "0.0.27" 33 | resolved "https://registry.yarnpkg.com/@types/simple-mock/-/simple-mock-0.0.27.tgz#a9f58db60b5c09c66156467346ff069f8d66352a" 34 | 35 | abbrev@1: 36 | version "1.1.0" 37 | resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f" 38 | 39 | abbrev@1.0.x: 40 | version "1.0.9" 41 | resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" 42 | 43 | align-text@^0.1.1, align-text@^0.1.3: 44 | version "0.1.4" 45 | resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" 46 | dependencies: 47 | kind-of "^3.0.2" 48 | longest "^1.0.1" 49 | repeat-string "^1.5.2" 50 | 51 | amdefine@>=0.0.4, amdefine@^1.0.0: 52 | version "1.0.1" 53 | resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" 54 | 55 | ansi-regex@^2.0.0: 56 | version "2.1.1" 57 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" 58 | 59 | ansi-styles@^2.2.1: 60 | version "2.2.1" 61 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" 62 | 63 | append-transform@^0.4.0: 64 | version "0.4.0" 65 | resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991" 66 | dependencies: 67 | default-require-extensions "^1.0.0" 68 | 69 | argparse@^1.0.7: 70 | version "1.0.9" 71 | resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" 72 | dependencies: 73 | sprintf-js "~1.0.2" 74 | 75 | array-differ@^1.0.0: 76 | version "1.0.0" 77 | resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031" 78 | 79 | array-find-index@^1.0.1: 80 | version "1.0.2" 81 | resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" 82 | 83 | array-uniq@^1.0.2: 84 | version "1.0.3" 85 | resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" 86 | 87 | assertion-error@^1.0.1: 88 | version "1.0.2" 89 | resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.2.tgz#13ca515d86206da0bac66e834dd397d87581094c" 90 | 91 | async@1.x, async@^1.4.0: 92 | version "1.5.2" 93 | resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" 94 | 95 | async@^2.1.4: 96 | version "2.5.0" 97 | resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d" 98 | dependencies: 99 | lodash "^4.14.0" 100 | 101 | babel-code-frame@^6.22.0: 102 | version "6.22.0" 103 | resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" 104 | dependencies: 105 | chalk "^1.1.0" 106 | esutils "^2.0.2" 107 | js-tokens "^3.0.0" 108 | 109 | babel-generator@^6.18.0: 110 | version "6.25.0" 111 | resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.25.0.tgz#33a1af70d5f2890aeb465a4a7793c1df6a9ea9fc" 112 | dependencies: 113 | babel-messages "^6.23.0" 114 | babel-runtime "^6.22.0" 115 | babel-types "^6.25.0" 116 | detect-indent "^4.0.0" 117 | jsesc "^1.3.0" 118 | lodash "^4.2.0" 119 | source-map "^0.5.0" 120 | trim-right "^1.0.1" 121 | 122 | babel-messages@^6.23.0: 123 | version "6.23.0" 124 | resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" 125 | dependencies: 126 | babel-runtime "^6.22.0" 127 | 128 | babel-runtime@^6.22.0: 129 | version "6.23.0" 130 | resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b" 131 | dependencies: 132 | core-js "^2.4.0" 133 | regenerator-runtime "^0.10.0" 134 | 135 | babel-template@^6.16.0: 136 | version "6.25.0" 137 | resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.25.0.tgz#665241166b7c2aa4c619d71e192969552b10c071" 138 | dependencies: 139 | babel-runtime "^6.22.0" 140 | babel-traverse "^6.25.0" 141 | babel-types "^6.25.0" 142 | babylon "^6.17.2" 143 | lodash "^4.2.0" 144 | 145 | babel-traverse@^6.18.0, babel-traverse@^6.25.0: 146 | version "6.25.0" 147 | resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.25.0.tgz#2257497e2fcd19b89edc13c4c91381f9512496f1" 148 | dependencies: 149 | babel-code-frame "^6.22.0" 150 | babel-messages "^6.23.0" 151 | babel-runtime "^6.22.0" 152 | babel-types "^6.25.0" 153 | babylon "^6.17.2" 154 | debug "^2.2.0" 155 | globals "^9.0.0" 156 | invariant "^2.2.0" 157 | lodash "^4.2.0" 158 | 159 | babel-types@^6.18.0, babel-types@^6.25.0: 160 | version "6.25.0" 161 | resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.25.0.tgz#70afb248d5660e5d18f811d91c8303b54134a18e" 162 | dependencies: 163 | babel-runtime "^6.22.0" 164 | esutils "^2.0.2" 165 | lodash "^4.2.0" 166 | to-fast-properties "^1.0.1" 167 | 168 | babylon@^6.17.2, babylon@^6.17.4: 169 | version "6.17.4" 170 | resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.17.4.tgz#3e8b7402b88d22c3423e137a1577883b15ff869a" 171 | 172 | balanced-match@^1.0.0: 173 | version "1.0.0" 174 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 175 | 176 | beeper@^1.0.0: 177 | version "1.1.1" 178 | resolved "https://registry.yarnpkg.com/beeper/-/beeper-1.1.1.tgz#e6d5ea8c5dad001304a70b22638447f69cb2f809" 179 | 180 | bl@^1.2.1: 181 | version "1.2.1" 182 | resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.1.tgz#cac328f7bee45730d404b692203fcb590e172d5e" 183 | dependencies: 184 | readable-stream "^2.0.5" 185 | 186 | brace-expansion@^1.1.7: 187 | version "1.1.8" 188 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" 189 | dependencies: 190 | balanced-match "^1.0.0" 191 | concat-map "0.0.1" 192 | 193 | browser-stdout@1.3.0: 194 | version "1.3.0" 195 | resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" 196 | 197 | builtin-modules@^1.0.0: 198 | version "1.1.1" 199 | resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" 200 | 201 | callback-stream@^1.0.2: 202 | version "1.1.0" 203 | resolved "https://registry.yarnpkg.com/callback-stream/-/callback-stream-1.1.0.tgz#4701a51266f06e06eaa71fc17233822d875f4908" 204 | dependencies: 205 | inherits "^2.0.1" 206 | readable-stream "> 1.0.0 < 3.0.0" 207 | 208 | camelcase-keys@^2.0.0: 209 | version "2.1.0" 210 | resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" 211 | dependencies: 212 | camelcase "^2.0.0" 213 | map-obj "^1.0.0" 214 | 215 | camelcase@^1.0.2: 216 | version "1.2.1" 217 | resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" 218 | 219 | camelcase@^2.0.0: 220 | version "2.1.1" 221 | resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" 222 | 223 | center-align@^0.1.1: 224 | version "0.1.3" 225 | resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" 226 | dependencies: 227 | align-text "^0.1.3" 228 | lazy-cache "^1.0.3" 229 | 230 | chai-as-promised@^6.0.0: 231 | version "6.0.0" 232 | resolved "https://registry.yarnpkg.com/chai-as-promised/-/chai-as-promised-6.0.0.tgz#1a02a433a6f24dafac63b9c96fa1684db1aa8da6" 233 | dependencies: 234 | check-error "^1.0.2" 235 | 236 | chai@^3.5.0: 237 | version "3.5.0" 238 | resolved "https://registry.yarnpkg.com/chai/-/chai-3.5.0.tgz#4d02637b067fe958bdbfdd3a40ec56fef7373247" 239 | dependencies: 240 | assertion-error "^1.0.1" 241 | deep-eql "^0.1.3" 242 | type-detect "^1.0.0" 243 | 244 | chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1: 245 | version "1.1.3" 246 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" 247 | dependencies: 248 | ansi-styles "^2.2.1" 249 | escape-string-regexp "^1.0.2" 250 | has-ansi "^2.0.0" 251 | strip-ansi "^3.0.0" 252 | supports-color "^2.0.0" 253 | 254 | check-error@^1.0.2: 255 | version "1.0.2" 256 | resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" 257 | 258 | cliui@^2.1.0: 259 | version "2.1.0" 260 | resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" 261 | dependencies: 262 | center-align "^0.1.1" 263 | right-align "^0.1.1" 264 | wordwrap "0.0.2" 265 | 266 | clone-stats@^0.0.1: 267 | version "0.0.1" 268 | resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1" 269 | 270 | clone@^1.0.0: 271 | version "1.0.2" 272 | resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149" 273 | 274 | colors@^1.1.2: 275 | version "1.1.2" 276 | resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" 277 | 278 | commander@2.9.0: 279 | version "2.9.0" 280 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" 281 | dependencies: 282 | graceful-readlink ">= 1.0.0" 283 | 284 | commander@^2.9.0: 285 | version "2.11.0" 286 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" 287 | 288 | commist@^1.0.0: 289 | version "1.0.0" 290 | resolved "https://registry.yarnpkg.com/commist/-/commist-1.0.0.tgz#c0c352501cf6f52e9124e3ef89c9806e2022ebef" 291 | dependencies: 292 | leven "^1.0.0" 293 | minimist "^1.1.0" 294 | 295 | concat-map@0.0.1: 296 | version "0.0.1" 297 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 298 | 299 | concat-stream@^1.6.0: 300 | version "1.6.0" 301 | resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" 302 | dependencies: 303 | inherits "^2.0.3" 304 | readable-stream "^2.2.2" 305 | typedarray "^0.0.6" 306 | 307 | core-js@^2.4.0: 308 | version "2.4.1" 309 | resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" 310 | 311 | core-util-is@~1.0.0: 312 | version "1.0.2" 313 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 314 | 315 | currently-unhandled@^0.4.1: 316 | version "0.4.1" 317 | resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" 318 | dependencies: 319 | array-find-index "^1.0.1" 320 | 321 | dateformat@^1.0.11: 322 | version "1.0.12" 323 | resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9" 324 | dependencies: 325 | get-stdin "^4.0.1" 326 | meow "^3.3.0" 327 | 328 | debug@2.6.0: 329 | version "2.6.0" 330 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.0.tgz#bc596bcabe7617f11d9fa15361eded5608b8499b" 331 | dependencies: 332 | ms "0.7.2" 333 | 334 | debug@^2.2.0, debug@^2.6.3: 335 | version "2.6.8" 336 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" 337 | dependencies: 338 | ms "2.0.0" 339 | 340 | decamelize@^1.0.0, decamelize@^1.1.2: 341 | version "1.2.0" 342 | resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" 343 | 344 | deep-eql@^0.1.3: 345 | version "0.1.3" 346 | resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-0.1.3.tgz#ef558acab8de25206cd713906d74e56930eb69f2" 347 | dependencies: 348 | type-detect "0.1.1" 349 | 350 | deep-is@~0.1.3: 351 | version "0.1.3" 352 | resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" 353 | 354 | default-require-extensions@^1.0.0: 355 | version "1.0.0" 356 | resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8" 357 | dependencies: 358 | strip-bom "^2.0.0" 359 | 360 | detect-indent@^4.0.0: 361 | version "4.0.0" 362 | resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" 363 | dependencies: 364 | repeating "^2.0.0" 365 | 366 | diff@3.2.0: 367 | version "3.2.0" 368 | resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" 369 | 370 | diff@^3.2.0: 371 | version "3.3.0" 372 | resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.0.tgz#056695150d7aa93237ca7e378ac3b1682b7963b9" 373 | 374 | duplexer2@0.0.2: 375 | version "0.0.2" 376 | resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.0.2.tgz#c614dcf67e2fb14995a91711e5a617e8a60a31db" 377 | dependencies: 378 | readable-stream "~1.1.9" 379 | 380 | duplexify@^3.1.2, duplexify@^3.2.0: 381 | version "3.5.0" 382 | resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.0.tgz#1aa773002e1578457e9d9d4a50b0ccaaebcbd604" 383 | dependencies: 384 | end-of-stream "1.0.0" 385 | inherits "^2.0.1" 386 | readable-stream "^2.0.0" 387 | stream-shift "^1.0.0" 388 | 389 | end-of-stream@1.0.0: 390 | version "1.0.0" 391 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.0.0.tgz#d4596e702734a93e40e9af864319eabd99ff2f0e" 392 | dependencies: 393 | once "~1.3.0" 394 | 395 | end-of-stream@^1.1.0: 396 | version "1.4.0" 397 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.0.tgz#7a90d833efda6cfa6eac0f4949dbb0fad3a63206" 398 | dependencies: 399 | once "^1.4.0" 400 | 401 | error-ex@^1.2.0: 402 | version "1.3.1" 403 | resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" 404 | dependencies: 405 | is-arrayish "^0.2.1" 406 | 407 | es6-promise@^4.0.5: 408 | version "4.1.1" 409 | resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.1.1.tgz#8811e90915d9a0dba36274f0b242dbda78f9c92a" 410 | 411 | escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2: 412 | version "1.0.5" 413 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 414 | 415 | escodegen@1.8.x: 416 | version "1.8.1" 417 | resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.8.1.tgz#5a5b53af4693110bebb0867aa3430dd3b70a1018" 418 | dependencies: 419 | esprima "^2.7.1" 420 | estraverse "^1.9.1" 421 | esutils "^2.0.2" 422 | optionator "^0.8.1" 423 | optionalDependencies: 424 | source-map "~0.2.0" 425 | 426 | esprima@2.7.x, esprima@^2.7.1: 427 | version "2.7.3" 428 | resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" 429 | 430 | esprima@^3.1.1: 431 | version "3.1.3" 432 | resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" 433 | 434 | estraverse@^1.9.1: 435 | version "1.9.3" 436 | resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44" 437 | 438 | esutils@^2.0.2: 439 | version "2.0.2" 440 | resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" 441 | 442 | extend-shallow@^2.0.1: 443 | version "2.0.1" 444 | resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" 445 | dependencies: 446 | is-extendable "^0.1.0" 447 | 448 | extend@^3.0.0: 449 | version "3.0.1" 450 | resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" 451 | 452 | fancy-log@^1.1.0: 453 | version "1.3.0" 454 | resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.0.tgz#45be17d02bb9917d60ccffd4995c999e6c8c9948" 455 | dependencies: 456 | chalk "^1.1.1" 457 | time-stamp "^1.0.0" 458 | 459 | fast-levenshtein@~2.0.4: 460 | version "2.0.6" 461 | resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" 462 | 463 | fileset@^2.0.2: 464 | version "2.0.3" 465 | resolved "https://registry.yarnpkg.com/fileset/-/fileset-2.0.3.tgz#8e7548a96d3cc2327ee5e674168723a333bba2a0" 466 | dependencies: 467 | glob "^7.0.3" 468 | minimatch "^3.0.3" 469 | 470 | find-up@^1.0.0: 471 | version "1.1.2" 472 | resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" 473 | dependencies: 474 | path-exists "^2.0.0" 475 | pinkie-promise "^2.0.0" 476 | 477 | fs.realpath@^1.0.0: 478 | version "1.0.0" 479 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 480 | 481 | get-stdin@^4.0.1: 482 | version "4.0.1" 483 | resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" 484 | 485 | glob-parent@^3.1.0: 486 | version "3.1.0" 487 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" 488 | dependencies: 489 | is-glob "^3.1.0" 490 | path-dirname "^1.0.0" 491 | 492 | glob-stream@^6.1.0: 493 | version "6.1.0" 494 | resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-6.1.0.tgz#7045c99413b3eb94888d83ab46d0b404cc7bdde4" 495 | dependencies: 496 | extend "^3.0.0" 497 | glob "^7.1.1" 498 | glob-parent "^3.1.0" 499 | is-negated-glob "^1.0.0" 500 | ordered-read-streams "^1.0.0" 501 | pumpify "^1.3.5" 502 | readable-stream "^2.1.5" 503 | remove-trailing-separator "^1.0.1" 504 | to-absolute-glob "^2.0.0" 505 | unique-stream "^2.0.2" 506 | 507 | glob@7.1.1: 508 | version "7.1.1" 509 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" 510 | dependencies: 511 | fs.realpath "^1.0.0" 512 | inflight "^1.0.4" 513 | inherits "2" 514 | minimatch "^3.0.2" 515 | once "^1.3.0" 516 | path-is-absolute "^1.0.0" 517 | 518 | glob@^5.0.15: 519 | version "5.0.15" 520 | resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" 521 | dependencies: 522 | inflight "^1.0.4" 523 | inherits "2" 524 | minimatch "2 || 3" 525 | once "^1.3.0" 526 | path-is-absolute "^1.0.0" 527 | 528 | glob@^7.0.3, glob@^7.0.5, glob@^7.1.1: 529 | version "7.1.2" 530 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" 531 | dependencies: 532 | fs.realpath "^1.0.0" 533 | inflight "^1.0.4" 534 | inherits "2" 535 | minimatch "^3.0.4" 536 | once "^1.3.0" 537 | path-is-absolute "^1.0.0" 538 | 539 | globals@^9.0.0: 540 | version "9.18.0" 541 | resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" 542 | 543 | glogg@^1.0.0: 544 | version "1.0.0" 545 | resolved "https://registry.yarnpkg.com/glogg/-/glogg-1.0.0.tgz#7fe0f199f57ac906cf512feead8f90ee4a284fc5" 546 | dependencies: 547 | sparkles "^1.0.0" 548 | 549 | graceful-fs@^4.1.2: 550 | version "4.1.11" 551 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" 552 | 553 | "graceful-readlink@>= 1.0.0": 554 | version "1.0.1" 555 | resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" 556 | 557 | graphql-subscriptions@^0.4.2: 558 | version "0.4.4" 559 | resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-0.4.4.tgz#39cff32d08dd3c990113864bab77154403727e9b" 560 | dependencies: 561 | "@types/graphql" "^0.9.1" 562 | es6-promise "^4.0.5" 563 | iterall "^1.1.1" 564 | 565 | graphql@^0.10.1: 566 | version "0.10.3" 567 | resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.10.3.tgz#c313afd5518e673351bee18fb63e2a0e487407ab" 568 | dependencies: 569 | iterall "^1.1.0" 570 | 571 | growl@1.9.2: 572 | version "1.9.2" 573 | resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" 574 | 575 | gulp-util@3.0.7: 576 | version "3.0.7" 577 | resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.7.tgz#78925c4b8f8b49005ac01a011c557e6218941cbb" 578 | dependencies: 579 | array-differ "^1.0.0" 580 | array-uniq "^1.0.2" 581 | beeper "^1.0.0" 582 | chalk "^1.0.0" 583 | dateformat "^1.0.11" 584 | fancy-log "^1.1.0" 585 | gulplog "^1.0.0" 586 | has-gulplog "^0.1.0" 587 | lodash._reescape "^3.0.0" 588 | lodash._reevaluate "^3.0.0" 589 | lodash._reinterpolate "^3.0.0" 590 | lodash.template "^3.0.0" 591 | minimist "^1.1.0" 592 | multipipe "^0.1.2" 593 | object-assign "^3.0.0" 594 | replace-ext "0.0.1" 595 | through2 "^2.0.0" 596 | vinyl "^0.5.0" 597 | 598 | gulplog@^1.0.0: 599 | version "1.0.0" 600 | resolved "https://registry.yarnpkg.com/gulplog/-/gulplog-1.0.0.tgz#e28c4d45d05ecbbed818363ce8f9c5926229ffe5" 601 | dependencies: 602 | glogg "^1.0.0" 603 | 604 | handlebars@^4.0.1, handlebars@^4.0.3: 605 | version "4.0.10" 606 | resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.10.tgz#3d30c718b09a3d96f23ea4cc1f403c4d3ba9ff4f" 607 | dependencies: 608 | async "^1.4.0" 609 | optimist "^0.6.1" 610 | source-map "^0.4.4" 611 | optionalDependencies: 612 | uglify-js "^2.6" 613 | 614 | has-ansi@^2.0.0: 615 | version "2.0.0" 616 | resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" 617 | dependencies: 618 | ansi-regex "^2.0.0" 619 | 620 | has-flag@^1.0.0: 621 | version "1.0.0" 622 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" 623 | 624 | has-gulplog@^0.1.0: 625 | version "0.1.0" 626 | resolved "https://registry.yarnpkg.com/has-gulplog/-/has-gulplog-0.1.0.tgz#6414c82913697da51590397dafb12f22967811ce" 627 | dependencies: 628 | sparkles "^1.0.0" 629 | 630 | help-me@^1.0.1: 631 | version "1.1.0" 632 | resolved "https://registry.yarnpkg.com/help-me/-/help-me-1.1.0.tgz#8f2d508d0600b4a456da2f086556e7e5c056a3c6" 633 | dependencies: 634 | callback-stream "^1.0.2" 635 | glob-stream "^6.1.0" 636 | through2 "^2.0.1" 637 | xtend "^4.0.0" 638 | 639 | hosted-git-info@^2.1.4: 640 | version "2.5.0" 641 | resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" 642 | 643 | indent-string@^2.1.0: 644 | version "2.1.0" 645 | resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" 646 | dependencies: 647 | repeating "^2.0.0" 648 | 649 | inflight@^1.0.4: 650 | version "1.0.6" 651 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 652 | dependencies: 653 | once "^1.3.0" 654 | wrappy "1" 655 | 656 | inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: 657 | version "2.0.3" 658 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 659 | 660 | invariant@^2.2.0: 661 | version "2.2.2" 662 | resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" 663 | dependencies: 664 | loose-envify "^1.0.0" 665 | 666 | is-absolute@^0.2.5: 667 | version "0.2.6" 668 | resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-0.2.6.tgz#20de69f3db942ef2d87b9c2da36f172235b1b5eb" 669 | dependencies: 670 | is-relative "^0.2.1" 671 | is-windows "^0.2.0" 672 | 673 | is-arrayish@^0.2.1: 674 | version "0.2.1" 675 | resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" 676 | 677 | is-buffer@^1.1.5: 678 | version "1.1.5" 679 | resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc" 680 | 681 | is-builtin-module@^1.0.0: 682 | version "1.0.0" 683 | resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" 684 | dependencies: 685 | builtin-modules "^1.0.0" 686 | 687 | is-extendable@^0.1.0: 688 | version "0.1.1" 689 | resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" 690 | 691 | is-extglob@^2.1.0: 692 | version "2.1.1" 693 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" 694 | 695 | is-finite@^1.0.0: 696 | version "1.0.2" 697 | resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" 698 | dependencies: 699 | number-is-nan "^1.0.0" 700 | 701 | is-glob@^3.1.0: 702 | version "3.1.0" 703 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" 704 | dependencies: 705 | is-extglob "^2.1.0" 706 | 707 | is-negated-glob@^1.0.0: 708 | version "1.0.0" 709 | resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" 710 | 711 | is-relative@^0.2.1: 712 | version "0.2.1" 713 | resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-0.2.1.tgz#d27f4c7d516d175fb610db84bbeef23c3bc97aa5" 714 | dependencies: 715 | is-unc-path "^0.1.1" 716 | 717 | is-unc-path@^0.1.1: 718 | version "0.1.2" 719 | resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-0.1.2.tgz#6ab053a72573c10250ff416a3814c35178af39b9" 720 | dependencies: 721 | unc-path-regex "^0.1.0" 722 | 723 | is-utf8@^0.2.0: 724 | version "0.2.1" 725 | resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" 726 | 727 | is-windows@^0.2.0: 728 | version "0.2.0" 729 | resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c" 730 | 731 | isarray@0.0.1: 732 | version "0.0.1" 733 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" 734 | 735 | isarray@~1.0.0: 736 | version "1.0.0" 737 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 738 | 739 | isexe@^2.0.0: 740 | version "2.0.0" 741 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" 742 | 743 | istanbul-api@^1.0.0-alpha: 744 | version "1.1.10" 745 | resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.1.10.tgz#f27e5e7125c8de13f6a80661af78f512e5439b2b" 746 | dependencies: 747 | async "^2.1.4" 748 | fileset "^2.0.2" 749 | istanbul-lib-coverage "^1.1.1" 750 | istanbul-lib-hook "^1.0.7" 751 | istanbul-lib-instrument "^1.7.3" 752 | istanbul-lib-report "^1.1.1" 753 | istanbul-lib-source-maps "^1.2.1" 754 | istanbul-reports "^1.1.1" 755 | js-yaml "^3.7.0" 756 | mkdirp "^0.5.1" 757 | once "^1.4.0" 758 | 759 | istanbul-lib-coverage@^1.1.1: 760 | version "1.1.1" 761 | resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz#73bfb998885299415c93d38a3e9adf784a77a9da" 762 | 763 | istanbul-lib-hook@^1.0.7: 764 | version "1.0.7" 765 | resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.0.7.tgz#dd6607f03076578fe7d6f2a630cf143b49bacddc" 766 | dependencies: 767 | append-transform "^0.4.0" 768 | 769 | istanbul-lib-instrument@^1.7.3: 770 | version "1.7.3" 771 | resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.7.3.tgz#925b239163eabdd68cc4048f52c2fa4f899ecfa7" 772 | dependencies: 773 | babel-generator "^6.18.0" 774 | babel-template "^6.16.0" 775 | babel-traverse "^6.18.0" 776 | babel-types "^6.18.0" 777 | babylon "^6.17.4" 778 | istanbul-lib-coverage "^1.1.1" 779 | semver "^5.3.0" 780 | 781 | istanbul-lib-report@^1.1.1: 782 | version "1.1.1" 783 | resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz#f0e55f56655ffa34222080b7a0cd4760e1405fc9" 784 | dependencies: 785 | istanbul-lib-coverage "^1.1.1" 786 | mkdirp "^0.5.1" 787 | path-parse "^1.0.5" 788 | supports-color "^3.1.2" 789 | 790 | istanbul-lib-source-maps@^1.2.1: 791 | version "1.2.1" 792 | resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.1.tgz#a6fe1acba8ce08eebc638e572e294d267008aa0c" 793 | dependencies: 794 | debug "^2.6.3" 795 | istanbul-lib-coverage "^1.1.1" 796 | mkdirp "^0.5.1" 797 | rimraf "^2.6.1" 798 | source-map "^0.5.3" 799 | 800 | istanbul-reports@^1.1.1: 801 | version "1.1.1" 802 | resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.1.1.tgz#042be5c89e175bc3f86523caab29c014e77fee4e" 803 | dependencies: 804 | handlebars "^4.0.3" 805 | 806 | istanbul@0.4.5: 807 | version "0.4.5" 808 | resolved "https://registry.yarnpkg.com/istanbul/-/istanbul-0.4.5.tgz#65c7d73d4c4da84d4f3ac310b918fb0b8033733b" 809 | dependencies: 810 | abbrev "1.0.x" 811 | async "1.x" 812 | escodegen "1.8.x" 813 | esprima "2.7.x" 814 | glob "^5.0.15" 815 | handlebars "^4.0.1" 816 | js-yaml "3.x" 817 | mkdirp "0.5.x" 818 | nopt "3.x" 819 | once "1.x" 820 | resolve "1.1.x" 821 | supports-color "^3.1.0" 822 | which "^1.1.1" 823 | wordwrap "^1.0.0" 824 | 825 | istanbul@1.0.0-alpha.2: 826 | version "1.0.0-alpha.2" 827 | resolved "https://registry.yarnpkg.com/istanbul/-/istanbul-1.0.0-alpha.2.tgz#06096bc08e98baad744aae46962d8df9fac63d08" 828 | dependencies: 829 | abbrev "1.0.x" 830 | async "1.x" 831 | istanbul-api "^1.0.0-alpha" 832 | js-yaml "3.x" 833 | mkdirp "0.5.x" 834 | nopt "3.x" 835 | which "^1.1.1" 836 | wordwrap "^1.0.0" 837 | 838 | iterall@^1.1.0, iterall@^1.1.1: 839 | version "1.1.1" 840 | resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.1.1.tgz#f7f0af11e9a04ec6426260f5019d9fcca4d50214" 841 | 842 | js-tokens@^3.0.0: 843 | version "3.0.2" 844 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" 845 | 846 | js-yaml@3.x, js-yaml@^3.7.0: 847 | version "3.8.4" 848 | resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.4.tgz#520b4564f86573ba96662af85a8cafa7b4b5a6f6" 849 | dependencies: 850 | argparse "^1.0.7" 851 | esprima "^3.1.1" 852 | 853 | jsesc@^1.3.0: 854 | version "1.3.0" 855 | resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" 856 | 857 | json-stable-stringify@^1.0.0: 858 | version "1.0.1" 859 | resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" 860 | dependencies: 861 | jsonify "~0.0.0" 862 | 863 | json3@3.3.2: 864 | version "3.3.2" 865 | resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" 866 | 867 | jsonify@~0.0.0: 868 | version "0.0.0" 869 | resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" 870 | 871 | kind-of@^3.0.2: 872 | version "3.2.2" 873 | resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" 874 | dependencies: 875 | is-buffer "^1.1.5" 876 | 877 | lazy-cache@^1.0.3: 878 | version "1.0.4" 879 | resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" 880 | 881 | leven@^1.0.0: 882 | version "1.0.2" 883 | resolved "https://registry.yarnpkg.com/leven/-/leven-1.0.2.tgz#9144b6eebca5f1d0680169f1a6770dcea60b75c3" 884 | 885 | levn@~0.3.0: 886 | version "0.3.0" 887 | resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" 888 | dependencies: 889 | prelude-ls "~1.1.2" 890 | type-check "~0.3.2" 891 | 892 | load-json-file@^1.0.0: 893 | version "1.1.0" 894 | resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" 895 | dependencies: 896 | graceful-fs "^4.1.2" 897 | parse-json "^2.2.0" 898 | pify "^2.0.0" 899 | pinkie-promise "^2.0.0" 900 | strip-bom "^2.0.0" 901 | 902 | lodash._baseassign@^3.0.0: 903 | version "3.2.0" 904 | resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" 905 | dependencies: 906 | lodash._basecopy "^3.0.0" 907 | lodash.keys "^3.0.0" 908 | 909 | lodash._basecopy@^3.0.0: 910 | version "3.0.1" 911 | resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" 912 | 913 | lodash._basecreate@^3.0.0: 914 | version "3.0.3" 915 | resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz#1bc661614daa7fc311b7d03bf16806a0213cf821" 916 | 917 | lodash._basetostring@^3.0.0: 918 | version "3.0.1" 919 | resolved "https://registry.yarnpkg.com/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz#d1861d877f824a52f669832dcaf3ee15566a07d5" 920 | 921 | lodash._basevalues@^3.0.0: 922 | version "3.0.0" 923 | resolved "https://registry.yarnpkg.com/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz#5b775762802bde3d3297503e26300820fdf661b7" 924 | 925 | lodash._getnative@^3.0.0: 926 | version "3.9.1" 927 | resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" 928 | 929 | lodash._isiterateecall@^3.0.0: 930 | version "3.0.9" 931 | resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" 932 | 933 | lodash._reescape@^3.0.0: 934 | version "3.0.0" 935 | resolved "https://registry.yarnpkg.com/lodash._reescape/-/lodash._reescape-3.0.0.tgz#2b1d6f5dfe07c8a355753e5f27fac7f1cde1616a" 936 | 937 | lodash._reevaluate@^3.0.0: 938 | version "3.0.0" 939 | resolved "https://registry.yarnpkg.com/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz#58bc74c40664953ae0b124d806996daca431e2ed" 940 | 941 | lodash._reinterpolate@^3.0.0: 942 | version "3.0.0" 943 | resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" 944 | 945 | lodash._root@^3.0.0: 946 | version "3.0.1" 947 | resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692" 948 | 949 | lodash.create@3.1.1: 950 | version "3.1.1" 951 | resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7" 952 | dependencies: 953 | lodash._baseassign "^3.0.0" 954 | lodash._basecreate "^3.0.0" 955 | lodash._isiterateecall "^3.0.0" 956 | 957 | lodash.escape@^3.0.0: 958 | version "3.2.0" 959 | resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-3.2.0.tgz#995ee0dc18c1b48cc92effae71a10aab5b487698" 960 | dependencies: 961 | lodash._root "^3.0.0" 962 | 963 | lodash.isarguments@^3.0.0: 964 | version "3.1.0" 965 | resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" 966 | 967 | lodash.isarray@^3.0.0: 968 | version "3.0.4" 969 | resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" 970 | 971 | lodash.keys@^3.0.0: 972 | version "3.1.2" 973 | resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" 974 | dependencies: 975 | lodash._getnative "^3.0.0" 976 | lodash.isarguments "^3.0.0" 977 | lodash.isarray "^3.0.0" 978 | 979 | lodash.restparam@^3.0.0: 980 | version "3.6.1" 981 | resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" 982 | 983 | lodash.template@^3.0.0: 984 | version "3.6.2" 985 | resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-3.6.2.tgz#f8cdecc6169a255be9098ae8b0c53d378931d14f" 986 | dependencies: 987 | lodash._basecopy "^3.0.0" 988 | lodash._basetostring "^3.0.0" 989 | lodash._basevalues "^3.0.0" 990 | lodash._isiterateecall "^3.0.0" 991 | lodash._reinterpolate "^3.0.0" 992 | lodash.escape "^3.0.0" 993 | lodash.keys "^3.0.0" 994 | lodash.restparam "^3.0.0" 995 | lodash.templatesettings "^3.0.0" 996 | 997 | lodash.templatesettings@^3.0.0: 998 | version "3.1.1" 999 | resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz#fb307844753b66b9f1afa54e262c745307dba8e5" 1000 | dependencies: 1001 | lodash._reinterpolate "^3.0.0" 1002 | lodash.escape "^3.0.0" 1003 | 1004 | lodash@^4.14.0, lodash@^4.2.0: 1005 | version "4.17.4" 1006 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" 1007 | 1008 | longest@^1.0.1: 1009 | version "1.0.1" 1010 | resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" 1011 | 1012 | loose-envify@^1.0.0: 1013 | version "1.3.1" 1014 | resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" 1015 | dependencies: 1016 | js-tokens "^3.0.0" 1017 | 1018 | loud-rejection@^1.0.0: 1019 | version "1.6.0" 1020 | resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" 1021 | dependencies: 1022 | currently-unhandled "^0.4.1" 1023 | signal-exit "^3.0.0" 1024 | 1025 | map-obj@^1.0.0, map-obj@^1.0.1: 1026 | version "1.0.1" 1027 | resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" 1028 | 1029 | meow@^3.3.0: 1030 | version "3.7.0" 1031 | resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" 1032 | dependencies: 1033 | camelcase-keys "^2.0.0" 1034 | decamelize "^1.1.2" 1035 | loud-rejection "^1.0.0" 1036 | map-obj "^1.0.1" 1037 | minimist "^1.1.3" 1038 | normalize-package-data "^2.3.4" 1039 | object-assign "^4.0.1" 1040 | read-pkg-up "^1.0.1" 1041 | redent "^1.0.0" 1042 | trim-newlines "^1.0.0" 1043 | 1044 | "minimatch@2 || 3", minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4: 1045 | version "3.0.4" 1046 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 1047 | dependencies: 1048 | brace-expansion "^1.1.7" 1049 | 1050 | minimist@0.0.8: 1051 | version "0.0.8" 1052 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 1053 | 1054 | minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0: 1055 | version "1.2.0" 1056 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" 1057 | 1058 | minimist@~0.0.1: 1059 | version "0.0.10" 1060 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" 1061 | 1062 | mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@^0.5.1: 1063 | version "0.5.1" 1064 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 1065 | dependencies: 1066 | minimist "0.0.8" 1067 | 1068 | mocha@^3.0.0: 1069 | version "3.4.2" 1070 | resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.4.2.tgz#d0ef4d332126dbf18d0d640c9b382dd48be97594" 1071 | dependencies: 1072 | browser-stdout "1.3.0" 1073 | commander "2.9.0" 1074 | debug "2.6.0" 1075 | diff "3.2.0" 1076 | escape-string-regexp "1.0.5" 1077 | glob "7.1.1" 1078 | growl "1.9.2" 1079 | json3 "3.3.2" 1080 | lodash.create "3.1.1" 1081 | mkdirp "0.5.1" 1082 | supports-color "3.1.2" 1083 | 1084 | mqtt-packet@^5.2.1: 1085 | version "5.4.0" 1086 | resolved "https://registry.yarnpkg.com/mqtt-packet/-/mqtt-packet-5.4.0.tgz#387104c06aa68fbb9f8159d0c722dd5c3e45df22" 1087 | dependencies: 1088 | bl "^1.2.1" 1089 | inherits "^2.0.3" 1090 | process-nextick-args "^1.0.7" 1091 | safe-buffer "^5.1.0" 1092 | 1093 | mqtt@^2.3.0: 1094 | version "2.9.1" 1095 | resolved "https://registry.yarnpkg.com/mqtt/-/mqtt-2.9.1.tgz#fef63535508b0ba70bcdff44ad2bd0ccebe57b62" 1096 | dependencies: 1097 | commist "^1.0.0" 1098 | concat-stream "^1.6.0" 1099 | end-of-stream "^1.1.0" 1100 | help-me "^1.0.1" 1101 | inherits "^2.0.3" 1102 | minimist "^1.2.0" 1103 | mqtt-packet "^5.2.1" 1104 | pump "^1.0.2" 1105 | readable-stream "^2.2.11" 1106 | reinterval "^1.1.0" 1107 | split2 "^2.1.1" 1108 | websocket-stream "^5.0.0" 1109 | xtend "^4.0.1" 1110 | 1111 | ms@0.7.2: 1112 | version "0.7.2" 1113 | resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" 1114 | 1115 | ms@2.0.0: 1116 | version "2.0.0" 1117 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 1118 | 1119 | multipipe@^0.1.2: 1120 | version "0.1.2" 1121 | resolved "https://registry.yarnpkg.com/multipipe/-/multipipe-0.1.2.tgz#2a8f2ddf70eed564dff2d57f1e1a137d9f05078b" 1122 | dependencies: 1123 | duplexer2 "0.0.2" 1124 | 1125 | nopt@3.x: 1126 | version "3.0.6" 1127 | resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" 1128 | dependencies: 1129 | abbrev "1" 1130 | 1131 | normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: 1132 | version "2.4.0" 1133 | resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" 1134 | dependencies: 1135 | hosted-git-info "^2.1.4" 1136 | is-builtin-module "^1.0.0" 1137 | semver "2 || 3 || 4 || 5" 1138 | validate-npm-package-license "^3.0.1" 1139 | 1140 | number-is-nan@^1.0.0: 1141 | version "1.0.1" 1142 | resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" 1143 | 1144 | object-assign@^3.0.0: 1145 | version "3.0.0" 1146 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" 1147 | 1148 | object-assign@^4.0.1: 1149 | version "4.1.1" 1150 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 1151 | 1152 | once@1.x, once@^1.3.0, once@^1.3.1, once@^1.4.0: 1153 | version "1.4.0" 1154 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 1155 | dependencies: 1156 | wrappy "1" 1157 | 1158 | once@~1.3.0: 1159 | version "1.3.3" 1160 | resolved "https://registry.yarnpkg.com/once/-/once-1.3.3.tgz#b2e261557ce4c314ec8304f3fa82663e4297ca20" 1161 | dependencies: 1162 | wrappy "1" 1163 | 1164 | optimist@^0.6.1: 1165 | version "0.6.1" 1166 | resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" 1167 | dependencies: 1168 | minimist "~0.0.1" 1169 | wordwrap "~0.0.2" 1170 | 1171 | optionator@^0.8.1: 1172 | version "0.8.2" 1173 | resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" 1174 | dependencies: 1175 | deep-is "~0.1.3" 1176 | fast-levenshtein "~2.0.4" 1177 | levn "~0.3.0" 1178 | prelude-ls "~1.1.2" 1179 | type-check "~0.3.2" 1180 | wordwrap "~1.0.0" 1181 | 1182 | ordered-read-streams@^1.0.0: 1183 | version "1.0.1" 1184 | resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz#77c0cb37c41525d64166d990ffad7ec6a0e1363e" 1185 | dependencies: 1186 | readable-stream "^2.0.1" 1187 | 1188 | parse-json@^2.2.0: 1189 | version "2.2.0" 1190 | resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" 1191 | dependencies: 1192 | error-ex "^1.2.0" 1193 | 1194 | path-dirname@^1.0.0: 1195 | version "1.0.2" 1196 | resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" 1197 | 1198 | path-exists@^2.0.0: 1199 | version "2.1.0" 1200 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" 1201 | dependencies: 1202 | pinkie-promise "^2.0.0" 1203 | 1204 | path-is-absolute@^1.0.0: 1205 | version "1.0.1" 1206 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 1207 | 1208 | path-parse@^1.0.5: 1209 | version "1.0.5" 1210 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" 1211 | 1212 | path-type@^1.0.0: 1213 | version "1.1.0" 1214 | resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" 1215 | dependencies: 1216 | graceful-fs "^4.1.2" 1217 | pify "^2.0.0" 1218 | pinkie-promise "^2.0.0" 1219 | 1220 | pify@^2.0.0: 1221 | version "2.3.0" 1222 | resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" 1223 | 1224 | pinkie-promise@^2.0.0: 1225 | version "2.0.1" 1226 | resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" 1227 | dependencies: 1228 | pinkie "^2.0.0" 1229 | 1230 | pinkie@^2.0.0: 1231 | version "2.0.4" 1232 | resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" 1233 | 1234 | prelude-ls@~1.1.2: 1235 | version "1.1.2" 1236 | resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" 1237 | 1238 | process-nextick-args@^1.0.7, process-nextick-args@~1.0.6: 1239 | version "1.0.7" 1240 | resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" 1241 | 1242 | pump@^1.0.0, pump@^1.0.2: 1243 | version "1.0.2" 1244 | resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.2.tgz#3b3ee6512f94f0e575538c17995f9f16990a5d51" 1245 | dependencies: 1246 | end-of-stream "^1.1.0" 1247 | once "^1.3.1" 1248 | 1249 | pumpify@^1.3.5: 1250 | version "1.3.5" 1251 | resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.3.5.tgz#1b671c619940abcaeac0ad0e3a3c164be760993b" 1252 | dependencies: 1253 | duplexify "^3.1.2" 1254 | inherits "^2.0.1" 1255 | pump "^1.0.0" 1256 | 1257 | read-pkg-up@^1.0.1: 1258 | version "1.0.1" 1259 | resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" 1260 | dependencies: 1261 | find-up "^1.0.0" 1262 | read-pkg "^1.0.0" 1263 | 1264 | read-pkg@^1.0.0: 1265 | version "1.1.0" 1266 | resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" 1267 | dependencies: 1268 | load-json-file "^1.0.0" 1269 | normalize-package-data "^2.3.2" 1270 | path-type "^1.0.0" 1271 | 1272 | "readable-stream@> 1.0.0 < 3.0.0", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.5, readable-stream@^2.1.5, readable-stream@^2.2.0, readable-stream@^2.2.11, readable-stream@^2.2.2: 1273 | version "2.3.3" 1274 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" 1275 | dependencies: 1276 | core-util-is "~1.0.0" 1277 | inherits "~2.0.3" 1278 | isarray "~1.0.0" 1279 | process-nextick-args "~1.0.6" 1280 | safe-buffer "~5.1.1" 1281 | string_decoder "~1.0.3" 1282 | util-deprecate "~1.0.1" 1283 | 1284 | readable-stream@~1.1.9: 1285 | version "1.1.14" 1286 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" 1287 | dependencies: 1288 | core-util-is "~1.0.0" 1289 | inherits "~2.0.1" 1290 | isarray "0.0.1" 1291 | string_decoder "~0.10.x" 1292 | 1293 | readable-stream@~2.0.0: 1294 | version "2.0.6" 1295 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" 1296 | dependencies: 1297 | core-util-is "~1.0.0" 1298 | inherits "~2.0.1" 1299 | isarray "~1.0.0" 1300 | process-nextick-args "~1.0.6" 1301 | string_decoder "~0.10.x" 1302 | util-deprecate "~1.0.1" 1303 | 1304 | redent@^1.0.0: 1305 | version "1.0.0" 1306 | resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" 1307 | dependencies: 1308 | indent-string "^2.1.0" 1309 | strip-indent "^1.0.1" 1310 | 1311 | regenerator-runtime@^0.10.0: 1312 | version "0.10.5" 1313 | resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658" 1314 | 1315 | reinterval@^1.1.0: 1316 | version "1.1.0" 1317 | resolved "https://registry.yarnpkg.com/reinterval/-/reinterval-1.1.0.tgz#3361ecfa3ca6c18283380dd0bb9546f390f5ece7" 1318 | 1319 | remap-istanbul@^0.9.5: 1320 | version "0.9.5" 1321 | resolved "https://registry.yarnpkg.com/remap-istanbul/-/remap-istanbul-0.9.5.tgz#a18617b1f31eec5a7dbee77538298b775606aaa8" 1322 | dependencies: 1323 | amdefine "^1.0.0" 1324 | gulp-util "3.0.7" 1325 | istanbul "0.4.5" 1326 | minimatch "^3.0.3" 1327 | source-map ">=0.5.6" 1328 | through2 "2.0.1" 1329 | 1330 | remove-trailing-separator@^1.0.1: 1331 | version "1.0.2" 1332 | resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.0.2.tgz#69b062d978727ad14dc6b56ba4ab772fd8d70511" 1333 | 1334 | repeat-string@^1.5.2: 1335 | version "1.6.1" 1336 | resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" 1337 | 1338 | repeating@^2.0.0: 1339 | version "2.0.1" 1340 | resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" 1341 | dependencies: 1342 | is-finite "^1.0.0" 1343 | 1344 | replace-ext@0.0.1: 1345 | version "0.0.1" 1346 | resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924" 1347 | 1348 | resolve@1.1.x: 1349 | version "1.1.7" 1350 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" 1351 | 1352 | resolve@^1.3.2: 1353 | version "1.3.3" 1354 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.3.tgz#655907c3469a8680dc2de3a275a8fdd69691f0e5" 1355 | dependencies: 1356 | path-parse "^1.0.5" 1357 | 1358 | right-align@^0.1.1: 1359 | version "0.1.3" 1360 | resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" 1361 | dependencies: 1362 | align-text "^0.1.1" 1363 | 1364 | rimraf@^2.6.1: 1365 | version "2.6.1" 1366 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" 1367 | dependencies: 1368 | glob "^7.0.5" 1369 | 1370 | safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.1.0, safe-buffer@~5.1.1: 1371 | version "5.1.1" 1372 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" 1373 | 1374 | safe-buffer@~5.0.1: 1375 | version "5.0.1" 1376 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" 1377 | 1378 | "semver@2 || 3 || 4 || 5", semver@^5.3.0: 1379 | version "5.3.0" 1380 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" 1381 | 1382 | signal-exit@^3.0.0: 1383 | version "3.0.2" 1384 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" 1385 | 1386 | simple-mock@^0.7.0: 1387 | version "0.7.3" 1388 | resolved "https://registry.yarnpkg.com/simple-mock/-/simple-mock-0.7.3.tgz#461c9e6f1c339cc49a7871b390676cd064388036" 1389 | 1390 | source-map@>=0.5.6, source-map@^0.5.0, source-map@^0.5.3, source-map@~0.5.1: 1391 | version "0.5.6" 1392 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" 1393 | 1394 | source-map@^0.4.4: 1395 | version "0.4.4" 1396 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" 1397 | dependencies: 1398 | amdefine ">=0.0.4" 1399 | 1400 | source-map@~0.2.0: 1401 | version "0.2.0" 1402 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d" 1403 | dependencies: 1404 | amdefine ">=0.0.4" 1405 | 1406 | sparkles@^1.0.0: 1407 | version "1.0.0" 1408 | resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.0.tgz#1acbbfb592436d10bbe8f785b7cc6f82815012c3" 1409 | 1410 | spdx-correct@~1.0.0: 1411 | version "1.0.2" 1412 | resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" 1413 | dependencies: 1414 | spdx-license-ids "^1.0.2" 1415 | 1416 | spdx-expression-parse@~1.0.0: 1417 | version "1.0.4" 1418 | resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" 1419 | 1420 | spdx-license-ids@^1.0.2: 1421 | version "1.2.2" 1422 | resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" 1423 | 1424 | split2@^2.1.1: 1425 | version "2.1.1" 1426 | resolved "https://registry.yarnpkg.com/split2/-/split2-2.1.1.tgz#7a1f551e176a90ecd3345f7246a0cfe175ef4fd0" 1427 | dependencies: 1428 | through2 "^2.0.2" 1429 | 1430 | sprintf-js@~1.0.2: 1431 | version "1.0.3" 1432 | resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" 1433 | 1434 | stream-shift@^1.0.0: 1435 | version "1.0.0" 1436 | resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" 1437 | 1438 | string_decoder@~0.10.x: 1439 | version "0.10.31" 1440 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" 1441 | 1442 | string_decoder@~1.0.3: 1443 | version "1.0.3" 1444 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" 1445 | dependencies: 1446 | safe-buffer "~5.1.0" 1447 | 1448 | strip-ansi@^3.0.0: 1449 | version "3.0.1" 1450 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" 1451 | dependencies: 1452 | ansi-regex "^2.0.0" 1453 | 1454 | strip-bom@^2.0.0: 1455 | version "2.0.0" 1456 | resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" 1457 | dependencies: 1458 | is-utf8 "^0.2.0" 1459 | 1460 | strip-indent@^1.0.1: 1461 | version "1.0.1" 1462 | resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" 1463 | dependencies: 1464 | get-stdin "^4.0.1" 1465 | 1466 | supports-color@3.1.2: 1467 | version "3.1.2" 1468 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5" 1469 | dependencies: 1470 | has-flag "^1.0.0" 1471 | 1472 | supports-color@^2.0.0: 1473 | version "2.0.0" 1474 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" 1475 | 1476 | supports-color@^3.1.0, supports-color@^3.1.2: 1477 | version "3.2.3" 1478 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" 1479 | dependencies: 1480 | has-flag "^1.0.0" 1481 | 1482 | through2-filter@^2.0.0: 1483 | version "2.0.0" 1484 | resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-2.0.0.tgz#60bc55a0dacb76085db1f9dae99ab43f83d622ec" 1485 | dependencies: 1486 | through2 "~2.0.0" 1487 | xtend "~4.0.0" 1488 | 1489 | through2@2.0.1: 1490 | version "2.0.1" 1491 | resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.1.tgz#384e75314d49f32de12eebb8136b8eb6b5d59da9" 1492 | dependencies: 1493 | readable-stream "~2.0.0" 1494 | xtend "~4.0.0" 1495 | 1496 | through2@^2.0.0, through2@^2.0.1, through2@^2.0.2, through2@~2.0.0: 1497 | version "2.0.3" 1498 | resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" 1499 | dependencies: 1500 | readable-stream "^2.1.5" 1501 | xtend "~4.0.1" 1502 | 1503 | time-stamp@^1.0.0: 1504 | version "1.1.0" 1505 | resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3" 1506 | 1507 | to-absolute-glob@^2.0.0: 1508 | version "2.0.1" 1509 | resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-2.0.1.tgz#70c375805b9e3105e899ee8dbdd6a9aa108f407b" 1510 | dependencies: 1511 | extend-shallow "^2.0.1" 1512 | is-absolute "^0.2.5" 1513 | is-negated-glob "^1.0.0" 1514 | 1515 | to-fast-properties@^1.0.1: 1516 | version "1.0.3" 1517 | resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" 1518 | 1519 | trim-newlines@^1.0.0: 1520 | version "1.0.0" 1521 | resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" 1522 | 1523 | trim-right@^1.0.1: 1524 | version "1.0.1" 1525 | resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" 1526 | 1527 | tslib@^1.7.1: 1528 | version "1.7.1" 1529 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.7.1.tgz#bc8004164691923a79fe8378bbeb3da2017538ec" 1530 | 1531 | tslint@^5.2.0: 1532 | version "5.5.0" 1533 | resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.5.0.tgz#10e8dab3e3061fa61e9442e8cee3982acf20a6aa" 1534 | dependencies: 1535 | babel-code-frame "^6.22.0" 1536 | colors "^1.1.2" 1537 | commander "^2.9.0" 1538 | diff "^3.2.0" 1539 | glob "^7.1.1" 1540 | minimatch "^3.0.4" 1541 | resolve "^1.3.2" 1542 | semver "^5.3.0" 1543 | tslib "^1.7.1" 1544 | tsutils "^2.5.1" 1545 | 1546 | tsutils@^2.5.1: 1547 | version "2.5.1" 1548 | resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.5.1.tgz#c2001390c79eec1a5ccfa7ac12d599639683e0cf" 1549 | dependencies: 1550 | tslib "^1.7.1" 1551 | 1552 | type-check@~0.3.2: 1553 | version "0.3.2" 1554 | resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" 1555 | dependencies: 1556 | prelude-ls "~1.1.2" 1557 | 1558 | type-detect@0.1.1: 1559 | version "0.1.1" 1560 | resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-0.1.1.tgz#0ba5ec2a885640e470ea4e8505971900dac58822" 1561 | 1562 | type-detect@^1.0.0: 1563 | version "1.0.0" 1564 | resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-1.0.0.tgz#762217cc06db258ec48908a1298e8b95121e8ea2" 1565 | 1566 | typedarray@^0.0.6: 1567 | version "0.0.6" 1568 | resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" 1569 | 1570 | typescript@^2.3.4: 1571 | version "2.4.1" 1572 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.1.tgz#c3ccb16ddaa0b2314de031e7e6fee89e5ba346bc" 1573 | 1574 | uglify-js@^2.6: 1575 | version "2.8.29" 1576 | resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" 1577 | dependencies: 1578 | source-map "~0.5.1" 1579 | yargs "~3.10.0" 1580 | optionalDependencies: 1581 | uglify-to-browserify "~1.0.0" 1582 | 1583 | uglify-to-browserify@~1.0.0: 1584 | version "1.0.2" 1585 | resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" 1586 | 1587 | ultron@~1.1.0: 1588 | version "1.1.0" 1589 | resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.0.tgz#b07a2e6a541a815fc6a34ccd4533baec307ca864" 1590 | 1591 | unc-path-regex@^0.1.0: 1592 | version "0.1.2" 1593 | resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" 1594 | 1595 | unique-stream@^2.0.2: 1596 | version "2.2.1" 1597 | resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-2.2.1.tgz#5aa003cfbe94c5ff866c4e7d668bb1c4dbadb369" 1598 | dependencies: 1599 | json-stable-stringify "^1.0.0" 1600 | through2-filter "^2.0.0" 1601 | 1602 | util-deprecate@~1.0.1: 1603 | version "1.0.2" 1604 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 1605 | 1606 | validate-npm-package-license@^3.0.1: 1607 | version "3.0.1" 1608 | resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" 1609 | dependencies: 1610 | spdx-correct "~1.0.0" 1611 | spdx-expression-parse "~1.0.0" 1612 | 1613 | vinyl@^0.5.0: 1614 | version "0.5.3" 1615 | resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.5.3.tgz#b0455b38fc5e0cf30d4325132e461970c2091cde" 1616 | dependencies: 1617 | clone "^1.0.0" 1618 | clone-stats "^0.0.1" 1619 | replace-ext "0.0.1" 1620 | 1621 | websocket-stream@^5.0.0: 1622 | version "5.0.0" 1623 | resolved "https://registry.yarnpkg.com/websocket-stream/-/websocket-stream-5.0.0.tgz#1d1318f0576ce20a12555372108ae9418a403634" 1624 | dependencies: 1625 | duplexify "^3.2.0" 1626 | inherits "^2.0.1" 1627 | readable-stream "^2.2.0" 1628 | safe-buffer "^5.0.1" 1629 | ws "^3.0.0" 1630 | xtend "^4.0.0" 1631 | 1632 | which@^1.1.1: 1633 | version "1.2.14" 1634 | resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5" 1635 | dependencies: 1636 | isexe "^2.0.0" 1637 | 1638 | window-size@0.1.0: 1639 | version "0.1.0" 1640 | resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" 1641 | 1642 | wordwrap@0.0.2: 1643 | version "0.0.2" 1644 | resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" 1645 | 1646 | wordwrap@^1.0.0, wordwrap@~1.0.0: 1647 | version "1.0.0" 1648 | resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" 1649 | 1650 | wordwrap@~0.0.2: 1651 | version "0.0.3" 1652 | resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" 1653 | 1654 | wrappy@1: 1655 | version "1.0.2" 1656 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 1657 | 1658 | ws@^3.0.0: 1659 | version "3.0.0" 1660 | resolved "https://registry.yarnpkg.com/ws/-/ws-3.0.0.tgz#98ddb00056c8390cb751e7788788497f99103b6c" 1661 | dependencies: 1662 | safe-buffer "~5.0.1" 1663 | ultron "~1.1.0" 1664 | 1665 | xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1: 1666 | version "4.0.1" 1667 | resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" 1668 | 1669 | yargs@~3.10.0: 1670 | version "3.10.0" 1671 | resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" 1672 | dependencies: 1673 | camelcase "^1.0.2" 1674 | cliui "^2.1.0" 1675 | decamelize "^1.0.0" 1676 | window-size "0.1.0" 1677 | --------------------------------------------------------------------------------