Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
While Enmap is a Map enhanced with Array methods, Enmap also offers some enhanced array methods for the data stored inside of it. Talk about Arrayception!
So what do I mean by methods for your stored data? I mean that you can store arrays inside Enmap, and directly push, pull, add and remove from those arrays. There are methods to work both on direct arrays, as well as arrays stored inside of an object.
Let's take a look at three example entries in Enmap that we can use. The first is a direct array, the second is an array inside an object, the last is an array of objects.
There are two methods to push to an array, one for simple arrays and one for arrays inside objects. Pushing in an Enmap array is the same as a regular array push: it adds the element to the end of the array.
The third parameter in push is the "path" to the array in an object. It works the same as the properties path used in Working With Objects.
Similarly, you can remove from an array. With the normal path system, you can either remove via the index in the array, or remove simple strings. To remove a complex object, you'll need to use a function in the remove method.
Mostly, this documentation will be concentrating on the "persistent" version of enmap - the one where data is saved automatically.
If you don't want persistence, the only difference is how you initialize the enmap:
By default, Enmap saves only in memory and does not save anything to disk. To have persistent storage, you need to add some options. Enmaps with a "name" option will save, and there are additional options you can use to fine-tune the saving and loading features.
The following is a list of all options that are available in Enmap, when initializing it:
name
: A name for the enmap. Defines the table name in SQLite (the name is "cleansed" before use).
If an enmap has a name, it is considered persistent and will require better-sqlite-pool
to run.
If an enmap does not have a name, it is not persistent and any option related to database interaction is ignored (fetchAll, autoFetch, polling and pollingInterval).
fetchAll
: Defaults to true
, which means fetching all keys on load. Setting it to false
means that no keys are fetched, so it loads faster and uses less memory.
autoFetch
: Defaults to true
. When enabled, will automatically fetch any key that's requested using get, getProp, etc. This is a "synchronous" operation, which means it doesn't need any of this promise or callback use.
dataDir
: Defaults to ./data
. Determines where the sqlite files will be stored. Can be relative (to your project root) or absolute on the disk. Windows users , remember to escape your backslashes!
cloneLevel
: Defaults to deep
. Determines how objects and arrays are treated when inserting and retrieving from the database.
none
: Data is inserted by reference, meaning if you change it in the Enmap it changes outside, and vice versa. This should only be used in non-persistent enmaps if you know what you're doing!.
shallow
: Any object or array will be inserted as a shallow copy, meaning the first level is copied but sub-elements are inserted as references. This emulates Enmap 3's behavior, but is not recommended unless you know what you're doing.
deep
: Any object or array will be inserted and retrieved as a deep copy, meaning it is a completely different object. Since there is no chance of ever creating side-effects from modifying object, This is the recommended, and default, setting.
polling
: defaults to false
. Determines whether Enmap will attempt to retrieve changes from the database on a regular interval. This means that if another Enmap in another process modifies a value, this change will be reflected in ALL enmaps using the polling feature.
pollingInterval
: defaults to 1000
, polling every second. Delay in milliseconds to poll new data from the database. The shorter the interval, the more CPU is used, so it's best not to lower this. Polling takes about 350-500ms if no data is found, and time will grow with more changes fetched. In my tests, 15 rows took a little more than 1 second, every second.
Now that we have a functional Enmap structure (which we'll always refer to as myEnmap
), we're ready to start writing data to it, and getting data from it.
The code samples on this page assume that you have correctly initialized
myEnmap
In terms of Enmap, "writing", "adding" and "editing" data is essentially the same thing. When using the basic set()
method, if the key does not exist it's created, and if it does, it's modified.
Enmap supports pretty much all native JavaScript data types. However, it cannot support Class Instances directly. That means if you have, say, a "User" object or a "House" class, they cannot be stored here.
There is however a workaround, which is to use Serializing and Deserializing.
Objects and Arrays are a little more complex to deal with, so they have their own page. See Working with Objects for more information.
The usage for the set()
method is simple:
key
must be a string or integer. A key should be unique, otherwise it will be overwritten by new values using the same key.
value
must be a supported native data type as mentioned above.
Here are a few examples of writing simple data values:
Getting data back from an Enmap is just as simple as writing to it. All you need is the key
of what you want to retrieve, and you get its value back!
That's pretty much it for only retrieving a single data value. There are more complex operations that are available, take a look at Array Methods for the more advanced things you can do on Enmap's data!
Removing data from Enmap is as simple as saving or retrieving. You can easily use the delete() method as such:
This page will describe how to use Enmap from multiple files within your same project. Note that I mean the same app, process, or shard, but different files within this one running process.
When Enmap is used with its default options, it loads everything in its cache and generally provides your data from this cache, not directly from the database. In the case where you want to use the data from one Enmap from multiple locations, you might encounter the following issue:
Hi! When I update data in Enmap from one file, it doesn't update in the other file, I have to restart the bot to update. Is this a bug?
However, this also means that when you do new Enmap({ name: "something" })
from more than one file, that's also a different instance, that doesn't share the same memory space. So not only will it not update the data in memory for the other file, it also uses double the memory. And of course, that's bad. So how do we fix this?
Admittedly, the vast majority of you Enmap users are doing Discord.js Bots, and even though Enmap works fine with any nodejs project that need simple data storage, bots are my main clients. Considering this fact, we have an extremely simple way to share an Enmap between multiple files: We attach it to the bot client. Usually your client is defined in your main file (index.js, app.js, bot.js, whatever you named it), and every part of your bot has access to this client. We can attach Enmap directly to it, like so:
This will work even if you're using a command handler, framework, or whatever - as long as you have access to a client variable, you have access to your enmaps.
In other frameworks and libraries, you might have something similar. For example with Express or Koa for http servers, you can sometimes attach the enmap to your request from the very top, in a middleware. If that's not possible, or if you find that to be complicated, you can use the next method.
This means you can simply require that file elsewhere. Let's say we called that file db.js
, here's how you'd use it:
And as I mentioned, as a bonus you now have the ability to create functions which you can export and use, to simplify your code and remove duplication. So, let's say I need to get all the tags for a specific guild, and my tags are built using an object as shown above. To get all those tags for a guild, you'd need filters, right? Like so:
now let's say you use this code a lot in your app, and you'd like to not have to type this whole thing every time. You could add a simple function in your module that only takes an ID and returns the tags:
To answer my own obvious question: it's not a bug, it's a feature that I cannot implement. The way Enmap's cache works is that the data is loaded in memory _in that _of Enmap, and only for that instance. This is what enables you to have many different Enmaps in your project - one Enmap doesn't share data with another.
Important Note: Do NOT override Discord.js' existing collections! That means, client.users, client.guilds, etc. - none of these should be overridden.
All things considered, are probably the recommended way to use your Enmap in multiple files within your project. Not only does it give you a single file to import, lets you define multiple Enmaps you can individually import, it also gives you the ability to add specific functions to do common actions you use throughout your project.
As covered in , modules are fairly straightforward. This is how I have done an Enmap shared module before:
And there you have it! There are other ways to build the exports, you can also split it differently, take a look at for more information.
Enmap is a great way to store structured data, and offers a few helper features that directly affect both objects and arrays.
Let's assume for a moment that we want to store the following data structure in Enmap:
This structure has 5 "properties": first
, second
, changeme
, isCool
, sub
. The sub
property has 2 properties of its own, yay
and thing
.
To store this structure in Enmap, you can use a variable, or just straight-up write the object:
Note: All further methods require the value to be an object. If you attempt to get, set, modify or remove using the below methods and your value isn't an object, Enmap will throw an error.
Retrieving a specific property from an object is done through the get()
method, by specifying both the key and the "path" to the property you want.
The exact method is <Enmap>.get(key, path)
.
You can also check if a specific property exists or not. This is done through the has
method, with a key, and path to the property:
There are a few various ways to modify properties of both Objects and Arrays. The very basic way to set a property on an object or array is through .set(key, value, path)
like the following examples:
As you can see, setProp() and getProp() work on the same concept that the path can be as complex as you want.
Arrays have additional helper methods, you can see them here.
What is Paths in Enmap, how to use them, what is their syntax?
In a whole lot of methods for Enmap, one of the properties is the "path". Paths are used in Object data saved in Enmap, that is to say, setting or ensuring a value that is an object at the top level.
To understand what a path really means, we can start by having an object as a value. Here I'm not even using Enmap, as the idea is related to basic JavaScript, not my module.
So here we have an object that actually has multiple levels, that is to say, the c
and sub
properties have, as a value, another object with its own keys. sub
takes this further with 4 different levels, just to fully demonstrate my point.
So how would we reach the values in this object? Well, in core JavaScript, let's say we wanted to get the word "cool", we'd use myObject.sub.values.are.cool
. This is one way to access object properties, the other one being myObject["sub"]["values"]["are"]["cool"]
(where those strings can be variables, btw, for dynamic property access).
Alright so what about the array, there? Well, arrays are accessed through their index, meaning their position in the array, starting at 0. That means to access the c.and
values, you'd do something like myObject.c.and[0]
. That looks like a strange syntax I'll admit, but considering you can use the same for objects, myObject["c"]["and"][1]
perhaps looks a bit more coherent.
Now that you've seen how to access those properties in regular JavaScript, what about doing it in Enmap? Well, it's actually quite simple: the path
parameter in the methods simply take exactly what you've seen above, with 2 exceptions:
The path doesn't include the object name (which is your key
)
You don't need to use variables for dynamic paths since it's a string
What does that mean in reality? Well let's rewrite the example above as Enmap code:
To access the "cool" string, the code then becomes myEnmap.get("myObject", "sub.values.are")
. Accessing the array values looks the same: myEnmap.get("myObject", "c.and[0]")
. In this case indexes can be used either way, so you can also do myEnmap.get("myObject", "c.and.0")
and that'll work equally well.
For this purpose, there are features in Enmap that enable less caching, by sacrificing some speed and ease of use. That is to say, with data not being fully loaded, there are some things that can't be done easily - see below for details.
The options are as follow:
fetchAll
: Defaults to true
, which means fetching all keys on load. Setting it to false
means that no keys are fetched, so it loads faster and uses less memory.
autoFetch
: Defaults to true
. When enabled, will automatically fetch any key that's requested using get, getProp, etc. This is a "synchronous" operation, which means it doesn't need any of this promise or callback use.
If fetchAll is set to false, no data (by default) will be loaded from the database - the Enmap will be completely empty. This means that doing a .size
check returns 0, looping and filtering doesn't return anything, and get() requests all return null.
Ok but... how's that useful? It's useful because if you don't need the data, it's not loaded. To load data, there are 2 different methods available.
enmap.fetchEverything()
will, of course, fetch all the data from the database - this is the method that's called on init() if fetchAll is true. This means you can stagger your loading time, or that you can decide, later, under certain conditions, you do want to load everything.
enmap.fetch(key)
can fetch a single key from the database, saves it to enmap, and returns a promise with the fetched value.
enmap.fetch([array, of, keys])
will fetch each key in the requested array, and return an array of [key, value]
pairs for each fetched value.
enmap.count
will give you the number of keys in the database itself, including uncached ones (.size is only cached values).
enmap.indexes
will give you a list of keys in the database, including uncached ones.
Yup. Those are the only things you really need to know for the current version of Enmap's fetchAll feature.
As described in , one disadvantage of Enmap is that it loads all your data in memory, so you're sacrificing RAM in order to gain speed. In larger projects, this might become a concern fairly quickly - or when using larger data sets that take more memory.
Learn how to manipulate the data you save and retrieve from the database, to more easily store complex data without having to convert it to simple data everywhere you use it.
Introduced in Enmap 5.6, Serializers and Deserializers are functions that you use to manipulate the data before storing it in the database, or before using it after retrieving it.
This feature is born from a limitation in Enmap: it cannot store very complex objects, such as the instance of a class, objects with circular references, functions, etc. So, typically when you have such data, you need to manually convert it to some simple representation before storing, and then do the inverse after getting it from enmap. This is a more automated way of doing it.
The Serializer function runs every single time data is stored in the enmap, if one is provided. This function receives the data provided to set() as an input, and must return a value to be stored in the database. This function MUST be synchronous, that is to say, cannot be an async function or return a promise.
The Deserializer function is the reverse, and runs on each value pulled from the database, before it is returned through the get() method. This function receives the data stored in the database and returns the value that you want to use directly. This function MUST be synchronous, that is to say, cannot be an async function or return a promise.
Taking a hit from my own example of Per-Server Settings, this is a better example that doesn't require storing just the name of a channel, but straight-up the channel itself.
This page is a work in progress and may not have the polish of a usual Evie-Written document!
Some quick docs:
Possible Operators (accepts all variations listed below, as strings):
+
, add
, addition
: Increments the value in the enmap by the provided value.
-
, sub
, subtract
: Decrements the value in the enmap by the provided value.
*
, mult
, multiply
: Multiply the value in the enmap by the provided value.
/
, div
, divide
: Divide the value in the enmap by the provided value.
%
, mod
, modulo
: Gets the modulo of the value in the enmap by the provided value.
^
, exp
, exponential
: Raises the value in the enmap by the power of the provided value.