Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@molassesapp/molasses-server

Package Overview
Dependencies
Maintainers
1
Versions
13
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@molassesapp/molasses-server - npm Package Compare versions

Comparing version 0.4.2 to 0.5.0

__tests__/setupFiles.js

76

__tests__/index.test.ts

@@ -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 @@

15

dist/index.d.ts

@@ -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

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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc