Memory stores#
memory is a global that keeps small, short lived data in the server's memory: who is waiting for a
match, the scores of the current event, a lock that should let go on its own. Everything written to it
has an expiration and goes away when that runs out. Nothing is written to disk.
Use a store for what has to be there tomorrow. Use memory for what only matters for
minutes or days, and for what several scripts need to share quickly.
local waiting = memory:hashMap("waiting")
waiting:set(player.name, { joined = clock() }, 60) -- gone after 60 seconds
local entry = waiting:get(player.name) -- nil once it has expiredThere are three kinds:
memory:hashMap(name)is keys and values.memory:sortedMap(name)is keys and values kept in order, which you read in pages.memory:queue(name, invisibilityTimeout?)is a line of values that scripts take from.
A name is 1 to 128 characters. Every server script that asks for the same kind with the same name gets the same data. A hash map and a sorted map with the same name are two different things.
What they share#
- Server only.
memoryworks in a serverScript. In aLocalScriptevery call errors with "memory stores run on the server". - Every write has an expiration. It is a number of seconds, more than 0 and up to 3,888,000 (45 days). There is no default, and leaving it out is an error. Writing a key again starts its expiration over. Expirations count real time, so a paused singleplayer game does not stop them.
- Keys are strings of 1 to 128 characters.
- Values are what a store takes: numbers, strings, booleans and tables of them,
nested up to 32 deep. A value is at most 32 KB once encoded.
nilis an error; remove the key instead. What you read back is a copy, so changing it changes nothing until you write it again. - How long it lasts. Everything is cleared when the place stops and when it goes back to editing.
A hot reload keeps it, like
game.persist. - One server. The data lives in this server. Nothing is shared with another server.
Hash maps#
local locks = memory:hashMap("locks")
locks:set("door", player.name, 10)
print(locks:get("door"))
local count = locks:update("visits", function(old)
return (old or 0) + 1
end, 3600)
locks:remove("door")
print(locks:list(50))| Method | What it does |
|---|---|
get(key) |
the value, or nil when there is none or it has expired |
set(key, value, expiration) |
writes the value. Returns true |
update(key, transform, expiration) |
calls transform(old) with the value or nil, and writes what it returns. Returns the new value |
remove(key) |
deletes the key. Removing a key that is not there is fine |
list(pageSize?, after?) |
the keys that come after after, in alphabetical order, up to pageSize of them. 1 to 1000, default 1000 |
- If
transformreturnsnil, nothing is written andupdatereturnsnil. Use that to leave the value as it is. - Hash maps have no size call.
Listing every key#
list reads one page. Pass the last key of a page as after to read the next one. A page with fewer
than pageSize keys is the last.
local after = nil
repeat
local page = locks:list(100, after)
for _, key in page do
print(key)
end
after = page[#page]
until #page < 100- A key that is there the whole time is listed once, even when other keys are written, removed or expire between two pages.
- A key written during the walk is listed if it comes after the last page read, and left out if it comes before.
afterdoes not have to be a key that exists.list(100, "m")starts at the first key after"m".
Sorted maps#
A sorted map keeps each key with a value and an optional sort key, and reads them back in order.
local scores = memory:sortedMap("event")
scores:set(player.name, { name = player.name }, 3600, 42)
local value, sortKey = scores:get(player.name)
local top = scores:getRange("descending", 10)
for i, row in top do
print(i, row.key, row.sortKey)
end| Method | What it does |
|---|---|
get(key) |
the value and its sort key, or nil |
set(key, value, expiration, sortKey?) |
writes them. Returns true if the key was new, false if it replaced one |
update(key, transform, expiration) |
calls transform(value, sortKey), and writes the value and sort key it returns. Returns them |
remove(key) |
deletes the key |
getRange(direction, count, exclusiveLowerBound?, exclusiveUpperBound?) |
up to count rows, "ascending" or "descending". count is 1 to 200 |
getSize() |
how many keys there are |
- A sort key is a number or a string of up to 128 characters. A number must be finite.
- If
transformreturnsnil, nothing is written andupdatereturnsnil. If it returns only a value, the key loses its sort key. getRangereturns a list of{ key = ..., value = ..., sortKey = ... }.sortKeyis missing on a row that has none.
The order#
Ascending order is:
- keys with a number sort key, smallest first,
- then keys with a string sort key, in alphabetical order,
- then keys with no sort key.
Keys with the same sort key, or with none, are in alphabetical order of the key. Alphabetical order compares characters, so capitals come before small letters. Descending is the same order backwards.
Bounds#
A bound is a table { key = ..., sortKey = ... } with one or both. The rows returned are strictly
after the lower bound and strictly before the upper bound, in the order above. Use the last row of a
page as the lower bound of the next page:
local page = scores:getRange("ascending", 100)
local last = page[#page]
local following = scores:getRange("ascending", 100, { key = last.key, sortKey = last.sortKey })- Going
"descending", use the last row of a page as the upper bound of the next page instead. - A bound with only a
sortKeyleaves out every key with that sort key. - A bound with only a
keycounts as having no sort key, so it sits among the keys with none. As a lower bound it leaves out every key that has a sort key.
Queues#
A queue hands values out to whichever script reads first. A value read is hidden, not removed. The reader removes it once it is done. If the reader never does, the value shows up again after the invisibility timeout, so another script can try.
local jobs = memory:queue("jobs", 30)
jobs:addAsync({ map = "desert" }, 600)
jobs:addAsync({ map = "castle" }, 600, 10) -- higher priority, read first
task.spawn(function()
while true do
local values, id = jobs:readAsync(1)
startMatch(values[1])
jobs:removeAsync(id :: string)
end
end)memory:queue(name, invisibilityTimeout?) takes the seconds a value stays hidden after it is read,
more than 0 and up to 45 days. The default is 30. The timeout belongs to the object you got, so two
calls with the same name and different timeouts read the same queue with different timeouts.
| Method | What it does |
|---|---|
addAsync(value, expiration, priority?) |
adds the value. priority is a finite number, default 0 |
readAsync(count, allOrNothing?, waitTimeout?) |
reads up to count values, 1 to 100. Returns the values and a read id |
removeAsync(id) |
removes the values of that read |
getSize(excludeInvisible?) |
how many values there are. true leaves out the hidden ones |
- Higher priority is read first. The same priority is read in the order it was added.
readAsynctakes only values that are not hidden. WithallOrNothingit takes nothing untilcountvalues are there.- When nothing can be read,
readAsyncwaits. It checks again every tick.waitTimeoutis how many seconds it waits;-1, the default, waits until something comes. When it gives up it returns an empty table andnil.0returns at once. It yields, so call it insidetask.spawnor a function that may yield. removeAsyncremoves only values still hidden by that read. Once a value shows up again, the old id no longer removes it, and a new read gives it a new id.
Limits#
| What | Limit |
|---|---|
| name | 1 to 128 characters |
| key | 1 to 128 characters |
| string sort key | up to 128 characters |
| value | 32 KB once encoded |
| expiration and invisibility | more than 0, up to 45 days |
list page |
1 to 1000 keys |
getRange |
1 to 200 rows |
readAsync |
1 to 100 values |
There is no limit on how many keys a map holds or how often you call it. All of it is in the server's memory, so keep it small.
Nothing is throttled and nothing counts against a quota, so calls do not fail for being too many, and there is no need to retry them.
