7 minute read
The new API changes for data stores have made me reflect a little on what the path for DataStore2 going forward is. As I say many times, my projects move with me. They are open source, yes, and open to contribution, but I don't support projects I don't use, and I don't add features I don't use. This is why, for example, there is no getting data stores by user ID in DataStore2. There are some safety cautions (mostly with users being in a separate server as you use it), but the primary reason is that I have never had a reason to want it, and nobody has sent a pull request.
That being said, I've moved a long way since DataStore2. DataStore2 is a module written 3 years ago for Battle Hats that I released just on a whim, and it blew up far past what I could have ever expected. I'm of course very happy that my work is being used in lots of huge games, and whenever the topic of data store libraries come out, they always have to compare to me. It's also nice to see Roblox seeing DataStore2 as a point of reference for future APIs. That being said, it is very challenging for me to maintain.
DataStore2(dataStoreName)
is ugly, and forced DataStore2 to adopt a weird __call
metatable structure. I would have gone with DataStore2.new
nowadays.:GetAsync()
and friends, but it's far from perfect.You'll notice a lot of these have "I didn't know" attached to it. That's where DataStore2 collapses--it was made by a 15 year old who was convinced XML style documentation was the future, and whose only prior experience with data storage at a higher scale than saving a text file was old PHP websites with MySQL injection. I've come a long way since then.
My projects still move with me, and I do not plan on touching anything data store related until my next project needs it (which won't be for a while, we've been explicitly trying to create a V-slice with the restriction of "nothing that will need to save data"). I have a lot of time to think on it, but I am considering not updating DataStore2 with anything other than contributions, and starting a new project. I will likely not advertise this new project on the Developer Forums, as I do not feel capable of single-handedly running a project of such an important backbone for thousands of people, especially with no income (if you'd contribute to a Patreon or Kofi, let me know :wink:). I might just keep it to a repository, and perhaps a thread on the Fivum, or the Community Dev Forums, or whatever I'm supposed to call it.
:Set(nil)
.:SetBackup
sense.:Get()
directly, then the code will explode and it is their fault (though we can provide a configuration option to error when they do this). This can also be supported by having helper methods like assign
.If this sounds a lot like Quicksave, that's because it is. I don't know why I want to reinvent the wheel, maybe I just like watching things roll.
This is just off my dome, there is no actual code to make this work yet. The data store library is referred to as Bikeshed, here.
-- Old code
DataStore2.Combine("DATA", "Inventory")
local inventoryStore = DataStore2(player, "Inventory")
local inventory = inventoryStore:Get({})
table.insert(inventory, { Name = "Doge" })
inventoryStore:Set(inventory)
-- or...
inventoryStore:Update(function(inventory)
table.insert(inventory, { Name = "Doge" })
return inventory
end)
-- New code
-- Ideally you will limit your use of withBikeshed, and pass down the
-- API through dependency injection (as parameters of a function).
withBikeshed(function(bikeshed)
local dataStore = bikeshed.new(player)
local inventoryStore = dataStore:getStore(stores.Inventory)
inventoryStore:get():andThen(function(inventory)
-- You would likely have helpers for this,
-- maybe in something like Llama.
local inventoryCopy = {}
for _, item in ipairs(inventory) do
table.insert(inventoryCopy, item)
end
table.insert(inventoryCopy, { Name = "Doge" })
-- This likely doesn't need to be a promise.
inventoryStore:set(inventoryCopy)
end)
-- or...
inventoryStore:update(function(inventory)
inventory = copy(inventory) -- `copy` is implemented elsewhere, by you.
table.insert(inventory, { Name = "Doge" })
return inventory
end)
end)
This is where stores.Inventory
comes from. It's a struct like this:
local t = require(ReplicatedStorage.Vendor.t)
local InventoryStore = {}
InventoryStore.key = "Inventory"
-- No more :Get({}).
InventoryStore.default = {}
-- `validate` is just a function that receives the data and returns
-- `true` if the data is valid and `false, error` if it is not.
-- This is beautifully generic, and allows for `t` to be used, so I do
-- not have to write more boilerplate myself.
InventoryStore.validate = t.array(Items.validateItem)
-- These are optional
InventoryStore.validateSerialized = t.array(
t.numberConstrained(1, #Items.items)
)
-- Both this and `deserialize` can possibly return promises, that's fine.
function InventoryStore.serialize(data)
local serialized = {}
for _, item in ipairs(data) do
table.insert(serialized, Items.getIdByName(item.Name))
end
return serialized
end
function InventoryStore.deserialize(data)
local deserialized = {}
for _, itemId in ipairs(data) do
table.insert(deserialized, Items.getItemById(itemId))
end
return deserialized
end
return InventoryStore
Things like :SetBackup
and :Save
were not invented with things like Combine in mind (that's why DataStore2.SaveAll
exists). Now that Combine is default, this new API could instead just have a global configuration.
Bikeshed.setGlobalConfig({
backupsEnabled = true, -- Might even be on by default, not sure.
retryTimes = 5,
loggingLevel = Bikeshed.LoggingLevel.Trace,
})
--- ...or even
local withBikeshed = Bikeshed.useConfig({
-- ...
})
withBikeshed(function(bikeshed)
local dataStore = bikeshed.new(player)
dataStore:getStore(stores.Inventory)
:get()
:andThen(function(inventory)
-- Player buys an item
dataStore:save()
end)
end)
I still am not solid on a good API for this, but this is what I can come up with, and is similar to what I did in Zombie Strike:
-- Looks like saving names was a stupid idea...
local function namesToIds(dataStore)
return dataStore:getStore(stores.Inventory)
:update(function(inventory)
local newInventory = {}
for _, item in ipairs(inventory) do
table.insert(newInventory, {
Id = Items.getIdByName(item.Name),
})
end
return newInventory
end)
:tap(function()
print("Yep, all done! Migrations return promises.")
end)
end
Bikeshed.setMigrations({ namesToIds })
If I call it DataStore3, shoot me.