@molassesapp/molasses-server
Advanced tools
Comparing version 0.4.2 to 0.5.0
@@ -0,1 +1,4 @@ | ||
/** | ||
* @jest-environment jsdom | ||
*/ | ||
import { MolassesClient } from "../src" | ||
@@ -11,2 +14,4 @@ import mockAxios from "jest-mock-axios" | ||
jest.mock("eventsource") | ||
const { sources } = require("eventsourcemock") | ||
const response: { | ||
@@ -103,2 +108,8 @@ data: { features: Feature[] } | ||
} | ||
const validMessage = new MessageEvent("foo", { | ||
data: JSON.stringify(response), | ||
}) | ||
const validMessageB = new MessageEvent("foo", { | ||
data: JSON.stringify(responseB), | ||
}) | ||
@@ -174,2 +185,3 @@ describe("@molassesapp/molasses-server", () => { | ||
sendEvents: false, | ||
streaming: false, | ||
}) | ||
@@ -192,3 +204,3 @@ client | ||
}) | ||
expect(mockAxios.get).toBeCalledWith("/get-features", { | ||
expect(mockAxios.get).toBeCalledWith("/features", { | ||
headers: { Authorization: "Bearer testapikey" }, | ||
@@ -203,2 +215,3 @@ }) | ||
sendEvents: false, | ||
streaming: false, | ||
}) | ||
@@ -214,2 +227,3 @@ expect(client.isActive("FOO_TEST")).toBeFalsy() | ||
sendEvents: false, | ||
streaming: false, | ||
}) | ||
@@ -255,3 +269,4 @@ client | ||
}) | ||
expect(mockAxios.get).toBeCalledWith("/get-features", { | ||
expect(mockAxios.get).toBeCalledWith("/features", { | ||
headers: { Authorization: "Bearer testapikey" }, | ||
@@ -266,2 +281,3 @@ }) | ||
sendEvents: false, | ||
streaming: false, | ||
}) | ||
@@ -302,2 +318,5 @@ client | ||
).toBeTruthy() | ||
expect(client.isActive("NON_EXISTENT", { id: "123", params: {} })).toBeFalsy() | ||
expect(client.isActive("NON_EXISTENT", { id: "123", params: {} }, true)).toBeTruthy() | ||
done() | ||
@@ -308,3 +327,3 @@ }) | ||
}) | ||
expect(mockAxios.get).toBeCalledWith("/get-features", { | ||
expect(mockAxios.get).toBeCalledWith("/features", { | ||
headers: { Authorization: "Bearer testapikey" }, | ||
@@ -380,2 +399,3 @@ }) | ||
sendEvents: false, | ||
streaming: false, | ||
}) | ||
@@ -440,3 +460,3 @@ client | ||
}) | ||
expect(mockAxios.get).toBeCalledWith("/get-features", { | ||
expect(mockAxios.get).toBeCalledWith("/features", { | ||
headers: { Authorization: "Bearer testapikey" }, | ||
@@ -452,2 +472,3 @@ }) | ||
refreshInterval: 100, | ||
streaming: false, | ||
}) | ||
@@ -459,3 +480,4 @@ client | ||
jest.runAllTimers() | ||
expect(mockAxios.get).toBeCalledWith("/get-features", { | ||
expect(mockAxios.get).toBeCalledWith("/features", { | ||
headers: { Authorization: "Bearer testapikey", "If-None-Match": "yo" }, | ||
@@ -468,3 +490,3 @@ }) | ||
}) | ||
expect(mockAxios.get).toBeCalledWith("/get-features", { | ||
expect(mockAxios.get).toBeCalledWith("/features", { | ||
headers: { Authorization: "Bearer testapikey" }, | ||
@@ -480,2 +502,3 @@ }) | ||
refreshInterval: 100, | ||
streaming: false, | ||
}) | ||
@@ -485,3 +508,3 @@ client.init().catch((reason) => { | ||
}) | ||
expect(mockAxios.get).toBeCalledWith("/get-features", { | ||
expect(mockAxios.get).toBeCalledWith("/features", { | ||
headers: { Authorization: "Bearer testapikey" }, | ||
@@ -542,2 +565,3 @@ }) | ||
sendEvents: true, | ||
streaming: false, | ||
}) | ||
@@ -578,5 +602,14 @@ client.init().then(() => { | ||
) | ||
client.experimentSuccess("NON_EXISTENT", null, { | ||
id: "123", | ||
params: { isScaredUser: "true" }, | ||
}) | ||
client.experimentSuccess("NON_EXISTENT", null, { | ||
id: "123", | ||
params: { isScaredUser: "true" }, | ||
}) | ||
done() | ||
}) | ||
expect(mockAxios.get).toBeCalledWith("/get-features", { | ||
expect(mockAxios.get).toBeCalledWith("/features", { | ||
headers: { Authorization: "Bearer testapikey" }, | ||
@@ -587,2 +620,29 @@ }) | ||
}) | ||
it("should handle streaming", (done) => { | ||
const client = new MolassesClient({ | ||
APIKey: "testapikey", | ||
sendEvents: false, | ||
streaming: true, | ||
}) | ||
client | ||
.init() | ||
.then(() => { | ||
console.log("hi james") | ||
done() | ||
}) | ||
.catch((err) => { | ||
console.error(err) | ||
done() | ||
}) | ||
sources["https://sdk.molasses.app/v1/event-stream"].emitOpen() | ||
sources["https://sdk.molasses.app/v1/event-stream"].emitMessage(validMessage) | ||
const err = new Error("unauthorized") as any | ||
err.status = 401 | ||
sources["https://sdk.molasses.app/v1/event-stream"].emitError(err) | ||
const othererror = new Error("i'm done dude") as any | ||
othererror.status = 503 | ||
sources["https://sdk.molasses.app/v1/event-stream"].emitError(othererror) | ||
}) | ||
}) |
@@ -6,2 +6,14 @@ # Change Log | ||
# [0.5.0](https://github.com/molassesapp/molasses-node/compare/v0.4.2...v0.5.0) (2021-01-15) | ||
### Bug Fixes | ||
* fix tests and add better default handling ([958706d](https://github.com/molassesapp/molasses-node/commit/958706d143479a789da3993b19d29757687f05c9)) | ||
* fix tests and broken urls ([37d6a9f](https://github.com/molassesapp/molasses-node/commit/37d6a9f63362cabcecac1f669a537939db0da54c)) | ||
## [0.4.2](https://github.com/molassesapp/molasses-node/compare/v0.4.1...v0.4.2) (2020-09-27) | ||
@@ -8,0 +20,0 @@ |
@@ -0,1 +1,2 @@ | ||
import { AxiosResponse } from "axios"; | ||
import { User } from "@molassesapp/common"; | ||
@@ -12,3 +13,6 @@ /** Options for the `MolassesClient` - APIKey is required */ | ||
sendEvents?: boolean; | ||
/** Whether to use the streaming api or the base url */ | ||
streaming?: boolean; | ||
refreshInterval?: number; | ||
maxDelay?: number; | ||
}; | ||
@@ -22,2 +26,5 @@ export declare class MolassesClient { | ||
private timer; | ||
private retryCount; | ||
private logger; | ||
private eventStream; | ||
/** | ||
@@ -28,6 +35,8 @@ * Creates a new MolassesClient. | ||
constructor(options: Options); | ||
scheduleReconnect(): void; | ||
setupEventSource(): Promise<unknown>; | ||
/** | ||
* `init` - Initializes the feature flags by fetching them from the Molasses Server | ||
* */ | ||
init(): Promise<boolean>; | ||
init(): Promise<unknown>; | ||
private timedFetch; | ||
@@ -43,3 +52,3 @@ /** Stops any polling by the molasses client */ | ||
*/ | ||
isActive(key: string, user?: User): boolean; | ||
isActive(key: string, user?: User, defaultValue?: boolean): boolean; | ||
/** | ||
@@ -53,5 +62,5 @@ * Sends a success event when a user completes the goal of an A/B test. This can include additional metadata. | ||
[key: string]: string; | ||
}, user: User): boolean; | ||
}, user: User): false | Promise<void | AxiosResponse<any>>; | ||
private uploadEvent; | ||
private fetchFeatures; | ||
} |
@@ -17,2 +17,4 @@ "use strict"; | ||
var common_1 = require("@molassesapp/common"); | ||
var EventSource = require("eventsource"); | ||
var winston = require("winston"); | ||
var MolassesClient = /** @class */ (function () { | ||
@@ -26,6 +28,8 @@ /** | ||
APIKey: "", | ||
URL: "https://us-central1-molasses-36bff.cloudfunctions.net", | ||
URL: "https://sdk.molasses.app/v1", | ||
debug: false, | ||
sendEvents: true, | ||
streaming: true, | ||
refreshInterval: 15000, | ||
maxDelay: 64000, | ||
}; | ||
@@ -35,2 +39,3 @@ this.featuresCache = {}; | ||
this.etag = ""; | ||
this.retryCount = 0; | ||
this.options = __assign(__assign({}, this.options), options); | ||
@@ -40,2 +45,13 @@ if (this.options.APIKey == "") { | ||
} | ||
this.logger = winston.createLogger({ | ||
level: this.options.debug ? "debug" : "info", | ||
transports: [ | ||
new winston.transports.Console({ | ||
format: winston.format.combine(winston.format(function (info) { | ||
info.message = "[Molasses] " + (info.message ? info.message : ""); | ||
return info; | ||
})(), winston.format.timestamp(), winston.format.simple()), | ||
}), | ||
], | ||
}); | ||
this.axios = axios_1.default.create({ | ||
@@ -48,2 +64,63 @@ validateStatus: function (status) { | ||
} | ||
MolassesClient.prototype.scheduleReconnect = function () { | ||
var _this = this; | ||
var scheduledTime = 1000 * this.retryCount * 2; | ||
if (scheduledTime === 0) { | ||
scheduledTime = 1000; | ||
} | ||
else if (scheduledTime >= 64000) { | ||
scheduledTime = 64000; | ||
} | ||
scheduledTime = scheduledTime - Math.trunc(Math.random() * 0.3 * scheduledTime); | ||
this.retryCount = this.retryCount + 1; | ||
setTimeout(function () { | ||
_this.logger.info("reconnecting - retry count:" + _this.retryCount); | ||
_this.setupEventSource(); | ||
}, scheduledTime); | ||
}; | ||
MolassesClient.prototype.setupEventSource = function () { | ||
var _this = this; | ||
return new Promise(function (resolve, reject) { | ||
var headers = { Authorization: "Bearer " + _this.options.APIKey }; | ||
_this.eventStream = new EventSource(_this.options.URL + "/event-stream", { | ||
headers: headers, | ||
}); | ||
_this.eventStream.onmessage = function (e) { | ||
var _a; | ||
try { | ||
var result = JSON.parse(e.data); | ||
if ((_a = result.data) === null || _a === void 0 ? void 0 : _a.features) { | ||
_this.logger.info("received feature update"); | ||
var jsonData = result.data.features; | ||
_this.featuresCache = jsonData.reduce(function (acc, value) { | ||
acc[value.key] = value; | ||
return acc; | ||
}, {}); | ||
_this.initiated = true; | ||
_this.retryCount = 0; | ||
resolve(void 0); | ||
} | ||
else { | ||
reject("Molasses - invalid message format"); | ||
} | ||
} | ||
catch (error) { | ||
reject("Molasses - invalid message format"); | ||
} | ||
}; | ||
_this.eventStream.onerror = function (err) { | ||
if (err.status === 401 || err.status === 403) { | ||
_this.logger.error("not authorized! failed to connect"); | ||
_this.eventStream.close(); | ||
reject("Molasses not authorized! failed to connect"); | ||
} | ||
else { | ||
_this.eventStream.close(); | ||
_this.scheduleReconnect(); | ||
_this.logger.error("ERROR - " + err.message); | ||
reject("Molasses - ERROR - " + err.message); | ||
} | ||
}; | ||
}); | ||
}; | ||
/** | ||
@@ -53,2 +130,6 @@ * `init` - Initializes the feature flags by fetching them from the Molasses Server | ||
MolassesClient.prototype.init = function () { | ||
this.logger.info("Starting Molasses"); | ||
if (this.options.streaming) { | ||
return this.setupEventSource(); | ||
} | ||
return this.fetchFeatures(); | ||
@@ -62,3 +143,8 @@ }; | ||
MolassesClient.prototype.stop = function () { | ||
clearTimeout(this.timer); | ||
if (this.options.streaming) { | ||
this.eventStream.close(); | ||
} | ||
else { | ||
clearTimeout(this.timer); | ||
} | ||
}; | ||
@@ -72,3 +158,5 @@ /** | ||
*/ | ||
MolassesClient.prototype.isActive = function (key, user) { | ||
MolassesClient.prototype.isActive = function (key, user, defaultValue) { | ||
var _this = this; | ||
if (defaultValue === void 0) { defaultValue = false; } | ||
if (!this.initiated) { | ||
@@ -78,2 +166,6 @@ return false; | ||
var feature = this.featuresCache[key]; | ||
if (!feature) { | ||
this.logger.warn("Molasses - feature " + key + " doesn't exist in your environment"); | ||
return defaultValue; | ||
} | ||
var result = common_1.isActive(feature, user); | ||
@@ -88,2 +180,4 @@ if (user && this.options.sendEvents) { | ||
testType: result ? "experiment" : "control", | ||
}).catch(function () { | ||
_this.logger.error("failed to upload experiment started"); | ||
}); | ||
@@ -100,2 +194,3 @@ } | ||
MolassesClient.prototype.experimentSuccess = function (key, additionalDetails, user) { | ||
var _this = this; | ||
if (!this.initiated || !this.options.sendEvents || !user || !user.id) { | ||
@@ -105,4 +200,8 @@ return false; | ||
var feature = this.featuresCache[key]; | ||
if (!feature) { | ||
this.logger.warn("Molasses - feature " + key + " doesn't exist in your environment"); | ||
return false; | ||
} | ||
var result = common_1.isActive(feature, user); | ||
this.uploadEvent({ | ||
return this.uploadEvent({ | ||
event: "experiment_success", | ||
@@ -114,2 +213,4 @@ tags: __assign(__assign({}, user.params), additionalDetails), | ||
testType: result ? "experiment" : "control", | ||
}).catch(function () { | ||
_this.logger.error("failed to upload experiment success"); | ||
}); | ||
@@ -120,3 +221,3 @@ }; | ||
var data = __assign(__assign({}, eventOptions), { tags: JSON.stringify(eventOptions.tags) }); | ||
this.axios.post("/analytics", data, { | ||
return this.axios.post("/analytics", data, { | ||
headers: headers, | ||
@@ -132,3 +233,3 @@ }); | ||
return this.axios | ||
.get("/get-features", { | ||
.get("/features", { | ||
headers: headers, | ||
@@ -147,2 +248,3 @@ }) | ||
}, {}); | ||
_this.logger.info("received feature update"); | ||
_this.etag = response.headers["etag"]; | ||
@@ -154,4 +256,7 @@ _this.initiated = true; | ||
.catch(function (err) { | ||
if (!_this.initiated) { | ||
throw new Error("Molasses - " + err.message); | ||
} | ||
_this.logger.error(err.message); | ||
_this.timedFetch(); | ||
throw new Error("Molasses - " + err.message); | ||
}); | ||
@@ -158,0 +263,0 @@ }; |
@@ -10,2 +10,3 @@ // Jest configuration for api | ||
rootDir: "../..", | ||
setupFiles: ["./packages/molasses-server/__tests__/setupFiles"], | ||
} |
{ | ||
"name": "@molassesapp/molasses-server", | ||
"version": "0.4.2", | ||
"version": "0.5.0", | ||
"description": "A Node (with TypeScript support) SDK for Molasses. It allows you to evaluate user's status for a feature. It also helps simplify logging events for A/B testing.", | ||
@@ -20,4 +20,6 @@ "main": "dist/index.js", | ||
"dependencies": { | ||
"@molassesapp/common": "^0.4.2", | ||
"axios": "^0.20.0" | ||
"@molassesapp/common": "^0.5.0", | ||
"axios": "^0.21.1", | ||
"eventsource": "^1.0.7", | ||
"winston": "^3.3.3" | ||
}, | ||
@@ -27,3 +29,3 @@ "publishConfig": { | ||
}, | ||
"gitHead": "8cbee03d9b42cdabf928610b2c23abb7289f7cf5" | ||
"gitHead": "299cb8a95e16dfbbe1ca8f2ea7ede7b7b91dca7a" | ||
} |
@@ -8,5 +8,5 @@ <p align="center"> | ||
It includes the Node (with TypeScript support) SDK for Molasses. It allows you to evaluate user's status for a feature. It also helps simplify logging events for A/B testing. | ||
`Molasses-server` includes the Node (with TypeScript support) SDK for Molasses. It allows you to evaluate a user's status for a feature. It also helps simplify logging events for A/B testing. | ||
`Molasses-server` uses polling to check if you have updated features. Once initialized, it takes microseconds to evaluate if a user is active. | ||
The SDK uses SSE to communicate with the Molasses Application. Once initialized, it takes microseconds to evaluate if a user is active. When you update a feature on Molasses, it sends that update to all of your clients through SSE and users would start experiencing that change instantly. | ||
@@ -13,0 +13,0 @@ ## Install |
124
src/index.ts
import axios, { AxiosInstance, AxiosResponse } from "axios" | ||
import { Feature, User, isActive } from "@molassesapp/common" | ||
const EventSource = require("eventsource") | ||
const winston = require("winston") | ||
/** Options for the `MolassesClient` - APIKey is required */ | ||
@@ -14,4 +15,6 @@ export type Options = { | ||
sendEvents?: boolean | ||
/** Whether to use the streaming api or the base url */ | ||
streaming?: boolean | ||
refreshInterval?: number | ||
maxDelay?: number | ||
} | ||
@@ -31,6 +34,8 @@ | ||
APIKey: "", | ||
URL: "https://us-central1-molasses-36bff.cloudfunctions.net", | ||
URL: "https://sdk.molasses.app/v1", | ||
debug: false, | ||
sendEvents: true, | ||
streaming: true, | ||
refreshInterval: 15000, | ||
maxDelay: 64000, | ||
} | ||
@@ -46,2 +51,5 @@ | ||
private timer: NodeJS.Timer | undefined | ||
private retryCount = 0 | ||
private logger: any | ||
private eventStream: any | ||
/** | ||
@@ -56,2 +64,17 @@ * Creates a new MolassesClient. | ||
} | ||
this.logger = winston.createLogger({ | ||
level: this.options.debug ? "debug" : "info", | ||
transports: [ | ||
new winston.transports.Console({ | ||
format: winston.format.combine( | ||
winston.format((info) => { | ||
info.message = `[Molasses] ${info.message ? info.message : ""}` | ||
return info | ||
})(), | ||
winston.format.timestamp(), | ||
winston.format.simple(), | ||
), | ||
}), | ||
], | ||
}) | ||
this.axios = axios.create({ | ||
@@ -65,2 +88,61 @@ validateStatus: function (status) { | ||
scheduleReconnect() { | ||
let scheduledTime = 1000 * this.retryCount * 2 | ||
if (scheduledTime === 0) { | ||
scheduledTime = 1000 | ||
} else if (scheduledTime >= 64000) { | ||
scheduledTime = 64000 | ||
} | ||
scheduledTime = scheduledTime - Math.trunc(Math.random() * 0.3 * scheduledTime) | ||
this.retryCount = this.retryCount + 1 | ||
setTimeout(() => { | ||
this.logger.info("reconnecting - retry count:" + this.retryCount) | ||
this.setupEventSource() | ||
}, scheduledTime) | ||
} | ||
setupEventSource() { | ||
return new Promise((resolve, reject) => { | ||
const headers = { Authorization: "Bearer " + this.options.APIKey } | ||
this.eventStream = new EventSource(this.options.URL + "/event-stream", { | ||
headers, | ||
} as any) | ||
this.eventStream.onmessage = (e) => { | ||
try { | ||
const result = JSON.parse(e.data) | ||
if (result.data?.features) { | ||
this.logger.info("received feature update") | ||
const jsonData: Feature[] = result.data.features | ||
this.featuresCache = jsonData.reduce<{ [key: string]: Feature }>( | ||
(acc, value: Feature) => { | ||
acc[value.key] = value | ||
return acc | ||
}, | ||
{}, | ||
) | ||
this.initiated = true | ||
this.retryCount = 0 | ||
resolve(void 0) | ||
} else { | ||
reject("Molasses - invalid message format") | ||
} | ||
} catch (error) { | ||
reject("Molasses - invalid message format") | ||
} | ||
} | ||
this.eventStream.onerror = (err: any) => { | ||
if (err.status === 401 || err.status === 403) { | ||
this.logger.error("not authorized! failed to connect") | ||
this.eventStream.close() | ||
reject("Molasses not authorized! failed to connect") | ||
} else { | ||
this.eventStream.close() | ||
this.scheduleReconnect() | ||
this.logger.error("ERROR - " + err.message) | ||
reject("Molasses - ERROR - " + err.message) | ||
} | ||
} | ||
}) | ||
} | ||
/** | ||
@@ -70,2 +152,6 @@ * `init` - Initializes the feature flags by fetching them from the Molasses Server | ||
init() { | ||
this.logger.info("Starting Molasses") | ||
if (this.options.streaming) { | ||
return this.setupEventSource() | ||
} | ||
return this.fetchFeatures() | ||
@@ -80,3 +166,7 @@ } | ||
stop() { | ||
clearTimeout(this.timer) | ||
if (this.options.streaming) { | ||
this.eventStream.close() | ||
} else { | ||
clearTimeout(this.timer) | ||
} | ||
} | ||
@@ -91,3 +181,3 @@ | ||
*/ | ||
isActive(key: string, user?: User) { | ||
isActive(key: string, user?: User, defaultValue = false) { | ||
if (!this.initiated) { | ||
@@ -98,2 +188,6 @@ return false | ||
const feature = this.featuresCache[key] | ||
if (!feature) { | ||
this.logger.warn(`Molasses - feature ${key} doesn't exist in your environment`) | ||
return defaultValue | ||
} | ||
const result = isActive(feature, user) | ||
@@ -108,2 +202,4 @@ if (user && this.options.sendEvents) { | ||
testType: result ? "experiment" : "control", | ||
}).catch(() => { | ||
this.logger.error("failed to upload experiment started") | ||
}) | ||
@@ -126,4 +222,8 @@ } | ||
const feature = this.featuresCache[key] | ||
if (!feature) { | ||
this.logger.warn(`Molasses - feature ${key} doesn't exist in your environment`) | ||
return false | ||
} | ||
const result = isActive(feature, user) | ||
this.uploadEvent({ | ||
return this.uploadEvent({ | ||
event: "experiment_success", | ||
@@ -138,2 +238,4 @@ tags: { | ||
testType: result ? "experiment" : "control", | ||
}).catch(() => { | ||
this.logger.error("failed to upload experiment success") | ||
}) | ||
@@ -148,3 +250,3 @@ } | ||
} | ||
this.axios.post("/analytics", data, { | ||
return this.axios.post("/analytics", data, { | ||
headers, | ||
@@ -160,3 +262,3 @@ }) | ||
return this.axios | ||
.get("/get-features", { | ||
.get("/features", { | ||
headers, | ||
@@ -179,2 +281,3 @@ }) | ||
) | ||
this.logger.info("received feature update") | ||
this.etag = response.headers["etag"] | ||
@@ -186,6 +289,9 @@ this.initiated = true | ||
.catch((err: Error) => { | ||
if (!this.initiated) { | ||
throw new Error("Molasses - " + err.message) | ||
} | ||
this.logger.error(err.message) | ||
this.timedFetch() | ||
throw new Error("Molasses - " + err.message) | ||
}) | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
55538
14
1194
4
+ Addedeventsource@^1.0.7
+ Addedwinston@^3.3.3
+ Added@colors/colors@1.6.0(transitive)
+ Added@dabh/diagnostics@2.0.3(transitive)
+ Added@molassesapp/common@0.5.0(transitive)
+ Added@types/triple-beam@1.3.5(transitive)
+ Addedasync@3.2.6(transitive)
+ Addedaxios@0.21.4(transitive)
+ Addedcolor@3.2.1(transitive)
+ Addedcolor-convert@1.9.3(transitive)
+ Addedcolor-name@1.1.3(transitive)
+ Addedcolor-string@1.9.1(transitive)
+ Addedcolorspace@1.1.4(transitive)
+ Addedenabled@2.0.0(transitive)
+ Addedeventsource@1.1.2(transitive)
+ Addedfecha@4.2.3(transitive)
+ Addedfn.name@1.1.0(transitive)
+ Addedinherits@2.0.4(transitive)
+ Addedis-arrayish@0.3.2(transitive)
+ Addedis-stream@2.0.1(transitive)
+ Addedkuler@2.0.0(transitive)
+ Addedlogform@2.7.0(transitive)
+ Addedms@2.1.3(transitive)
+ Addedone-time@1.0.0(transitive)
+ Addedreadable-stream@3.6.2(transitive)
+ Addedsafe-buffer@5.2.1(transitive)
+ Addedsafe-stable-stringify@2.5.0(transitive)
+ Addedsimple-swizzle@0.2.2(transitive)
+ Addedstack-trace@0.0.10(transitive)
+ Addedstring_decoder@1.3.0(transitive)
+ Addedtext-hex@1.0.0(transitive)
+ Addedtriple-beam@1.4.1(transitive)
+ Addedutil-deprecate@1.0.2(transitive)
+ Addedwinston@3.17.0(transitive)
+ Addedwinston-transport@4.9.0(transitive)
- Removed@molassesapp/common@0.4.2(transitive)
- Removedaxios@0.20.0(transitive)
Updated@molassesapp/common@^0.5.0
Updatedaxios@^0.21.1