Skip to content

Instantly share code, notes, and snippets.

@boatbomber
Last active December 13, 2022 18:53
Show Gist options
  • Save boatbomber/6a98e36238ed79aec1c1db93b8cf1c7f to your computer and use it in GitHub Desktop.
Save boatbomber/6a98e36238ed79aec1c1db93b8cf1c7f to your computer and use it in GitHub Desktop.
InfStore - Storing inf size dictionaries in Roblox Datastores via automagic efficient chunking
-- InfStore.lua
-- boatbomber
-- A module to have DataStores hold an inf size dictionary
-- by automagically chunking the data behind the scenes
-- Example:
-- local store = InfStore.new("Global_v0.1.0", "Tutorials")
-- store:Get()
-- store:Add("UniqueTutorialId", TutorialData)
--
-- local submissions = InfStore.new(plr.UserId .. "_v0.1.0", "Submissions")
-- submissions:Add("UniqueSubmissionId", SubmissionData)
local DataStoreService = game:GetService("DataStoreService")
local HttpService = game:GetService("HttpService")
local LIMIT = 2_000_000 -- ~2Mb
local VERBOSE = true
local function verCall(callback)
-- Callback instead of print string so when VERBOSE is false, expensive computations are skipped entirely
if VERBOSE then
callback()
end
end
local module = {}
local existingStores = {}
function module.new(identifier: string, name: string)
if existingStores[identifier] == nil then
existingStores[identifier] = {}
elseif existingStores[identifier][name] then
return existingStores[identifier][name]
end
local InfStore = {
_store = DataStoreService:GetDataStore(identifier),
_name = name,
_chunks = {},
_cached = nil,
}
existingStores[identifier][name] = InfStore
local debugId = identifier.."|"..name
verCall(function()
print("Created InfStore:", debugId)
end)
function InfStore:Get(skipCache: boolean?)
if (not skipCache) and (self._cached ~= nil) then
return self._cached
end
local result = {}
local i = 0
while true do
i += 1
-- Get next chunk
local chunk = self:GetChunk(i, skipCache)
-- Exit if we've reached the end
if chunk == nil then
break
end
-- Copy the chunk's values into our result
for k, v in pairs(chunk) do
result[k] = v
end
verCall(function()
print(debugId, string.format(" :Get() chunk %d (%.2f Mb)", i, #HttpService:JSONEncode(chunk)/1024/1024))
end)
end
verCall(function()
print(debugId, string.format(":Get() returned a combined %d chunks (%.2f Mb)", i-1, #HttpService:JSONEncode(result)/1024/1024))
end)
self._cached = result
return result
end
function InfStore:GetChunk(chunk: number, skipCache: boolean?)
-- Used for manual lazy loading to get chunks on demand instead of all initially
if (not skipCache) and (self._chunks[chunk]) then
return self._chunks[chunk]
else
local c = self._store:GetAsync(self._name .. chunk)
self._chunks[chunk] = c
return c
end
end
function InfStore:SetChunk(chunk: number, value: {})
verCall(function()
warn(debugId, string.format(":SetChunk() overwriting chunk %d to a %.2f Mb table", chunk, #HttpService:JSONEncode(value)/1024/1024))
end)
-- Used for overwriting stores
self._store:SetAsync(self._name .. chunk, value)
end
function InfStore:Add(key: string, value: any)
local cost = #HttpService:JSONEncode(value)
local i = 0
while true do
i += 1
-- Try next chunk
local complete = false
self._store:UpdateAsync(self._name .. i, function(chunk)
-- New chunk
if chunk == nil then
complete = true
return {
[key] = value,
}
end
-- No space in this chunk
if LIMIT - #HttpService:JSONEncode(chunk) <= cost then
return nil
end
-- Add to chunk
complete = true
chunk[key] = value
self._chunks[i] = chunk
verCall(function()
print(debugId, string.format(":Add() inserted a value (%.3f Kb) into chunk %d (%.2f Mb) at ['%s']", cost/1024, i, #HttpService:JSONEncode(chunk)/1024/1024, key))
end)
return chunk
end)
if complete then
self._cached = nil
break
end
end
end
function InfStore:Remove(key: string)
local i = 0
while true do
i += 1
local complete, empty = false, false
self._store:UpdateAsync(self._name .. i, function(chunk)
-- Reached last chunk, exit
if chunk == nil then
complete = true
return nil
end
-- Chunk doesn't contain key, skip
if chunk[key] == nil then
return nil
end
-- Remove key from chunk
chunk[key] = nil
self._chunks[i] = chunk
verCall(function()
print(debugId, ":Remove() cleared ['" .. key .. "'] from chunk " .. i)
end)
if next(chunk) == nil then
-- Removing this key has emptied the chunk
empty = true
end
return chunk
end)
-- Move chunks down if this one got emptied by the removal
if empty then
-- Clear this chunk
self._store:RemoveAsync(self._name .. i)
self._chunks[i] = nil
-- Push down subsequent chunks to fill the whole
local n = 0
while true do
n += 1
-- Get next chunk
local chunk = self._store:GetAsync(self._name .. (i+n))
-- Exit if we've reached the end
if chunk == nil then
break
end
-- Move chunk down
local a, b = (i+n-1), (i+n)
self._store:SetAsync(self._name .. a, chunk)
self._chunks[a] = chunk
self._store:RemoveAsync(self._name .. b)
self._chunks[b] = nil
end
end
-- Exit once we've done the last chunk
if complete then
self._cached = nil
break
end
end
end
function InfStore:RemoveBulk(keys: {string})
local i = 0
while true do
i += 1
local complete, empty = false, false
self._store:UpdateAsync(self._name .. i, function(chunk)
-- Reached last chunk, exit
if chunk == nil then
complete = true
return nil
end
-- Remove keys from chunk
local changed = false
for _, key in keys do
if chunk[key] ~= nil then
chunk[key] = nil
changed = true
end
end
self._chunks[i] = chunk
-- If we didn't change anything, skip
if not changed then
return nil
end
if next(chunk) == nil then
-- Removing this key has emptied the chunk
empty = true
end
return chunk
end)
-- Move chunks down if this one got emptied by the removal
if empty then
-- Clear this chunk
self._store:RemoveAsync(self._name .. i)
self._chunks[i] = nil
-- Push down subsequent chunks to fill the whole
local n = 0
while true do
n += 1
-- Get next chunk
local chunk = self._store:GetAsync(self._name .. (i+n))
-- Exit if we've reached the end
if chunk == nil then
break
end
-- Move chunk down
local a, b = (i+n-1), (i+n)
self._store:SetAsync(self._name .. a, chunk)
self._chunks[a] = chunk
self._store:RemoveAsync(self._name .. b)
self._chunks[b] = nil
end
end
-- Exit once we've done the last chunk
if complete then
self._cached = nil
break
end
end
verCall(function()
print(debugId, ":RemoveBulk() cleared {" .. table.concat(keys, ", ") .. "} from the store")
end)
end
function InfStore:Replace(key: string, value: any)
local i = 0
while true do
i += 1
local complete = false
self._store:UpdateAsync(self._name .. i, function(chunk)
-- Empty chunk, exit
if chunk == nil then
complete = true
return nil
end
-- Doesn't have our key, skip
if chunk[key] == nil then
return nil
end
-- Update key in chunk
chunk[key] = value
complete = true
self._chunks[i] = chunk
verCall(function()
print(debugId, ":Replace() replaced value for ['" .. key .. "'] in chunk", i)
end)
return chunk
end)
-- Exit if we've reached the end
if complete then
self._cached = nil
break
end
end
end
function InfStore:Update(key: string, transformer: (any) -> any?)
local i = 0
while true do
i += 1
local complete = false
self._store:UpdateAsync(self._name .. i, function(chunk)
-- Empty chunk, exit
if chunk == nil then
complete = true
return nil
end
-- Doesn't have our key, skip
if chunk[key] == nil then
return nil
end
-- Update key in chunk
local success, replace = pcall(transformer, chunk[key])
if success and replace then
chunk[key] = replace
self._chunks[i] = chunk
end
complete = true
if success and replace then -- Only use datastore budget if we actually changed chunk
verCall(function()
print(debugId, ":Update() transformed value for ['" .. key .. "'] in chunk", i)
end)
return chunk
else
return nil
end
end)
-- Exit if we've reached the end
if complete then
self._cached = nil
break
end
end
end
-- Aliases for API style consistency in various projects
InfStore.Retrieve = InfStore.Get
InfStore.Pull = InfStore.Get
InfStore.Set = InfStore.Add
InfStore.Insert = InfStore.Add
InfStore.Push = InfStore.Add
InfStore.Delete = InfStore.Remove
InfStore.Patch = InfStore.Replace
InfStore.Transform = InfStore.Update
return InfStore
end
return module
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment