So I'm creating a singleton class and when I require it from my server.js file it works fine, but when I require it from another file it returns as undefined. I'll try to post relevant code but some will have to be cut out due to work.
server.js
const express = require('express');
const bodyParser = require('body-parser');
const path = require('path');
const http = require('http');
const app = express();
const config = require('config');
const FBConfigsListener = require('./server/amq_listeners/fb_configs.listener');
const FBConfigs = require('./server/models/FBConfigs');
//Api file for interacting with mongodb
const api = require('./server/routes/api.routes');
//Parsers
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
//Angular Dist output folder
app.use(express.static(path.join(__dirname, 'dist')));
//Api location
app.use('/api', api);
//Send all other requests to angular
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'dist/index.html'));
});
//set port
var port = config.get('webserver.port');
app.set('port', port);
const server = http.createServer(app);
server.listen(port, () => console.log(`Running on localhost:${port}`));
models/FBConfigs.js
var ConfigModel = require('./config');
var config = require('config');
var _ = require('lodash');
var FBConfigsListener = require('../amq_listeners/fb_configs.listener');
var AMQAdapter = require('../adapters/amq.adapter');
var uniqid = require('uniqid');
const connectionOptions = config.get('activemq.connectionOptions');
class FBConfigs {
constructor() {
console.log(config.get('environments'));
this.listener = FBConfigsListener;
this.configs = {};
this.unique_keys = ['id'];
this.update_topic = '/topic/fusebuilder.update.config.';
console.log(FBConfigsListener);
//AMQ Client
this.amq_client = AMQAdapter.getInstance(connectionOptions.host, connectionOptions.port);
}
add(key, config) {
if (!(key in this.configs)) {
this.configs[key] = new ConfigModel(this.unique_keys);
}
this.configs[key].add(config);
}
get(key) {
let configs_json = {};
if (key) {
configs_json[key] = JSON.parse(this.configs[key].toString());
} else {
for (let key in this.configs) {
configs_json[key] = JSON.parse(this.configs[key].toString());
}
}
return configs_json;
}
updateByID(key, id, input_config) {
let configs = this.configs[key].get();
for (let config of configs) {
if (input_config.id === config.id) {
this.update(key, _.merge(config, input_config));
}
}
}
//Send update to config topic
update(key, config) {
let topic = this.update_topic + key;
var update_object = {};
if (Array.isArray(config)) {
update_object[key] = [...config];
} else {
update_object[key] = [config];
}
console.log(`Sending ${key} update:${JSON.stringify(update_object)}`);
this.amq_client.sendMessage(topic, update_object);
}
copyTo(key, id, env) {
let selected_env = config.get('environments.' + env);
// let tmp_amq_client = new AMQAdapter(selected_env.host, selected_env.port);
let selected_config = this.configs[key].getByID(id);
console.log(this);
if (key === 'fuses') {
console.log('In FBConfig Copy to for fuses');
const get_fuse_topic = '/topic/fusebuilder.get_fuse';
const tran_id = uniqid();
const sendObj = { fuseName: id, tran_id };
this.amq_client.sendMessage(get_fuse_topic, sendObj);
var startTime = process.hrtime()[0];
var timeout = false;
while (!this.listener.get_copy_fuse_data(tran_id)) {
console.log('Waiting for config');
sleep(100);
if (process.hrtime()[0] - startTime > 3) {
console.log('Timed out');
timeout = true;
break;
}
}
console.log(JSON.stringify(FBConfigsListener.get_copy_fuse_data(tran_id)));
} else {
tmp_amq_client.sendMessage(this.update_topic, selected_config);
}
console.log(`Copy ${key} id:${id} to ${env}`);
}
}
module.exports = new FBConfigs();
amq_listener/fb_configs.listener.js
const config = require('config');
var AMQAdapter = require('../adapters/amq.adapter');
var FBConfigs = require('../models/FBConfigs');
**removed for work**
class FBConfigsListener {
constructor() {
this.instance;
this.copy_fuse_data = {};
//AMQ Client
this.amq_client = AMQAdapter.getInstance(connectionOptions.host, connectionOptions.port);
//Subscribe to configs
this.amq_client.subscribe(config_subscribe_topic, this.config_topic_callback.bind(this));
//Request Tables
this.amq_client.sendMessage(config_request_topic, { tables: config_tables });
//Subscribe to Copy Fuse topic
this.amq_client.subscribe(subscribe_fuse_copy_topic, this.copy_fuse_callback.bind(this));
}
config_topic_callback(err, message) {
let dest = this.amq_client.getDestination(message);
let key = this.get_key_from_topic(dest);
this.amq_client.readMessage(message, body => {
let configs = JSON.parse(body);
if (key in configs) {
for (let config of configs[key]) {
FBConfigs.add(key, config);
}
}
});
}
copy_fuse_callback(err, message) {
this.amq_client.readMessage(message, body => {
const config = JSON.parse(body);
this.copy_fuse_data[config.tran_id] = config;
});
}
//Get Key from the topic and convert using key map if needed
get_key_from_topic(topic) {
let key = topic.split('.')[topic.split('.').length - 1];
key = key in key_map ? key_map[key] : key;
return key;
}
get_copy_fuse_data(id) {
if (id in this.copy_fuse_data) {
return this.copy_fuse_data[id];
} else {
return false;
}
}
}
module.exports = new FBConfigsListener();
Error happens in FBConfigs. FBConfigsListener returns {} so all functions in there are undefined. Even if I do console.log(require('../amq_listeners/fb_configs.listener')) it prints {} But doing the same thing in server.js (with updated path) it prints the module.
Also tips on how to improve my coding style would be appreciated too.
Edit
So I found out that I have a circular dependency between these classes. How can this be fixed while allowing me to call one from the other.
I would suggest you to instantiate your dependencies firstly and store them in some object which you can pass then to your dependent classes. The structure can be
factories/services.js
/*
* Instantiates passed services and passes injector object to them
*/
module.exports = function createServices(injector, services) {
return Object.entries(services)
.reduce((aggregator, [name, serv]) => {
const name_ = camelCase(name);
aggregator.set(name_, new serv(injector));
return aggregator;
}, new Map());
};
lib/service.js
/**
* Base class for classes need any injections
*/
module.exports = class Service {
constructor(injector) {
this.injector = injector;
}
get dependencies() {
return this.injector.dependencies;
}
/*
* Background jobs can be ran here
*/
async startService() {}
/*
* Background jobs can be stopped here
*/
async stopService() {}
};
lib/injector.js
const Service = require('./service');
/*
* Contains all dependencies
*/
module.exports = class Injector {
constructor() {
this.services = new Map();
this._dependencies = {};
}
has(name) {
return this.services.has(name);
}
register(name, service) {
if (this.has(name)) {
throw new Error(`Service ${name} already exists`);
}
if (service instanceof Service === false) {
throw new Error('Argument #2 should be an instance of Service');
}
this.services.set(name, service);
this._dependencies[name] = service;
}
unregister(name) {
if (! this.has(name)) {
throw new Error(`Service ${name} not found`);
}
this.services.delete(name);
delete this._dependencies[name];
}
get dependencies() {
return { ...this._dependencies };
}
/*
* Starts all registered services
*/
async start() {
for (let service of this.services.values()) {
await service.startService();
}
}
/*
* Stops all registered services
*/
async stop() {
for (let service of this.services.values()) {
await service.stopService();
}
}
};
Then import, initialize and bind your services in the main file (don't forget to export just a class, not an object like you do it now).
server.js
const createServices = require('./factories/services.js');
const injector = require('./lib/injector');
const Injector = new injector();
const services = createServices(Injector, [require('./server/amq_listeners/fb_configs.listener'), require('./server/models/FBConfigs')]);
services.forEach((service, name) => {
Injector.register(name, service);
});
// Start services
Injector.start();
Inherit required classes to Service class and you will get an access to all dependencies there (don't forget to call super() from constructor). Like
models/FBConfigs.js
const Service = require('../lib/service');
class FBConfigs extends Service {
constructor(injector) {
super(injector);
const { FBConfigsListener } = this.dependencies;
...your code here
}
async startService() {
...run bg job or init some connection
}
async stopService() {
...stop bg job or close some connection
}
}
module.exports = FBConfigs;
Also you can pass some config object to createServices (I didn't include it here) with keys equal to service names and values containing config object and pass config to appropriate service.
It is caused by that circular dependency. You should avoid it or used very carefully.
In your case the fix is probably pretty simple, move the line var FBConfigs = require('../models/FBConfigs'); from listener at the end of the file as the last line (yes, even after the module.exports).
Edit: Actually it maybe is not enough as I checked the code more in detail. As you are not using Listener in FBConfig constructor, you can create method assignListener, remove this.listener from that constructor and call it later in server.js which will do the this.listener
Or the last solution, which is also "best practice". Do not export the instances. Export the classes only. Then in server.js create these instances after both are required.
Related
I have several JavaScript files that I create enums. for example:
source.enum.js
const enumUtils = require('../enum.utils');
const EmailAddressesSourceType = enumUtils.createEnum([
['DIRECTORY', 'directory'],
['FILE', 'file'],
['ARRAY', 'array']
]);
module.exports = { EmailAddressesSourceType };
The enum.utils.js is just a file that do the simple function of creating an enum from array:
class EnumUtils {
constructor() { }
// This method takes a map of elements and converts them to freeze objects (an enum-like object).
createEnum(mapItems) {
if (!mapItems || mapItems.length <= 0) {
throw new Error(`No array received: ${mapItems} (1000000)`);
}
const mapList = new Map([...mapItems]);
const symbolMap = {};
mapList.forEach((value, key) => { symbolMap[key] = value; });
return Object.freeze(symbolMap);
}
}
const enumUtils = new EnumUtils();
module.exports = enumUtils;
Now since I have 5-6 js files with enums, I want to avoid 'const enumUtils = require('../enum.utils');' in each of them, and do it all together in index.js file, something like this:
const { EmailAddressStatus, EmailAddressType, SendEmailStepName } = require('./files/emailAddress.enum');
const { Placeholder } = require('./files/placeholder.enum');
const { EmailAddressesSourceType } = require('./files/sources.enum');
const { Mode, Status, Method } = require('./files/system.enum');
const { StatusIcon, Color, ColorCode } = require('./files/text.enum');
const createEnum = (mapItems) => {
if (!mapItems || mapItems.length <= 0) {
throw new Error(`No array received: ${mapItems} (1000000)`);
}
const mapList = new Map([...mapItems]);
const symbolMap = {};
mapList.forEach((value, key) => { symbolMap[key] = value; });
return Object.freeze(symbolMap);
};
module.exports = {
createEnum(Color), createEnum(ColorCode), createEnum(EmailAddressStatus), createEnum(EmailAddressType), createEnum(EmailAddressesSourceType),
createEnum(Method), createEnum(Mode), createEnum(Placeholder), createEnum(SendEmailStepName), createEnum(Status), createEnum(StatusIcon)
};
But, there are compilation error in:
module.exports = {
createEnum(Color), createEnum(ColorCode), createEnum(EmailAddressStatus), createEnum(EmailAddressType), createEnum(EmailAddressesSourceType),
createEnum(Method), createEnum(Mode), createEnum(Placeholder), createEnum(SendEmailStepName), createEnum(Status), createEnum(StatusIcon)
};
My question is, there is a workaround so enable me to reduce the 'const enumUtils = require('../enum.utils');' in each file of the enums js file?
Thanks!
UPDATE 1
The error I'm getting is this:
The current status of the file (before I was trying to refactor) - It works OK:
index.js
const { EmailAddressStatus, EmailAddressType, SendEmailStepName } = require('./files/emailAddress.enum');
const { Placeholder } = require('./files/placeholder.enum');
const { EmailAddressesSourceType } = require('./files/sources.enum');
const { Mode, Status, Method } = require('./files/system.enum');
const { StatusIcon, Color, ColorCode } = require('./files/text.enum');
module.exports = {
Color, ColorCode, EmailAddressStatus, EmailAddressType, EmailAddressesSourceType,
Method, Mode, Placeholder, SendEmailStepName, Status, StatusIcon
};
This guy, guy-incognito, solved for me the issue. Now it works like a charm. Thanks man!
const { EmailAddressStatus, EmailAddressType, SendEmailStepName } = require('./files/emailAddress.enum');
const { Placeholder } = require('./files/placeholder.enum');
const { EmailAddressesSourceType } = require('./files/sources.enum');
const { Mode, Status, Method } = require('./files/system.enum');
const { StatusIcon, Color, ColorCode } = require('./files/text.enum');
const createEnum = (mapItems) => {
if (!mapItems || mapItems.length <= 0) {
throw new Error(`No array received: ${mapItems} (1000000)`);
}
const mapList = new Map([...mapItems]);
const symbolMap = {};
mapList.forEach((value, key) => { symbolMap[key] = value; });
return Object.freeze(symbolMap);
};
module.exports = {
Color: createEnum(Color),
ColorCode: createEnum(ColorCode),
EmailAddressStatus: createEnum(EmailAddressStatus),
EmailAddressType: createEnum(EmailAddressType),
EmailAddressesSourceType: createEnum(EmailAddressesSourceType),
Method: createEnum(Method),
Mode: createEnum(Mode),
Placeholder: createEnum(Placeholder),
SendEmailStepName: createEnum(SendEmailStepName),
Status: createEnum(Status),
StatusIcon: createEnum(StatusIcon)
};
What is the best way to make socket.io to a singleton? here i have three files i need to use socket.io method in user.mjs
socket.mjs
class socketBusiness extends baseBusiness {
//io = null;
//connectedUsers = {}
constructor(io) {
super(io);
this.io = io;
this.connectedUsers = {};
this.addUserRef= {};
this.bindEvents();
}
bindEvents() {
this.io.on("connection", this.onConnection);
this.io.use(this.onBeginConnection);
}
unBindEvents() {
this.io.off("connection", this.onConnection);
}
onConnection(socket) {
let _io = this.io;
let socketId = socket.id;
socket.on("disconnect", reason => {
});
socket.on("chat message", function(msg) {
});
}
addUserRef(userId, cstId) {
let arr = this.addUserRef[cstId] || [];
if (arr.indexOf(cstId) < 0) {
arr.push(cstId);
}
this.addUserRef[userId] = arr;
}
}
export default socketBusiness;
user.mjs
const socket = require("socket.mjs)
export async function addCst(req, res) {
socket.addUserRef(req.id,req.cstId)
}
How i can access the socket.io method ? any help will be highly appreciated
www.mjs
import socket from '../socket.mjs';
var server = createServer(app);
var io = new SocketServer(server, {})
var sb = new socketBusiness(io);
Export an instance:
export default new socketBusiness;
If you export a singleton, you can't pass io in the constructor. Move the initialization logic to a method:
constructor() { }
init(io) {
this.io = io;
this.bindEvents();
}
Then, initialize the singleton when you got the socket:
var io = new SocketServer(server, {})
socket.init(io);
I'm following a tutorial which is using jest to test the javascript. The instructor created a static function called genesis() on a class called Block and it worked for him just fine, but when I tried to do it I got TypeError: block.genesis is not a function. If I remove the static keyword it recognises the function and the test passes.
Here is the class:
const { GENESIS_DATA } = require('./config');
class Block {
constructor({ timestamp, lastHash, hash, data }) {
this.timestamp = timestamp;
this.lastHash = lastHash;
this.hash = hash;
this.data = data;
}
static genesis() {
return new Block(GENESIS_DATA);
}
}
module.exports = Block;
And the test:
const Block = require('./block');
const { GENESIS_DATA } = require('./config');
describe('Block', () => {
const timestamp = 'a-date';
const lastHash = 'a-hash';
const hash = 'another-hash';
const data = ['blockchain', 'data'];
const block = new Block({ timestamp, lastHash, hash, data });
describe('genesis()', () => {
const genesisBlock = block.genesis();
it('returns a block instance', () => {
expect(genesisBlock instanceof Block).toBe(true);
});
it('returns the genesis data', () => {
expect(genesisBlock).toEqual(GENESIS_DATA);
});
});
});
The genesis method is part of the class, not the instance. You want to call Block.genesis() instead of block.genesis()
I try to add an option to my app to toggle using a cache. For cache I am using Redis and made simple wrapper for that. The problem is coming when I try to overwrite redis.get method, it's simply doens't work or cannot found this.
'use strict';
const redis = require('redis');
const config = require('../config');
const REDIS_EMPTY_VALUE = 'NOT_EXIST';
const MINUTE = 60;
let client = redis.createClient({
host: config.get('REDIS_HOST'),
port: config.get('REDIS_PORT')
});
client.on("error", function (err) {
logging.error('Redis Error: ' + err);
throw new Error(err);
});
/**
* Next are custom extensions for Redis module
*/
client.emptyValue = REDIS_EMPTY_VALUE;
client.minute = MINUTE;
client.setAndExprire = function(key, value, expire) {
this.set(key, value);
this.expire(key, expire);
};
// Here is the problem
client.get = function(key, cb) {
if (config.get('disable-cache') === 'true') return cb(null, null);
return client.get(key, cb);
}
module.exports = client;
Since you override client.get, the client.get become the new function that you defined. So that, you can call to the function that come along with redis package. You can use another object (custom) to call the redis function like below:
'use strict';
const redis = require('redis');
const config = require('../config');
const REDIS_EMPTY_VALUE = 'NOT_EXIST';
const MINUTE = 60;
const definedFunctions = [
'hgetall', 'hexists', 'hmset', 'hmget', 'hkeys', 'hvals', 'hget', 'hset', 'hdel',
'mget', 'mset', 'set', 'del', 'exists', 'lpush', 'lrange',
'zrange', 'zrem', 'zadd', 'zrangebyscore', 'zrevrangebyscore',
'expire', 'incrby'
];
let client = redis.createClient({
host: config.get('REDIS_HOST'),
port: config.get('REDIS_PORT')
});
client.on("error", function (err) {
console.error('Redis Error: ' + err);
throw new Error(err);
});
/**
* Next are custom extensions for Redis module
*/
const custom = {
emptyValue: REDIS_EMPTY_VALUE,
minute: MINUTE
};
definedFunctions.map((fn) => {
custom[fn] = (...args) => {
return client[fn](args);
};
});
custom.get = function(key, cb) {
if (config.get('disable-cache') === 'true') return cb(null, null);
return client.get(key, cb);
}
module.exports = custom;
I have created a method that grabs the image data based on another method. As you can see the uploadAvatar() method stores the object back from Cloudinary into a constructor object this._userImage;. Then my getUserImage() method should return the user image data but instead it returns {} that when I log it to the console :P.
ImageUploader.js
'use strict';
const cloudinary = require('cloudinary');
class ImageUploader {
constructor(imageObj) {
this.imageObj = imageObj;
this._apiKey = "key";
this._apiSecret = "secret";
this.config = cloudinary.config({
cloud_name: 'name',
api_key: this._apiKey,
api_secret: this._apiSecret
});
this._userImage = {}; // where user image object should be stored
}
* uploadAvatar(path) {
cloudinary.uploader.upload(path, (data) => {
this._userImage = data; // where I overwrite the constructor obj
});
}
getUserImage() {
return this._userImage; // this returns `{}` when I log it in UsersController.js
}
}
module.exports = ImageUploader;
UsersController.js
'use strict'
const Validator = require("../Helpers/ValidatorHelper.js");
const ImageUploader = require("../Helpers/ImageUploader.js");
class UsersController {
* registerView (request, response) {
yield response.sendView('users.register');
}
* register (request, response) {
const user = request.only('display_name', 'username', 'email', 'password', 'confirm_password', 'console');
var avatar = request.file('avatar');
let validator = new Validator(user, avatar);
let imageUpload = new ImageUploader(avatar);
let avatarStatus = yield validator.validateAvatar();
var img;
if (avatarStatus) { // is true
yield imageUpload.uploadAvatar(avatar.file.path);
} else { // is false
// pick a random avatar
}
console.log(imageUpload.getUserImage());
return response.status(200).json({
user: user,
avatar: imageUpload.getUserImage()
});
}
}
module.exports = UsersController
Not sure but here you could lost your this context.
cloudinary.uploader.upload(path, (data) => {
this._userImage = data; // where I overwrite the constructor obj
});
Check if 'this' there still links to your UsersController object