I would like to store some information in the socket room variables, but am getting the following error: UnhandledPromiseRejectionWarning: TypeError: Cannot set property 'host' of undefined
This is my code:
io.on('connection', (socket, next) => {
socket.on('create game', async () => {
console.log('Creating game...');
socket.username = await generateUsername();
socket.roomId = generateId();
socket.join(socket.roomId);
io.sockets.adapter.rooms[socket.roomId].host = socket.username;
io.sockets.adapter.rooms[socket.roomId].isOpen = true;
io.sockets.adapter.rooms[socket.roomId].players = [socket.username];
console.log('Game created! ID: ', socket.roomId);
});
}
If I try to log socket.roomId it would return something like rBAhx0. And when I log io.sockets.adapter.rooms, I get the following:
Map {
'PX_o3Di9sp_xsD6oAAAB' => Set { 'PX_o3Di9sp_xsD6oAAAB' },
'rBAhx0' => Set { 'PX_o3Di9sp_xsD6oAAAB' }
}
However, when I try to log io.sockets.adapter.rooms[socket.roomId], it returns undefined. How can I fix this?
Socket.io made some breaking changes in the newest version and rooms cannot be accessed like they used to. It changed a lot of objects and arrays into Maps and Sets, you can see it in the logs you've posted.
Set objects are collections of values. You can iterate through the elements of a set in insertion order. A value in the Set may only occur once; it is unique in the Set's collection.
The Map object holds key-value pairs and remembers the original insertion order of the keys. Any value (both objects and primitive values) may be used as either a key or a value.
Accessing properties of a Map works differently than accessing properties of a normal Object. Example:
const myMap = new Map();
myMap.set("foo", "bar");
console.log(myMap["foo"]) // undefined
console.log(myMap.get("foo")) // bar
Same applies to Sets, however in your case querying this Set in particular is probably a wrong approach, as this set only holds a collection of ids, and not actual room objects. Even if you were to get a value out of the Set, you could not access it's properties (host, isOpen and players) since it is only a string.
The version 3.0 made accessing the list of all rooms directly impossible I'm afraid. However the adapter now has a property socketRooms, which can be used in place of it.
In order to access rooms of a socket easier, you should access them like so:
io.sockets.adapter.socketRooms(socketId);
However that would still just return a list of strings.
The simplest solution to this problem would be to create an external variable outside of the connection scope.
const rooms = {};
io.on('connection', (socket, next) => {
socket.on('create game', async () => {
console.log('Creating game...');
socket.username = await generateUsername();
socket.roomId = generateId();
socket.join(socket.roomId);
if (!rooms[socket.roomId]) rooms[socket.roomId] = {};
rooms[socket.roomId].host = socket.username;
rooms[socket.roomId].isOpen = true;
rooms[socket.roomId].players = [socket.username];
console.log('Game created! ID: ', socket.roomId);
});
}
As you can see in your own log, io.sockets.adapter.rooms is a Map object (in the later versions of socket.io), not a plain object. That means that a room is accessed with .get(), not with plain indexing such as what you are using:
io.sockets.adapter.rooms[socket.roomId].host = socket.username;
That simply won't work because there is no plain socket.roomId property on the Map.
If you wanted to get the room object, you would have to do this:
let roomObj = io.sockets.adapter.rooms.get(socket.roomId);
roomObj.host = socket.username;
In my opinion, I wouldn't mess with the room objects directly. We already know that socket.io has changed their design before, rendering any code that tried to use them as broken and in need of redesign. Instead, I'd probably just create my own data structure where I stored my own data and then I won't be dependent upon the whims of how socket.io changes their design. Accessing the room objects directly is not a committed design or committed API of socket.io. They can change how it works whenever they want and have, indeed, changed it recently.
After the breaking update from Socket.io of changing Array to Map.
A simple solution would be changing:
io.sockets.adapter.rooms[socket.roomId].host = socket.username;
to:
io.sockets.adapter.get(socket.roomId).host = socket.username;
Related
I've been working on chrome extention project.
What I am trying to do is store the input value and pull it out when the specific button is pressed.
Below is the part of js code:
import { Dexie } from '../node_modules/dexie/dist/dexie.mjs';
var DBName = 'test';
buttonA.addEventListener('click', () => {
const inp = document.getElementById("inp");
const db = new Dexie(DBName);
db.version(2).stores({
friend: '++id, name'
});
db.friend.add({
name: inp.value
})
});
buttonB.addEventListener('click', () => {
const db = new Dexie(DBName);
const ch = db.friend;
console.log("Checking the value in DB: " + ch);
});
When I try it, it stores whatever input to indexed DB after clicking buttonA (confirmed by using Chrome Developer Tool), however when it comes to clicking on buttonB, the log tells that ch is undefined (same for db.friend.name or db.name as well).
Because of this, even when I use get(), it returns me an error since I am accessing on undefined variable.
Could someone help figuring out why the program does not access properly to an indexed DB that exists when I click on a buttonB?
Thank you.
Problems:
The second instance of Dexie does not declare which tables there are, so db.friend is unknown.
Your code creates a new Dexie for every click. It would be much better and faster to reuse a single Dexie instance. If you create a new Dexie instance everytime, you must also close it after you to avoid resource leakage.
Recommendation:
Declare a singleton Dexie instance with version().stores(...) so it populates the 'friend' property for you.
Preferably this code could be in it's own module (such as 'db.js').
Use that single Dexie instance from any place where you need to store or read from the db.
New to MongoDB, very new to Atlas. I'm trying to set up a trigger such that it reads all the data from a collection named Config. This is my attempt:
exports = function(changeEvent) {
const mongodb = context.services.get("Cluster0");
const db = mongodb.db("TestDB");
var collection = db.collection("Config");
config_docs = collection.find().toArray();
console.log(JSON.stringify(config_docs));
}
the function is part of an automatically created realm application called Triggers_RealmApp, which has Cluster0 as a named linked data source. When I go into Collections in Cluster0, TestDB.Config is one of the collections.
Some notes:
it's not throwing an error, but simply returning {}.
When I change context.services.get("Cluster0"); to something else, it throws an error
When I change "TestDB" to a db that doesnt exist, or "Config" to a collection which doesn't exist, I get the same output; {}
I've tried creating new Realm apps, manually creating services, creating new databases and new collections, etc. I keep bumping into the same issue.
The mongo docs reference promises and awaits, which I haven't seen in any examples (link). I tried experimenting with that a bit and got nowhere. From what I can tell, what I've already done is the typical way of doing it.
Images:
Collection:
Linked Data Source:
I ended up taking it up with MongoDB directly, .find() is asynchronous and I was handling it incorrectly. Here is the reply straight from the horses mouth:
As I understand it, you are not getting your expected results from the query you posted above. I know it can be confusing when you are just starting out with a new technology and can't get something to work!
The issue is that the collection.find() function is an asynchronous function. That means it sends out the request but does not wait for the reply before continuing. Instead, it returns a Promise, which is an object that describes the current status of the operation. Since a Promise really isn't an array, your statment collection.find().toArray() is returning an empty object. You write this empty object to the console.log and end your function, probably before the asynchronous call even returns with your data.
There are a couple of ways to deal with this. The first is to make your function an async function and use the await operator to tell your function to wait for the collection.find() function to return before continuing.
exports = async function(changeEvent) {
const mongodb = context.services.get("Cluster0");
const db = mongodb.db("TestDB");
var collection = db.collection("Config");
config_docs = await collection.find().toArray();
console.log(JSON.stringify(config_docs));
};
Notice the async keyword on the first line, and the await keyword on the second to last line.
The second method is to use the .then function to process the results when they return:
exports = function(changeEvent) {
const mongodb = context.services.get("Cluster0");
const db = mongodb.db("TestDB");
var collection = db.collection("Config");
collection.find().toArray().then(config_docs => {
console.log(JSON.stringify(config_docs));
});
};
The connection has to be a connection to the primary replica set and the user log in credentials are of a admin level user (needs to have a permission of cluster admin)
So basically, my bot comes with reaction roles and it works pretty efficiently by using partials. One of the checks before assigning the given role, Is to check whether the reaction role is reacted upon the msgID that I defined in my code. (This is useful for servers that have a channel like #reaction-roles and there is always 1 message that stays there for people to react with)
That is working fine. However, I was trying to do something new with my bot where I need the msgID to be saved (because the bot repeats the msg over and over to different people), however since the const of msgID is in the method called bot.on(message) I cannot access the const anywhere outside the method. Is there any way to get around this? Perhaps a way to temp store it in a config file?
I'm not familiar with discord SDK so this is a more general suggestion.
Think if you really need to use const here. I would suggest using let and defining it outside of the function like:
let msgID
bot.on(message, (message) => {
msgID = message.id
})
// now it's available here
use(msgID)
Just keep in mind that const will not work here
You can assign obj to const variable outside the function and assign value to it inside the function and then you can access it outside the function as well.
Remember, when you assign object to a const it is mutable in a sense that you can change its key/values but you cannot assign something else to this variable.
const someObj = {key: null};
function someName() {
someObj.key = "hello";
}
console.log(someObj.key);
I took data from MongoDB and executed it with forEach statement.
However, I want to change the date data from 2018-08-01T00:00:00.000Z' to 'YYYY-MM.
When I ran console.log using the Moment package of Nodejs, the data I wanted was printed out.
console.log(moment(repo.created_at).format("YYYY-MM")) code normally outputs the data I want, but the original data did not change when saved as repo.created.
I don't know how to solve this problem.
/* GET MyPage Page */
router.get(`/:userId/admin/mypage`, function (req, res, next) {
let userId = req.params.userId; // UserID Variable
Repo.find({ // MongoDB
'owner.login': userId
}, function (err, repo) {
if (err) throw err;
// console.log(repo);
repo.forEach(function (repo) {
repo.created_at = moment(repo.created_at).format("YYYY-MM");
console.log(repo.created_at)
repo.updated_at = moment(repo.updated_at).format("YYYY-MM");
console.log(repo.updated_at)
})
res.render('mypage/main', {
userId: userId,
dataArray: repo,
})
})
When I console.log it it's still like this.
2018-08-01T00:00:00.000Z
2018-09-01T00:00:00.000Z
2018-09-01T00:00:00.000Z
2018-09-01T00:00:00.000Z
2018-09-01T00:00:00.000Z
2018-09-01T00:00:00.000Z
2018-08-01T00:00:00.000Z
2018-09-01T00:00:00.000Z
2017-08-01T00:00:00.000Z
But with console.log(moment(repo.created_at).format("YYYY-MM")) code. it works fine
2018-09
2018-09
2018-09
2018-09
2018-09
2018-09
2018-08
I recommend to use a map function instead of forEach, because it is normally more performant and doesn't mutate the original array. (A short demonstration about the differences between map and forEach is given in the article "JavaScript — Map vs. ForEach
What’s the difference between Map and forEach in JavaScript?" at codeburst.io.)
Furthermore you should use a different name for your parameter names, because it can cause confusion about what is meant. This is called "scope" in JavaScript and it is also an example of "variable shadowing" or "name shadowing" and can cause subtle bugs. Therefor, you should use this with caution. In your case, you could name the loop parameter holding the array entries of your repo something like "repoElement", "repoItem", "repoEntry" or you find a more descriptive name.
You also say
but the original data did not change when saved as repo.created
If you do something like repoItem.created = moment(repoItem.created_at).format("YYYY-MM");, you add a new property to the repoItem-object, but the created_at property remains untouched. In the following example, I have named the formatted dates createdAt and updatedAt, so that these new properties get created. But every new repoItem will also have the untouched created_at and updated_at properties available next to the new properties createdAt and updatedAt.
// ...
const dataArray = repo.map(repoItem => ({
...repoItem,
createdAt: moment(repoItem.created_at).format("YYYY-MM"),
updatedAt: moment(repoItem.updated_at).format("YYYY-MM")
});
res.render('mypage/main', {
userId,
dataArray
})
// ...
If you don't want to include the originally formatted data, you have to make another step and destruct the data that you want before adding it to the new object. You can also destruct everything and then delete created_at and updated_at. This can be done like this:
const newRepoItem = { ...repoItem };
delete newRepoItem.created_at;
delete newRepoItem.updated_at;
or by using the "rest" parameter like this:
const { created_at, updated_at, ...newRepoItem } = repoItem;
// now, you can use the new variable newRepoItem.
More information about removing unwanted properties from an object can be found here: Remove value from object without mutation.
I would use the version that is better understood by you and your team. In my opinion, the keyword delete is the solution that is easier to understand for most people, because it literally says what it does. But if you like destructuring and are used to it, this can be the more compact and readable solution. Don't forget, that it could also be possible, that you only have a few properties in your repoItem that you care about. In this case, you can simply destructure the items that you want to keep. This could look like in the following example:
// ...
const dataArray = repo.map(repoItem => {
const { firstPropertyToKeep, secondPropertyToKeep } = repoItem;
return ({
firstPropertyToKeep,
secondPropertyToKeep,
createdAt: moment(repoItem.created_at).format("YYYY-MM"),
updatedAt: moment(repoItem.updated_at).format("YYYY-MM")
});
res.render('mypage/main', {
userId,
dataArray
})
I have create a REPL where you can see the difference between forEach and map: https://repl.it/#helgedrews/forEach-vs-map
I hope this helps a bit.
Just use .map()
Example:
const updatedRepos = repos.map((repo)=>{
return {
...repo,
created_at: moment(repo.created_at).format("YYYY-MM");
updated_at: moment(repo.updated_at).format("YYYY-MM");
}
})
use [].map instead or set variable to save the result
I have a command handler that uses a map with commands assigned from user command folders and a general comm folder. User can add their own commands. the map filters using name inside the file which is also how the call them. How do I make a comparison if there is more than one with the same name? Afaik maps can't do this and fear I have to redo it all.
At first I was trying to assign every user it's own map
client.User1.get(command, argument);
client.User2.get(command argument);
was this actually the right way to do it? I am using a .JSON with enabled command names for initial comparison. Below some of the code, as you ccan probably tell i'm not that experienced.
const assignedChannels = client.opts.channels.slice(0);
assignedChannels.unshift('#basiccommands');
const rootForCommands = "./commands";
client.commandCall = new Map();
for (const channelspecificDir of assignedChannels) {
const commandFiles = fs.readdirSync(`${rootForCommands}/${channelspecificDir}`);
for (const file of commandFiles) {
const commandCollection = require(`${rootForCommands}/${channelspecificDir}/${file}`);
//sets a new item in the collection.
//Key of map as command name and the value the function to send message and argument true or false
client.commandCall.set(commandCollection.name, commandCollection);
const currentProfile = JSON.parse(fs.readFileSync(`./userProfiles/${channel}.json`, 'utf8'));
if (!currentProfile.all_commands.includes(commandPre)){
return console.log("Command does not exist");
}
try {
client.commandCall.get(commandPre, argU).execute(channel, argU);
} catch (error) {
console.log('command broke');
console.error(error);
}
My prevoious method was to assign user specific folder and general command folder to a map and at the end of each iteration change the name of the map object. But that does not work for some reason.
Actually, you can use symbols as keys for maps
const map = new Map()
map.set(Symbol(‘the same name’), value1)
map.set(Symbol(‘the same name’), value2)
You can use any values for Map’s keys, even objects and functions
map.set({}, value3)
map.set({}, value4)
In all previous expressions we get different values (links) for Map’s keys.
BTW. Be aware with const some = require(‘some_file_name’).
require() caches a value for a file name. So, if your files may be updated during a run time and you need to read these updates you shouldn’t use ‘require()’. It’s good only for importing modules and static values (e.g. configs)