From 07caa9c4a22a87a7cc456e8273863fbe3e98b13d Mon Sep 17 00:00:00 2001 From: Cooldude2606 Date: Thu, 21 May 2020 20:47:56 +0100 Subject: [PATCH] Added datastore --- expcore/datastore.lua | 267 +++++++++++++++++++++++++++------ modules/commands/interface.lua | 3 +- 2 files changed, 219 insertions(+), 51 deletions(-) diff --git a/expcore/datastore.lua b/expcore/datastore.lua index 371e76f6..0c6c22c9 100644 --- a/expcore/datastore.lua +++ b/expcore/datastore.lua @@ -9,123 +9,290 @@ local Datastore = {} global.datastores = Datastores Event.on_load(function() Datastores = global.datastores + for _, datastore in pairs(Datastores) do + setmetatable(datastore, DatastoreManager.metatable) + end end) ----- Datastore Manager ----- +-- @section datastoreManager + +--- Metatable used on datastores +DatastoreManager.metatable = { + __newidnex = function(_, _, _) error('Datastore can not be modified', 2) end, + __call = function(self, ...) return self:get(...) end, + __index = Datastore +} --- Make a new datastore -function DatastoreManager.connect(tableName, saveToDisk, propagateChanges) +function DatastoreManager.connect(tableName, saveToDisk, autoSave, propagateChanges) + if Datastores[tableName] then return Datastores[tableName] end + local new_datastore = { + name = tableName, + auto_save = autoSave or false, + save_to_disk = saveToDisk or false, + propagate_changes = propagateChanges or false, + serializer = false, + combined = false, + events = {}, + data = {} + } + + Datastores[tableName] = new_datastore + return setmetatable(new_datastore, DatastoreManager.metatable) end --- Make a new datastore that is contained within another function DatastoreManager.combine(datastore, subTableName) - + local new_datastore = DatastoreManager.connect(subTableName) + new_datastore.serializer = datastore.serializer + new_datastore.auto_save = datastore.auto_save + new_datastore.combined = datastore + return new_datastore end --- Ingest the result from a request +local function ingest_error(err) print('Datastore ingest error, Unable to parse json:', err) end function DatastoreManager.ingest(action, tableName, key, valueJson) + local datastore = assert(Datastores[tableName], 'Datastore ingest error, Datastore not found '..tostring(tableName)) + assert(type(action) == 'string', 'Datastore ingest error, Action is not a string got: '..type(action)) + assert(type(key) == 'string', 'Datastore ingest error, Key is not a string got: '..type(key)) + + if action == 'remove' then + datastore:raw_set(key) + + elseif action == 'message' then + local success, value = xpcall(game.json_to_table, ingest_error, valueJson) + if not success or value == nil then return end + datastore:raise_event('on_message', key, value) + + elseif action == 'propagate' then + local success, value = xpcall(game.json_to_table, ingest_error, valueJson) + if not success or value == nil then return end + value = datastore:raise_event('on_received', key, value) + datastore:set(key, value) + + end end --- Commonly used serializer, returns the objects name function DatastoreManager.name_serializer(rawKey) - + return rawKey.name end ----- Datastore ----- +-- @section datastore + +--- Internal, Get the data following combine logic +function Datastore:raw_get(key, isTable) + if self.combined then + local data = self.combined:raw_get(key, true) + if data[self.name] == nil and isTable then + data[self.name] = {} + end + return data[self.name] + else + if self.data[key] == nil and isTable then + self.data[key] = {} + end + return self.data[key] + end +end + +--- Internal, Set the data following combine logic +function Datastore:raw_set(key, value) + if self.combined then + local data = self.combined:raw_get(key, true) + data[self.name] = value + else + self.data[key] = value + end +end + +--- Internal, return the serialized key +local function serialize_error(err) error('An error ocurred in a datastore serializer: '..err) end +function Datastore:serialize(rawKey) + if type(rawKey) == 'string' then return rawKey end + assert(self.serializer, 'Datastore does not have a serializer and received non string key') + local success, key = xpcall(self.serializer, serialize_error, rawKey) + return success and key or nil +end + +--- Internal, writes an event to the output file to be saved and/or propagated +function Datastore:write_action(action, key, value) + local data = {action, self.name, '"'..key..'"'} + if value ~= nil then + data[4] = type(value) == 'table' and '"'..game.table_to_json(value)..'"' or '"'..tostring(value)..'"' + end + game.write_file('datastore.pipe', table.concat(data, ' ')..'\n', true, 0) +end --- Request a value from an external source function Datastore:request(key) - + if self.combined then return self.combined:request(key) end + key = self:serialize(key) + self:write_action('request', key) end --- Save a value to an external source function Datastore:save(key) - + if self.combined then return self.combined:save(key) end + if not self.save_to_disk then return end + key = self:serialize(key) + local value = self:raw_get(key) + value = self:raise_event('on_save', key, value) + local action = self.propagateChanges and 'propagate' or 'save' + self:write_action(action, key, value) end --- Save a value to an external source and remove locally function Datastore:unload(key) - -end - ---- Remove a value locally and on the external source -function Datastore:remove(key) - -end - ---- Get a value from local storage -function Datastore:get(key, default) - -end - ---- Set a value in local storage -function Datastore:set(key, value) - -end - ---- Increment the value in local storage, only works for number values -function Datastore:increment(key, delta) - -end - ---- Use a callback function to update the value locally -function Datastore:update(key, callback) - + if self.combined then return self.combined:unload(key) end + key = self:serialize(key) + self:save(key) + self:raw_set(key) end --- Use to send a message over the connection, works regardless of saveToDisk and propagateChanges function Datastore:message(key, message) + key = self:serialize(key) + self:write_action('message', key, message) +end +--- Remove a value locally and on the external source, works regardless of propagateChanges +function Datastore:remove(key) + key = self:serialize(key) + self:raw_set(key) + self:write_action('remove', key) + if self.combined and self.combined.auto_save then return self.combined:save(key) end +end + +--- Get a value from local storage +function Datastore:get(key, default) + key = self:serialize(key) + local value = self:raw_get(key) + if value ~= nil then return value end + return table.deep_copy(default) +end + +--- Set a value in local storage +function Datastore:set(key, value) + key = self:serialize(key) + self:raw_set(key, value) + self:raise_event('on_update', key, value) + if self.auto_save then self:save(key) end + return value +end + +--- Increment the value in local storage, only works for number values +function Datastore:increment(key, delta) + key = self:serialize(key) + local value = self:raw_get(key) or 0 + return Datastore:set(key, value + (delta or 1)) +end + +--- Use a callback function to update the value locally +local function update_error(err) error('An error ocurred in datastore update: '..err, 2) end +function Datastore:update(key, callback) + key = self:serialize(key) + local value = self:raw_get(key) + local success, new_value = xpcall(callback, update_error, key, value) + if success and new_value ~= nil then + self:set(key, new_value) + else + self:raise_event('on_update', key, value) + if self.auto_save then self:save(key) end + end +end + +--- Used to filter elements from a table +local function filter_error(err) print('An error ocurred in a datastore filter:', err) end +local function filter(tbl, callback) + if not callback then return tbl end + local rtn = {} + for key, value in pairs(tbl) do + local success, add = xpcall(callback, filter_error, key, value) + if success and add then rtn[key] = value end + end + return rtn end --- Get all keys in the datastore, optional filter callback function Datastore:get_all(callback) - + if not self.combined then + return filter(self.data, callback) + else + local name = self.name + local data = self.combined:get_all() + for key, value in pairs(data) do + data[key] = value[name] + end + return filter(data, callback) + end end --- Save all the keys in the datastore, optional filter callback function Datastore:save_all(callback) - + local data = self:get_all(callback) + for key in pairs(data) do self:save(key) end end --- Unload all the keys in the datastore, optional filter callback function Datastore:unload_all(callback) - + local data = self:get_all(callback) + for key in pairs(data) do self:unload(key) end end --- Set a callback that will be used to serialize keys which aren't strings function Datastore:set_serializer(callback) - + assert(type(callback) == 'function', 'Callback must be a function') + self.serializer = callback end ----- Events ----- +-- @section events + +--- Raise a custom event on this datastore +local function event_error(err) print('An error ocurred in a datastore event handler:', err) end +function Datastore:raise_event(event_name, key, value) + local handlers = self.events[event_name] + if not handlers then return value end + for _, handler in ipairs(handlers) do + local success, new_value = xpcall(handler, event_error, key, value) + if success and new_value ~= nil then value = new_value end + end + return value +end + +--- Returns a function which will add a callback to an event +local function event_factory(event_name) + return function(self, callback) + assert(type(callback) == 'function', 'Handler must be a function') + local handlers = self.events[event_name] + if not handlers then + self.events[event_name] = { callback } + else + handlers[#handlers+1] = callback + end + end +end --- Register a callback that triggers only when data is received -function Datastore:on_received(callback) - -end +Datastore.on_received = event_factory('on_received') --- Register a callback that triggers before data is saved -function Datastore:on_save(callback) - -end +Datastore.on_save = event_factory('on_save') --- Register a callback that triggers before data is unloaded -function Datastore:on_unload(callback) - -end +Datastore.on_unload = event_factory('on_unload') --- Register a callback that triggers when a message is received -function Datastore:on_message(callback) - -end +Datastore.on_message = event_factory('on_message') --- Register a callback that triggers any time a value is changed -function Datastore:on_update(callback) - -end +Datastore.on_update = event_factory('on_update') ----- Module Return ----- return DatastoreManager \ No newline at end of file diff --git a/modules/commands/interface.lua b/modules/commands/interface.lua index a826d286..f5c8b12f 100644 --- a/modules/commands/interface.lua +++ b/modules/commands/interface.lua @@ -17,7 +17,8 @@ local interface_modules = { ['Roles']='expcore.roles', ['Store']='expcore.store', ['Gui']='expcore.gui', - ['Async']='expcore.async' + ['Async']='expcore.async', + ['Datastore']='expcore.datastore' } -- loads all the modules given in the above table