Anton Tarasenko
2 years ago
43 changed files with 5239 additions and 758 deletions
@ -0,0 +1,5 @@ |
|||||||
|
; Define all databases you want Acedia to use here. |
||||||
|
; For simply making default Acedia configs work, set `createIfMissing` below |
||||||
|
; to `true`. |
||||||
|
[Database LocalDatabase] |
||||||
|
createIfMissing=false |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,791 @@ |
|||||||
|
/** |
||||||
|
* Auxiliary object for simplifying working with databases. |
||||||
|
* Copyright 2023 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class DBConnection extends AcediaObject |
||||||
|
dependson(Database) |
||||||
|
dependson(DBCache); |
||||||
|
|
||||||
|
/** |
||||||
|
* # `DBConnection` |
||||||
|
* |
||||||
|
* Auxiliary object for simplifying working with databases. |
||||||
|
* `Database` class has a rather simple interface and there are several issues |
||||||
|
* that constantly arise when trying to use it: |
||||||
|
* |
||||||
|
* 1. If one tries to read/write data from/to specific location in |
||||||
|
* the database, then `JSONPointer` has to be kept in addition to |
||||||
|
* the `Database` reference at all times; |
||||||
|
* 2. One has to perform initial checks about whether database can even be |
||||||
|
* connected to, if at desired location there is a proper data |
||||||
|
* structure, etc.. If one also wants to start using data before |
||||||
|
* database's response or after its failure, then the same work of |
||||||
|
* duplication that data locally must be performed. |
||||||
|
* 3. Instead of immediate operations, database operations are delayed and |
||||||
|
* user has to handle their results asynchronously in separate methods. |
||||||
|
* |
||||||
|
* `DBConnection` takes care of these issues by providing you synchronous |
||||||
|
* methods for accessing cached version of the data at the given location that |
||||||
|
* is duplicated to the database as soon as possible (and even if its no longer |
||||||
|
* possible in the case of a failure). |
||||||
|
* `DBConnection` makes immediate changes on the local cache and reports |
||||||
|
* about possible failures with database later through signals `OnEditResult()` |
||||||
|
* (reports about success of writing operations) and `OnStateChanged()` |
||||||
|
* (reports about state changes of connected database, including complete |
||||||
|
* failures). |
||||||
|
* The only reading of database's values occurs at the moment of connecting |
||||||
|
* to it, after that all the data is read from the local cache. |
||||||
|
* Possible `DBConnection` states include: |
||||||
|
* |
||||||
|
* * `DBCS_Idle` - database was created, but not yet connected; |
||||||
|
* * `DBCS_Connecting` - `Connect()` method was successfully called, but |
||||||
|
* its result is still unknown; |
||||||
|
* * `DBCS_Connected` - database is connected and properly working; |
||||||
|
* * `DBCS_Disconnected` - database was manually disconnected and now |
||||||
|
* operates solely on the local cache. Once disconnected `DBConnection` |
||||||
|
* cannot be reconnected - create a new one instead. |
||||||
|
* * `DBCS_FaultyDatabase` - database is somehow faulty. Precise reason |
||||||
|
* can be found out with `GetError()` method: |
||||||
|
* |
||||||
|
* * `FDE_None` - no error has yet occurred; |
||||||
|
* * `FDE_CannotReadRootData` - root data couldn't be read from |
||||||
|
* the database, most likely because of the invalid `JSONPointer` |
||||||
|
* for the root value; |
||||||
|
* * `FDE_UnexpectedRootData` - root data was read from the database, |
||||||
|
* but has an unexpected format. `DBConnection` expects either |
||||||
|
* JSON object or array (can be specified which one) and this error |
||||||
|
* occurs if its not found at specified database's location; |
||||||
|
* * `FDE_Unknown` - database returned `DBR_InvalidDatabase` result |
||||||
|
* for one of the queries. This is likely to happen when database |
||||||
|
* is damaged. More precise details depend on the implementation, |
||||||
|
* but result boils down to database being unusable. |
||||||
|
* |
||||||
|
* ## Usage |
||||||
|
* |
||||||
|
* Usage is straightforward: |
||||||
|
* |
||||||
|
* 1. Initialize with appropriate database by calling `Initialize()`; |
||||||
|
* 2. Start connecting to it by calling `Connect()`; |
||||||
|
* 3. Use `ReadDataByJSON()`/`WriteDataByJSON()` to read/write into |
||||||
|
* the connected database; |
||||||
|
* |
||||||
|
* You can use it transparently even if database connection fails, but if you |
||||||
|
* need to handle such failure - connect to the `OnStateChanged()` signal for |
||||||
|
* tracking `DBCS_FaultyDatabase` state and to `OnEditResult()` for tracking |
||||||
|
* success of writing operations. |
||||||
|
* |
||||||
|
* ## Implementation |
||||||
|
* |
||||||
|
* The brunt of work is done by `DBCache` and most of the logic in this |
||||||
|
* class is for tracking state of the connection to the database and then |
||||||
|
* reporting these changes through its own signals. |
||||||
|
* The most notable hidden functionality is tracking requests by ID - |
||||||
|
* `DBConnection` makes each request with unique ID and then stores them inside |
||||||
|
* `requestIDs` (and in case of write requests - along with corresponding |
||||||
|
* `JSONPointer` inside `queuedPointers`). This is necessary because: |
||||||
|
* |
||||||
|
* 1. Even if `DBConnection` gets reallocated - as far as UnrealScript is |
||||||
|
* concerned it is still the same object, so the responses we no longer |
||||||
|
* care about will still arrive. Keeping track of the IDs that interest |
||||||
|
* us inside `requestIDs` allows us to filter out responses we no |
||||||
|
* longer care about; |
||||||
|
* 2. Tracking corresponding (to the IDs) `queuedPointers` also allow us |
||||||
|
* to know responses to which writing requests we've received. |
||||||
|
* |
||||||
|
* ## Remarks |
||||||
|
* |
||||||
|
* Currently `DBConnection` doesn't support important feature of *incrementing* |
||||||
|
* data that allows several sources to safely change the same value |
||||||
|
* asynchronously. We're skipping on it right now to save time as its not |
||||||
|
* really currently needed, however it will be added in the future. |
||||||
|
*/ |
||||||
|
|
||||||
|
enum DBConnectionState |
||||||
|
{ |
||||||
|
// `DBConnection` was created, but didn't yet attempt to connect |
||||||
|
// to database |
||||||
|
DBCS_Idle, |
||||||
|
// `DBConnection` is currently connecting |
||||||
|
DBCS_Connecting, |
||||||
|
// `DBConnection` has already connected without errors |
||||||
|
DBCS_Connected, |
||||||
|
// `DBConnection` was manually disconnected |
||||||
|
DBCS_Disconnected, |
||||||
|
// `DBConnection` was disconnected because of the database error, |
||||||
|
// @see `FaultyDatabaseError` for more. |
||||||
|
DBCS_FaultyDatabase |
||||||
|
}; |
||||||
|
// Current connection state |
||||||
|
var private DBConnectionState currentState; |
||||||
|
|
||||||
|
enum FaultyDatabaseError |
||||||
|
{ |
||||||
|
// No error has occurred yet |
||||||
|
FDE_None, |
||||||
|
// Root data isn't available |
||||||
|
FDE_CannotReadRootData, |
||||||
|
// Root data has incorrect format |
||||||
|
FDE_UnexpectedRootData, |
||||||
|
// Some internal error in database has occurred |
||||||
|
FDE_Unknown |
||||||
|
}; |
||||||
|
// Reason for why current state is in `DBCS_FaultyDatabase` state; |
||||||
|
// `FDE_None` if `DBConnection` is in any other state. |
||||||
|
var private FaultyDatabaseError dbFailureReason; |
||||||
|
// Keeps track whether root value read from the database was of the correct |
||||||
|
// type. Only relevant after database tried connecting (i.e. it is in states |
||||||
|
// `DBCS_Connected`, `DBCS_Disconnected` or `DBCS_FaultyDatabase`). |
||||||
|
// This variable helps us determine whether error should be |
||||||
|
// `FDE_CannotReadRootData` or `FDE_UnexpectedRootData`. |
||||||
|
var private bool rootIsOfExpectedType; |
||||||
|
|
||||||
|
// `Database` + `JSONPointer` combo that point at the data we want to |
||||||
|
// connect to |
||||||
|
var private Database dbInstance; |
||||||
|
var private JSONPointer rootPointer; |
||||||
|
// Local, cached version of that data |
||||||
|
var private DBCache localCache; |
||||||
|
|
||||||
|
// This is basically an array of (`int`, `JSONPointer`) pairs for tracking |
||||||
|
// database requests of interest |
||||||
|
var private array<int> requestIDs; |
||||||
|
var private array<JSONPointer> queuedPointers; |
||||||
|
// Next usable ID. `DBConnection` is expected to always use unique IDs. |
||||||
|
var private int nextRequestID; |
||||||
|
|
||||||
|
|
||||||
|
var private DBConnection_StateChanged_Signal onStateChangedSignal; |
||||||
|
var private DBConnection_EditResult_Signal onEditResultSignal; |
||||||
|
|
||||||
|
var private LoggerAPI.Definition errDoubleInitialization; |
||||||
|
|
||||||
|
/** |
||||||
|
* Signal that will be emitted whenever `DBConnection` changes state. |
||||||
|
* List of the available states: |
||||||
|
* |
||||||
|
* * `DBCS_Idle` - database was created, but not yet connected; |
||||||
|
* * `DBCS_Connecting` - `Connect()` method was successfully called, but |
||||||
|
* its result is still unknown; |
||||||
|
* * `DBCS_Connected` - database is connected and properly working; |
||||||
|
* * `DBCS_Disconnected` - database was manually disconnected and now |
||||||
|
* operates solely on the local cache. Once disconnected `DBConnection` |
||||||
|
* cannot be reconnected - create a new one instead. |
||||||
|
* * `DBCS_FaultyDatabase` - database is somehow faulty. Precise reason |
||||||
|
* can be found out with `GetError()` method. |
||||||
|
* |
||||||
|
* This method *is not* called when `DBConnection` is deallocated. |
||||||
|
* |
||||||
|
* [Signature] |
||||||
|
* void <slot>( |
||||||
|
* DBConnection instance, |
||||||
|
* DBConnectionState oldState, |
||||||
|
* DBConnectionState newState) |
||||||
|
* |
||||||
|
* @param instance Instance of the `DBConnection` that has changed state. |
||||||
|
* @param oldState State it was previously in. |
||||||
|
* @param oldState New state. |
||||||
|
*/ |
||||||
|
/* SIGNAL */ |
||||||
|
public final function DBConnection_StateChanged_Slot OnStateChanged( |
||||||
|
AcediaObject receiver) |
||||||
|
{ |
||||||
|
return DBConnection_StateChanged_Slot(onStateChangedSignal |
||||||
|
.NewSlot(receiver)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Signal that will be emitted whenever `DBConnection` receives response from |
||||||
|
* connected database about success of writing operation. |
||||||
|
* |
||||||
|
* Responses to old requests can still be received even if database got |
||||||
|
* disconnected. |
||||||
|
* |
||||||
|
* Any emissions of this signal when the database's state is `DBCS_Connecting` |
||||||
|
* correspond to reapplying edits made prior connection was established. |
||||||
|
* |
||||||
|
* [Signature] |
||||||
|
* void <slot>(JSONPointer editLocation, bool isSuccessful) |
||||||
|
* |
||||||
|
* @param editLocation Location of the writing operation this is |
||||||
|
* a response to. |
||||||
|
* @param isSuccessful Whether writing operation ended in the success. |
||||||
|
*/ |
||||||
|
/* SIGNAL */ |
||||||
|
public final function DBConnection_EditResult_Slot OnEditResult( |
||||||
|
AcediaObject receiver) |
||||||
|
{ |
||||||
|
return DBConnection_EditResult_Slot(onEditResultSignal.NewSlot(receiver)); |
||||||
|
} |
||||||
|
|
||||||
|
protected function Constructor() |
||||||
|
{ |
||||||
|
localCache = DBCache(_.memory.Allocate(class'DBCache')); |
||||||
|
onStateChangedSignal = DBConnection_StateChanged_Signal( |
||||||
|
_.memory.Allocate(class'DBConnection_StateChanged_Signal')); |
||||||
|
onEditResultSignal = DBConnection_EditResult_Signal( |
||||||
|
_.memory.Allocate(class'DBConnection_EditResult_Signal')); |
||||||
|
} |
||||||
|
|
||||||
|
protected function Finalizer() |
||||||
|
{ |
||||||
|
rootIsOfExpectedType = false; |
||||||
|
currentState = DBCS_Idle; |
||||||
|
_.memory.Free(dbInstance); |
||||||
|
_.memory.Free(rootPointer); |
||||||
|
_.memory.Free(localCache); |
||||||
|
dbInstance = none; |
||||||
|
rootPointer = none; |
||||||
|
localCache = none; |
||||||
|
_.memory.FreeMany(queuedPointers); |
||||||
|
queuedPointers.length = 0; |
||||||
|
requestIDs.length = 0; |
||||||
|
// Free signals |
||||||
|
_.memory.Free(onStateChangedSignal); |
||||||
|
_.memory.Free(onEditResultSignal); |
||||||
|
onStateChangedSignal = none; |
||||||
|
onEditResultSignal = none; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Initializes `DBConnection` with database and location to which it must be |
||||||
|
* connected. |
||||||
|
* |
||||||
|
* For the initialization to be successful `DBConnection` must not yet be |
||||||
|
* initialized and `initDatabase` be not `none`. |
||||||
|
* |
||||||
|
* To check whether caller `DBConnection` is initialized |
||||||
|
* @see `IsInitialized()`. |
||||||
|
* |
||||||
|
* @param initDatabase Database with data we want to connect to. |
||||||
|
* @param initRootPointer Location of said data in the given database. |
||||||
|
* If `none` is specified, uses root object of the database. |
||||||
|
* @return `true` if initialization was successful and `false` otherwise. |
||||||
|
*/ |
||||||
|
public final function bool Initialize( |
||||||
|
Database initDatabase, |
||||||
|
optional JSONPointer initRootPointer) |
||||||
|
{ |
||||||
|
if (IsInitialized()) return false; |
||||||
|
if (initDatabase == none) return false; |
||||||
|
if (!initDatabase.IsAllocated()) return false; |
||||||
|
|
||||||
|
dbInstance = initDatabase; |
||||||
|
dbInstance.NewRef(); |
||||||
|
if (initRootPointer != none) { |
||||||
|
rootPointer = initRootPointer.Copy(); |
||||||
|
} |
||||||
|
else { |
||||||
|
rootPointer = _.json.Pointer(); |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Reads data from the `DBConnection` at the location defined by the given |
||||||
|
* `JSONPointer`. |
||||||
|
* |
||||||
|
* If data was initialized with non-empty location for the root data, then |
||||||
|
* actual returned data's location in the database is defined by appending |
||||||
|
* given `pointer` to that root pointer. |
||||||
|
* |
||||||
|
* Data is actually always read from the local cache and, therefore, we can |
||||||
|
* read data we've written via `DBConnection` even without actually connecting |
||||||
|
* to the database. |
||||||
|
* |
||||||
|
* @param pointer Location from which to read the data. |
||||||
|
* @return Data recorded for the given `JSONPointer`. `none` if it is missing. |
||||||
|
*/ |
||||||
|
public final function AcediaObject ReadDataByJSON(JSONPointer pointer) |
||||||
|
{ |
||||||
|
return localCache.Read(pointer); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Writes given data into the `DBConnection` at the location defined by |
||||||
|
* the given `JSONPointer`. |
||||||
|
* |
||||||
|
* If data was initialized with non-empty location for the root data, then |
||||||
|
* actual location for writing data in the database is defined by appending |
||||||
|
* given `pointer` to that root pointer. |
||||||
|
* |
||||||
|
* Data is actually always also written into the local cache, even when |
||||||
|
* there is no connection to the database. Once connection is made - all valid |
||||||
|
* changes will be duplicated into it. |
||||||
|
* Success of failure of actually making changes into the database can be |
||||||
|
* tracked with `OnEditResult()` signal. |
||||||
|
* |
||||||
|
* This operation also returns immediate indication of whether it has |
||||||
|
* failed *locally*. This can happen when trying to perform operation |
||||||
|
* impossible for the local cache. For example, we cannot write any data at |
||||||
|
* location "/a/b/c" for the JSON object "{"a":45.6}". |
||||||
|
* If operation ended in failure locally, then change to database won't |
||||||
|
* even be attempted. |
||||||
|
* |
||||||
|
* @param pointer Location into which to write the data. |
||||||
|
* @param data Data to write into the connection. |
||||||
|
* @return `true` on success and `false` on failure. `true` is required for |
||||||
|
* the writing database request to be made. |
||||||
|
*/ |
||||||
|
public final function bool WriteDataByJSON( |
||||||
|
JSONPointer pointer, |
||||||
|
AcediaObject data) |
||||||
|
{ |
||||||
|
if (pointer == none) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
if (localCache.Write(pointer, data)) |
||||||
|
{ |
||||||
|
ModifyDataInDatabase(pointer, data, false); |
||||||
|
return true; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Increments given data into the `DBConnection` at the location defined by |
||||||
|
* the given `JSONPointer`. |
||||||
|
* |
||||||
|
* If data was initialized with non-empty location for the root data, then |
||||||
|
* actual location for incrementing data in the database is defined by |
||||||
|
* appending given `pointer` to that root pointer. |
||||||
|
* |
||||||
|
* Data is actually always also incremented into the local cache, even when |
||||||
|
* there is no connection to the database. Once connection is made - all valid |
||||||
|
* changes will be duplicated into it. |
||||||
|
* Success of failure of actually making changes into the database can be |
||||||
|
* tracked with `OnEditResult()` signal. |
||||||
|
* |
||||||
|
* This operation also returns immediate indication of whether it has |
||||||
|
* failed *locally*. This can happen when trying to perform operation |
||||||
|
* impossible for the local cache. For example, we cannot increment any data at |
||||||
|
* location "/a/b/c" for the JSON object "{"a":45.6}". |
||||||
|
* If operation ended in failure locally, then change to database won't |
||||||
|
* even be attempted. |
||||||
|
* |
||||||
|
* @param pointer Location at which to increment the data. |
||||||
|
* @param data Data with which to increment value inside the connection. |
||||||
|
* @return `true` on success and `false` on failure. `true` is required for |
||||||
|
* the incrementing database request to be made. |
||||||
|
*/ |
||||||
|
public final function bool IncrementDataByJSON( |
||||||
|
JSONPointer pointer, |
||||||
|
AcediaObject data) |
||||||
|
{ |
||||||
|
if (pointer == none) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
if (localCache.Increment(pointer, data)) |
||||||
|
{ |
||||||
|
ModifyDataInDatabase(pointer, data, true); |
||||||
|
return true; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Removes data from the `DBConnection` at the location defined by the given |
||||||
|
* `JSONPointer`. |
||||||
|
* |
||||||
|
* If data was initialized with non-empty location for the root data, then |
||||||
|
* actual location at which to remove data in the database is defined by |
||||||
|
* appending given `pointer` to that root pointer. |
||||||
|
* |
||||||
|
* Data is actually always also removed from the local cache, even when |
||||||
|
* there is no connection to the database. Once connection is made - all valid |
||||||
|
* changes will be duplicated into it. |
||||||
|
* Success of failure of actually making changes into the database can be |
||||||
|
* tracked with `OnEditResult()` signal. |
||||||
|
* |
||||||
|
* This operation also returns immediate indication of whether it has |
||||||
|
* failed *locally*. |
||||||
|
* If operation ended in failure locally, then change to database won't |
||||||
|
* even be attempted. |
||||||
|
* |
||||||
|
* @param pointer Location at which to remove data. |
||||||
|
* @return `true` on success and `false` on failure. `true` is required for |
||||||
|
* the removal database request to be made. |
||||||
|
*/ |
||||||
|
public final function bool RemoveDataByJSON(JSONPointer pointer) |
||||||
|
{ |
||||||
|
if (pointer == none) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
if (localCache.Remove(pointer)) |
||||||
|
{ |
||||||
|
RemoveDataInDatabase(pointer); |
||||||
|
return true; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
private final function ModifyDataInDatabase( |
||||||
|
JSONPointer pointer, |
||||||
|
AcediaObject data, |
||||||
|
bool increment) |
||||||
|
{ |
||||||
|
local JSONPointer dataPointer; |
||||||
|
|
||||||
|
if (currentState != DBCS_Connected) { |
||||||
|
return; |
||||||
|
} |
||||||
|
dataPointer = rootPointer.Copy(); |
||||||
|
dataPointer.Append(pointer); |
||||||
|
// `dataPointer` is consumed by `RegisterNextRequestID()` method |
||||||
|
if (increment) |
||||||
|
{ |
||||||
|
dbInstance |
||||||
|
.IncrementData( |
||||||
|
dataPointer, |
||||||
|
data, |
||||||
|
RegisterNextRequestID(dataPointer)) |
||||||
|
.connect = EditDataHandler; |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
dbInstance |
||||||
|
.WriteData(dataPointer, data, RegisterNextRequestID(dataPointer)) |
||||||
|
.connect = EditDataHandler; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private final function RemoveDataInDatabase(JSONPointer pointer) |
||||||
|
{ |
||||||
|
local JSONPointer dataPointer; |
||||||
|
|
||||||
|
if (currentState != DBCS_Connected) { |
||||||
|
return; |
||||||
|
} |
||||||
|
dataPointer = rootPointer.Copy(); |
||||||
|
dataPointer.Append(pointer); |
||||||
|
// `dataPointer` is consumed by `RegisterNextRequestID()` method |
||||||
|
dbInstance |
||||||
|
.RemoveData(dataPointer, RegisterNextRequestID(dataPointer)) |
||||||
|
.connect = EditDataHandler; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Checks caller `DBConnection` was successfully initialized. |
||||||
|
* |
||||||
|
* @return `true` if caller `DBConnection` was initialized and `false` |
||||||
|
* otherwise. |
||||||
|
*/ |
||||||
|
public final function bool IsInitialized() |
||||||
|
{ |
||||||
|
return (dbInstance != none); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns current state of the connection of `DBConnection` to the database |
||||||
|
* it was initialized with. |
||||||
|
* |
||||||
|
* @see `OnStateChanged()` for more information about connection states. |
||||||
|
* @return Current connection state. |
||||||
|
*/ |
||||||
|
public final function DBConnectionState GetConnectionState() |
||||||
|
{ |
||||||
|
return currentState; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Checks whether caller `DBConnection` is currently connected without errors |
||||||
|
* to the database it was initialized with. |
||||||
|
* |
||||||
|
* @return `true` if caller `DBConnection` is connected to the database and |
||||||
|
* `false` otherwise. |
||||||
|
*/ |
||||||
|
public final function bool IsConnected() |
||||||
|
{ |
||||||
|
return (currentState == DBCS_Connected); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Checks whether an error has occurred with connection to the database. |
||||||
|
* |
||||||
|
* `DBConnection` can get disconnected from database manually and without |
||||||
|
* any errors, so, if you simply want to check whether connection exists, |
||||||
|
* @see `IsConnected()` or @see `GetConnectionState()`. |
||||||
|
* To obtain more detailed information @see `GetError()`. |
||||||
|
* |
||||||
|
* @return `true` if there were no error thus far and `false` otherwise. |
||||||
|
*/ |
||||||
|
public final function bool IsOk() |
||||||
|
{ |
||||||
|
return (dbFailureReason == FDE_None); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns error that has occurred during connection. |
||||||
|
* |
||||||
|
* @return Error that has occurred during connection to the database, |
||||||
|
* `FDE_None` if there was no errors. |
||||||
|
*/ |
||||||
|
public final function FaultyDatabaseError GetError() |
||||||
|
{ |
||||||
|
return dbFailureReason; |
||||||
|
} |
||||||
|
|
||||||
|
private final function ChangeState(DBConnectionState newState) |
||||||
|
{ |
||||||
|
local DBConnectionState oldState; |
||||||
|
|
||||||
|
oldState = currentState; |
||||||
|
currentState = newState; |
||||||
|
onStateChangedSignal.Emit(self, oldState, newState); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Attempts connection to the database caller `DBConnection` was initialized |
||||||
|
* with. Result isn't immediate and can be tracked with `OnStateChanged()` |
||||||
|
* signal. |
||||||
|
* |
||||||
|
* Connection checks whether data by the initialization address can be read and |
||||||
|
* has proper type (by default JSON object, but JSON array can be used |
||||||
|
* instead). |
||||||
|
* |
||||||
|
* Whether connection is successfully established isn't known at the moment |
||||||
|
* this function returns. User `OnStateChanged()` to track that. |
||||||
|
* |
||||||
|
* @param expectArray Set this to `true` if the expected root value is |
||||||
|
* JSON array. |
||||||
|
*/ |
||||||
|
public final function Connect(optional bool expectArray) |
||||||
|
{ |
||||||
|
local Collection incrementObject; |
||||||
|
|
||||||
|
if (!IsInitialized()) return; |
||||||
|
if (currentState != DBCS_Idle) return; |
||||||
|
|
||||||
|
if (expectArray) { |
||||||
|
incrementObject = _.collections.EmptyArrayList(); |
||||||
|
} |
||||||
|
else { |
||||||
|
incrementObject = _.collections.EmptyHashTable(); |
||||||
|
} |
||||||
|
dbInstance.IncrementData( |
||||||
|
rootPointer, |
||||||
|
incrementObject, |
||||||
|
RegisterNextRequestID()).connect = IncrementCheckHandler; |
||||||
|
incrementObject.FreeSelf(); |
||||||
|
// Copy of the `rootPointer` is consumed by `RegisterNextRequestID()` |
||||||
|
// method |
||||||
|
dbInstance.ReadData(rootPointer,, RegisterNextRequestID(rootPointer.Copy())) |
||||||
|
.connect = InitialLoadingHandler; |
||||||
|
ChangeState(DBCS_Connecting); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Disconnects `DBConnection` from its database, preventing its further |
||||||
|
* updates. |
||||||
|
* |
||||||
|
* Database can only be disconnected if connection was at least initialized |
||||||
|
* (state isn't `DBCS_Idle`) and no error has yet occurred (state isn't |
||||||
|
* `DBCS_FaultyDatabase`). |
||||||
|
* |
||||||
|
* @return `true` if `DBConnection` was disconnected from the database and |
||||||
|
* `false` otherwise (including if it already was disconnected). |
||||||
|
*/ |
||||||
|
public final function bool Disconnect() |
||||||
|
{ |
||||||
|
if ( currentState != DBCS_FaultyDatabase |
||||||
|
&& currentState != DBCS_Idle |
||||||
|
&& currentState != DBCS_Disconnected) |
||||||
|
{ |
||||||
|
ChangeState(DBCS_Disconnected); |
||||||
|
return true; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
private final function int RegisterNextRequestID( |
||||||
|
optional /*take*/ JSONPointer relativePointer) |
||||||
|
{ |
||||||
|
if (relativePointer != none) { |
||||||
|
queuedPointers[queuedPointers.length] = relativePointer; |
||||||
|
} |
||||||
|
else { |
||||||
|
queuedPointers[queuedPointers.length] = _.json.Pointer(); |
||||||
|
} |
||||||
|
requestIDs[requestIDs.length] = nextRequestID; |
||||||
|
nextRequestID += 1; |
||||||
|
return (nextRequestID - 1); |
||||||
|
} |
||||||
|
|
||||||
|
private final function JSONPointer FetchRequestPointer(int requestID) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local JSONPointer result; |
||||||
|
|
||||||
|
while (i < requestIDs.length) |
||||||
|
{ |
||||||
|
if (requestIDs[i] < requestID) |
||||||
|
{ |
||||||
|
// We receive all requests in order, so if `requestID` is higher |
||||||
|
// than IDs of some other requests - it means that they are older, |
||||||
|
// lost requests |
||||||
|
_.memory.Free(queuedPointers[i]); |
||||||
|
queuedPointers.Remove(i, 1); |
||||||
|
requestIDs.Remove(i, 1); |
||||||
|
} |
||||||
|
if (requestIDs[i] == requestID) |
||||||
|
{ |
||||||
|
result = queuedPointers[i]; |
||||||
|
queuedPointers.Remove(i, 1); |
||||||
|
requestIDs.Remove(i, 1); |
||||||
|
return result; |
||||||
|
} |
||||||
|
i += 1; |
||||||
|
} |
||||||
|
return none; |
||||||
|
} |
||||||
|
|
||||||
|
private final function bool FetchIfRequestStillValid(int requestID) |
||||||
|
{ |
||||||
|
local JSONPointer result; |
||||||
|
|
||||||
|
result = FetchRequestPointer(requestID); |
||||||
|
if (result != none) |
||||||
|
{ |
||||||
|
_.memory.Free(result); |
||||||
|
return true; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
private final function IncrementCheckHandler( |
||||||
|
Database.DBQueryResult result, |
||||||
|
Database source, |
||||||
|
int requestID) |
||||||
|
{ |
||||||
|
if (!FetchIfRequestStillValid(requestID)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
// If we could successfully increment value with appropriate JSON value, |
||||||
|
// then its type is correct |
||||||
|
rootIsOfExpectedType = (result == DBR_Success); |
||||||
|
} |
||||||
|
|
||||||
|
private final function InitialLoadingHandler( |
||||||
|
Database.DBQueryResult result, |
||||||
|
/*take*/ AcediaObject data, |
||||||
|
Database source, |
||||||
|
int requestID) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local array<DBCache.PendingEdit> completedEdits; |
||||||
|
|
||||||
|
if (!FetchIfRequestStillValid(requestID)) |
||||||
|
{ |
||||||
|
_.memory.Free(data); |
||||||
|
return; |
||||||
|
} |
||||||
|
if (HandleInitializationError(result)) |
||||||
|
{ |
||||||
|
_.memory.Free(data); |
||||||
|
return; |
||||||
|
} |
||||||
|
completedEdits = localCache.SetRealData(data); |
||||||
|
for (i = 0; i < completedEdits.length; i += 1) |
||||||
|
{ |
||||||
|
if (completedEdits[i].successful) |
||||||
|
{ |
||||||
|
if (completedEdits[i].type == DBCET_Remove) { |
||||||
|
RemoveDataInDatabase(completedEdits[i].location); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
ModifyDataInDatabase( |
||||||
|
completedEdits[i].location, |
||||||
|
completedEdits[i].data, |
||||||
|
completedEdits[i].type == DBCET_Increment); |
||||||
|
} |
||||||
|
} |
||||||
|
else { |
||||||
|
onEditResultSignal.Emit(completedEdits[i].location, false); |
||||||
|
} |
||||||
|
_.memory.Free(completedEdits[i].location); |
||||||
|
_.memory.Free(completedEdits[i].data); |
||||||
|
} |
||||||
|
_.memory.Free(data); |
||||||
|
ChangeState(DBCS_Connected); |
||||||
|
} |
||||||
|
|
||||||
|
// Return `true` if further initialization must be stopped. |
||||||
|
private final function bool HandleInitializationError( |
||||||
|
Database.DBQueryResult result) |
||||||
|
{ |
||||||
|
// Get disconnected before even response has even arrived |
||||||
|
if (currentState == DBCS_Disconnected) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
if (currentState == DBCS_Connected) |
||||||
|
{ |
||||||
|
_.logger.Auto(errDoubleInitialization).Arg(rootPointer.ToText()); |
||||||
|
return true; |
||||||
|
} |
||||||
|
if (result == DBR_InvalidDatabase) |
||||||
|
{ |
||||||
|
dbFailureReason = FDE_Unknown; |
||||||
|
ChangeState(DBCS_FaultyDatabase); |
||||||
|
return true; |
||||||
|
} |
||||||
|
if (result != DBR_Success) |
||||||
|
{ |
||||||
|
dbFailureReason = FDE_CannotReadRootData; |
||||||
|
ChangeState(DBCS_FaultyDatabase); |
||||||
|
return true; |
||||||
|
} |
||||||
|
if (!rootIsOfExpectedType) |
||||||
|
{ |
||||||
|
dbFailureReason = FDE_UnexpectedRootData; |
||||||
|
ChangeState(DBCS_FaultyDatabase); |
||||||
|
return true; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
private final function EditDataHandler( |
||||||
|
Database.DBQueryResult result, |
||||||
|
Database source, |
||||||
|
int requestID) |
||||||
|
{ |
||||||
|
local JSONPointer relatedPointer; |
||||||
|
|
||||||
|
relatedPointer = FetchRequestPointer(requestID); |
||||||
|
if (relatedPointer == none) { |
||||||
|
return; |
||||||
|
} |
||||||
|
if (result == DBR_InvalidDatabase) |
||||||
|
{ |
||||||
|
dbFailureReason = FDE_Unknown; |
||||||
|
ChangeState(DBCS_FaultyDatabase); |
||||||
|
relatedPointer.FreeSelf(); |
||||||
|
return; |
||||||
|
} |
||||||
|
if (result == DBR_Success) { |
||||||
|
onEditResultSignal.Emit(relatedPointer, true); |
||||||
|
} |
||||||
|
else { |
||||||
|
onEditResultSignal.Emit(relatedPointer, false); |
||||||
|
} |
||||||
|
relatedPointer.FreeSelf(); |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
errDoubleInitialization = (l=LOG_Error,m="`DBConnection` connected to \"%1\" was double-initialized. This SHOULD NOT happen. Please report this bug.") |
||||||
|
} |
@ -0,0 +1,40 @@ |
|||||||
|
/** |
||||||
|
* Signal class for `DBConnections`'s `OnEditResult()` signal. |
||||||
|
* Copyright 2023 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class DBConnection_EditResult_Signal extends Signal |
||||||
|
dependson(DBConnection); |
||||||
|
|
||||||
|
public final function Emit(JSONPointer editLocation, bool isSuccessful) |
||||||
|
{ |
||||||
|
local Slot nextSlot; |
||||||
|
StartIterating(); |
||||||
|
nextSlot = GetNextSlot(); |
||||||
|
while (nextSlot != none) |
||||||
|
{ |
||||||
|
DBConnection_EditResult_Slot(nextSlot) |
||||||
|
.connect(editLocation, isSuccessful); |
||||||
|
nextSlot = GetNextSlot(); |
||||||
|
} |
||||||
|
CleanEmptySlots(); |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
relatedSlotClass = class'DBConnection_EditResult_Slot' |
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
/** |
||||||
|
* Slot class for `DBConnections`'s `OnEditResult()` signal. |
||||||
|
* Copyright 2023 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class DBConnection_EditResult_Slot extends Slot |
||||||
|
dependson(DBConnection); |
||||||
|
|
||||||
|
delegate connect(JSONPointer editLocation, bool isSuccessful) |
||||||
|
{ |
||||||
|
DummyCall(); |
||||||
|
} |
||||||
|
|
||||||
|
protected function Constructor() |
||||||
|
{ |
||||||
|
connect = none; |
||||||
|
} |
||||||
|
|
||||||
|
protected function Finalizer() |
||||||
|
{ |
||||||
|
super.Finalizer(); |
||||||
|
connect = none; |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
/** |
||||||
|
* Signal class for `DBConnections`'s `OnStatusChanged()` signal. |
||||||
|
* Copyright 2023 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class DBConnection_StateChanged_Signal extends Signal |
||||||
|
dependson(DBConnection); |
||||||
|
|
||||||
|
public final function Emit( |
||||||
|
DBConnection instance, |
||||||
|
DBConnection.DBConnectionState oldState, |
||||||
|
DBConnection.DBConnectionState newState) |
||||||
|
{ |
||||||
|
local Slot nextSlot; |
||||||
|
StartIterating(); |
||||||
|
nextSlot = GetNextSlot(); |
||||||
|
while (nextSlot != none) |
||||||
|
{ |
||||||
|
DBConnection_StateChanged_Slot(nextSlot) |
||||||
|
.connect(instance, oldState, newState); |
||||||
|
nextSlot = GetNextSlot(); |
||||||
|
} |
||||||
|
CleanEmptySlots(); |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
relatedSlotClass = class'DBConnection_StateChanged_Slot' |
||||||
|
} |
@ -0,0 +1,44 @@ |
|||||||
|
/** |
||||||
|
* Slot class for `DBConnections`'s `OnStatusChanged()` signal. |
||||||
|
* Copyright 2023 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class DBConnection_StateChanged_Slot extends Slot |
||||||
|
dependson(DBConnection); |
||||||
|
|
||||||
|
delegate connect( |
||||||
|
DBConnection instance, |
||||||
|
DBConnection.DBConnectionState oldState, |
||||||
|
DBConnection.DBConnectionState newState) |
||||||
|
{ |
||||||
|
DummyCall(); |
||||||
|
} |
||||||
|
|
||||||
|
protected function Constructor() |
||||||
|
{ |
||||||
|
connect = none; |
||||||
|
} |
||||||
|
|
||||||
|
protected function Finalizer() |
||||||
|
{ |
||||||
|
super.Finalizer(); |
||||||
|
connect = none; |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
} |
@ -0,0 +1,389 @@ |
|||||||
|
/** |
||||||
|
* Set of tests for `DBConnection` and `DBCache` classes. |
||||||
|
* Copyright 2023 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class TEST_DBConnection extends TestCase |
||||||
|
abstract; |
||||||
|
|
||||||
|
var DBCache cache; |
||||||
|
|
||||||
|
protected static function TESTS() |
||||||
|
{ |
||||||
|
Context("Testing how `DBCache` handles data writes and reads when" |
||||||
|
@ "the actual database's data is yet to be downloaded."); |
||||||
|
Test_DBCache_BasicWriteRead_DuringLoading(); |
||||||
|
Test_DBCache_AllOperations_DuringLoading(); |
||||||
|
Context("Testing how `DBCache` handles applying pending writes after real" |
||||||
|
@ "data was set."); |
||||||
|
Test_DBCache_ApplyingPendingEdits(); |
||||||
|
Test_DBCache_ApplyingPendingEdit_AllOperations(); |
||||||
|
Context("Testing how `DBCache` handles data writes and reads when" |
||||||
|
@ "the actual database's data is already setup."); |
||||||
|
Test_DBCache_BasicWriteRead_AfterLoading(); |
||||||
|
Test_DBCache_AllOperations_AfterLoading(); |
||||||
|
} |
||||||
|
|
||||||
|
/* Creates following object: |
||||||
|
{ |
||||||
|
"A": "simpleValue", |
||||||
|
"B": { |
||||||
|
"A": [true, { |
||||||
|
"A": "simpleValue", |
||||||
|
"B": 11.12, |
||||||
|
"": [true, null, "huh"] |
||||||
|
}, "huh"], |
||||||
|
"B": -13.95 |
||||||
|
}, |
||||||
|
"C": -5, |
||||||
|
"D": [] |
||||||
|
} */ |
||||||
|
protected static function HashTable MakeTestJSONObject() |
||||||
|
{ |
||||||
|
local ArrayList outerArray, innerArray; |
||||||
|
local HashTable result, outerObject, innerObject; |
||||||
|
|
||||||
|
innerArray = __().collections.EmptyArrayList(); |
||||||
|
innerArray.AddBool(true).AddItem(none).AddString("huh"); |
||||||
|
innerObject = __().collections.EmptyHashTable(); |
||||||
|
innerObject.SetString(P("A"), "simpleValue"); |
||||||
|
innerObject.SetFloat(P("B"), 11.12).SetItem(P(""), innerArray); |
||||||
|
outerArray = __().collections.EmptyArrayList(); |
||||||
|
outerArray.AddBool(true).AddItem(innerObject).AddString("huh"); |
||||||
|
outerObject = __().collections.EmptyHashTable(); |
||||||
|
outerObject.SetItem(P("A"), outerArray).SetFloat(P("B"), -13.95); |
||||||
|
result = __().collections.EmptyHashTable(); |
||||||
|
result.SetString(P("A"), "simpleValue"); |
||||||
|
result.SetItem(P("B"), outerObject); |
||||||
|
result.SetInt(P("C"), -5); |
||||||
|
result.SetItem(P("D"), __().collections.EmptyArrayList()); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
protected static function CheckLocation(string pointer, string expectedResult) |
||||||
|
{ |
||||||
|
local AcediaObject value; |
||||||
|
|
||||||
|
value = default.cache |
||||||
|
.Read(__().json.Pointer(__().text.FromString(pointer))); |
||||||
|
if (BaseText(value) != none) |
||||||
|
{ |
||||||
|
TEST_ExpectTrue( |
||||||
|
__().json.Print(value).ToString() |
||||||
|
== ("\"" $ expectedResult $ "\"")); |
||||||
|
} |
||||||
|
else { |
||||||
|
TEST_ExpectTrue(__().json.Print(value).ToString() == expectedResult); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// This is a rather extensive and long test. |
||||||
|
// Despite its length, I've chosen to not break it up into smaller parts, |
||||||
|
// since the same value `default.cache` is changed and built up all the way |
||||||
|
// through this method. |
||||||
|
// Trying to break it up into smaller and simpler tests would only mean |
||||||
|
// putting less stress of `DBCache` than we currently do. |
||||||
|
protected static function Test_DBCache_BasicWriteRead_DuringLoading() |
||||||
|
{ |
||||||
|
local AcediaObject complexData; |
||||||
|
|
||||||
|
Issue("Simple write/read sequence not working correctly."); |
||||||
|
default.cache = DBCache(__().memory.Allocate(class'DBCache')); |
||||||
|
complexData = MakeTestJSONObject(); |
||||||
|
default.cache.Write( |
||||||
|
__().json.Pointer(P("/just/some/place/far/away")), |
||||||
|
complexData); |
||||||
|
default.cache.Write( |
||||||
|
__().json.Pointer(P("/just/another/place/not/near")), |
||||||
|
complexData); |
||||||
|
CheckLocation("/just/another/place/not/near/A", "simpleValue"); |
||||||
|
CheckLocation("/just/some/place/far/away/B/A/2", "huh"); |
||||||
|
CheckLocation("/just/another/place/not/near/B/A/1//2", "huh"); |
||||||
|
CheckLocation("/just/some/place/far/away/B/A/1/", "[true,null,\"huh\"]"); |
||||||
|
|
||||||
|
Issue("Data is read incorrectly after being overridden."); |
||||||
|
default.cache.Write( |
||||||
|
__().json.Pointer(P("/just/another/place/not/near/B/A/1//1")), |
||||||
|
__().box.bool(false)); |
||||||
|
CheckLocation( |
||||||
|
"/just/another/place/not/near/B/A/1/", |
||||||
|
"[true,false,\"huh\"]"); |
||||||
|
default.cache.Write( |
||||||
|
__().json.Pointer(P("/just/another/place/not/near/B/A")), |
||||||
|
__().box.int(121)); |
||||||
|
CheckLocation("/just/another/place/not/near/B/A", "121"); |
||||||
|
default.cache.Write( |
||||||
|
__().json.Pointer(P("/just/some/place/far/away/B/A/1/B")), |
||||||
|
complexData); |
||||||
|
CheckLocation("/just/some/place/far/away/B/A/1/B/B/A/0", "true"); |
||||||
|
default.cache.Write( |
||||||
|
__().json.Pointer( |
||||||
|
P("/just/some/place/far/away/C/inside_the_number!/hey")), |
||||||
|
__().box.float(1.1)); |
||||||
|
CheckLocation("/just/some/place/far/away/C/inside_the_number!/hey", "null"); |
||||||
|
default.cache.Write( |
||||||
|
__().json.Pointer(P("/just/some/place/far/away/D/7/hey")), |
||||||
|
__().box.int(-345)); |
||||||
|
CheckLocation("/just/some/place/far/away/D", "[]"); |
||||||
|
CheckLocation("/just/some/place/far/away/D/7/hey", "-345"); |
||||||
|
default.cache.Write( |
||||||
|
__().json.Pointer( |
||||||
|
P("/just/some/place/far/away/D/inside_the_array!/hey")), |
||||||
|
__().box.float(1.1)); |
||||||
|
CheckLocation("/just/some/place/far/away/D/inside_the_array!/hey", "null"); |
||||||
|
CheckLocation("/just/some/place/far/away/D", "[]"); |
||||||
|
default.cache.Write( |
||||||
|
__().json.Pointer(P("/just/some/place/far/away/D/-")), |
||||||
|
__().box.bool(true)); |
||||||
|
CheckLocation("/just/some/place/far/away/D", "[true]"); |
||||||
|
default.cache.Write( |
||||||
|
__().json.Pointer(P("/just/some/place/far/away/D/7")), |
||||||
|
__().box.bool(true)); |
||||||
|
CheckLocation( |
||||||
|
"/just/some/place/far/away/D", |
||||||
|
"[true,null,null,null,null,null,null,true]"); |
||||||
|
Issue("Writing at the end of a JSON array several times doesn't correctly" |
||||||
|
@ "keep all values."); |
||||||
|
default.cache.Write( |
||||||
|
__().json.Pointer(P("/just/some/place/far/away/D/-")), |
||||||
|
__().box.int(13524)); |
||||||
|
default.cache.Write( |
||||||
|
__().json.Pointer(P("/just/some/place/far/away/D/-")), none); |
||||||
|
default.cache.Write( |
||||||
|
__().json.Pointer(P("/just/some/place/far/away/D/-")), |
||||||
|
__().box.int(121)); |
||||||
|
CheckLocation( |
||||||
|
"/just/some/place/far/away/D", |
||||||
|
"[true,null,null,null,null,null,null,true,13524,null,121]"); |
||||||
|
} |
||||||
|
|
||||||
|
protected static function Test_DBCache_AllOperations_DuringLoading() |
||||||
|
{ |
||||||
|
local AcediaObject complexData; |
||||||
|
local ArrayList newArrayData; |
||||||
|
|
||||||
|
newArrayData = __().collections.EmptyArrayList(); |
||||||
|
newArrayData = newArrayData.AddInt(45); |
||||||
|
newArrayData = newArrayData.AddItem(none); |
||||||
|
newArrayData = newArrayData.AddString("lol"); |
||||||
|
|
||||||
|
Issue("Increment/remove/read sequence not working correctly."); |
||||||
|
default.cache = DBCache(__().memory.Allocate(class'DBCache')); |
||||||
|
complexData = MakeTestJSONObject(); |
||||||
|
default.cache.Increment(__().json.Pointer(P("")), complexData); |
||||||
|
default.cache.Increment(__().json.Pointer(P("/B/A/1/A")), |
||||||
|
__().text.FromString("oi")); |
||||||
|
default.cache.Remove(__().json.Pointer(P("/B/A/1/B"))); |
||||||
|
default.cache.Remove(__().json.Pointer(P("/B/A/1/"))); |
||||||
|
default.cache.Increment(__().json.Pointer(P("/B/A")), newArrayData); |
||||||
|
default.cache.Increment(__().json.Pointer(P("/C")), __().box.float(34.5)); |
||||||
|
default.cache.Increment(__().json.Pointer(P("/C")), __().box.bool(true)); |
||||||
|
default.cache.Increment(__().json.Pointer(P("/D")), newArrayData); |
||||||
|
default.cache.Increment(__().json.Pointer(P("/D")), newArrayData); |
||||||
|
default.cache.Increment(__().json.Pointer(P("/D")), newArrayData); |
||||||
|
default.cache.Increment(__().json.Pointer(P("/B/A/1/A")), |
||||||
|
__().text.FromString("! Yeah!")); |
||||||
|
// Override all increments! |
||||||
|
default.cache.Write(__().json.Pointer(P("/D")), newArrayData); |
||||||
|
CheckLocation("/B/A/1/A", "simpleValueoi! Yeah!"); |
||||||
|
CheckLocation("/B/A", |
||||||
|
"[true,{\"A\":\"simpleValueoi! Yeah!\"},\"huh\",45,null,\"lol\"]"); |
||||||
|
CheckLocation("/C", "29.5"); |
||||||
|
CheckLocation("/D", "[45,null,\"lol\"]"); |
||||||
|
} |
||||||
|
|
||||||
|
protected static function Test_DBCache_ApplyingPendingEdits() |
||||||
|
{ |
||||||
|
SubTest_DBCache_ApplyingPendingEdits_Simple(); |
||||||
|
SubTest_DBCache_ApplyingPendingEdits_Complex(); |
||||||
|
} |
||||||
|
|
||||||
|
protected static function SubTest_DBCache_ApplyingPendingEdits_Simple() |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local array<DBCache.PendingEdit> result; |
||||||
|
|
||||||
|
Issue("Pending writes successfully apply in simple JSON object case."); |
||||||
|
default.cache = DBCache(__().memory.Allocate(class'DBCache')); |
||||||
|
default.cache.Write( |
||||||
|
__().json.Pointer(P("")), |
||||||
|
__().collections.EmptyHashTable()); |
||||||
|
default.cache.Write(__().json.Pointer(P("/hey")), __().box.int(476)); |
||||||
|
default.cache.Write(__().json.Pointer(P("/hope")), none); |
||||||
|
default.cache.Write(__().json.Pointer(P("/-")), __().ref.float(324.3)); |
||||||
|
result = default.cache.SetRealData(__().text.FromString("woah")); |
||||||
|
for (i = 0; i < result.length; i += 1) { |
||||||
|
TEST_ExpectTrue(result[i].successful); |
||||||
|
} |
||||||
|
CheckLocation("/hey", "476"); |
||||||
|
CheckLocation("/hope", "null"); |
||||||
|
CheckLocation("/-", "324.3"); |
||||||
|
|
||||||
|
Issue("Pending don't fail in a simple case of writing into JSON string."); |
||||||
|
default.cache = DBCache(__().memory.Allocate(class'DBCache')); |
||||||
|
default.cache.Write(__().json.Pointer(P("/hey")), __().box.int(476)); |
||||||
|
default.cache.Write(__().json.Pointer(P("/hope")), none); |
||||||
|
default.cache.Write(__().json.Pointer(P("/-")), __().ref.float(324.3)); |
||||||
|
result = default.cache.SetRealData(__().text.FromString("woah")); |
||||||
|
for (i = 0; i < result.length; i += 1) { |
||||||
|
TEST_ExpectFalse(result[i].successful); |
||||||
|
} |
||||||
|
CheckLocation("/hey", "null"); |
||||||
|
CheckLocation("/hope", "null"); |
||||||
|
CheckLocation("/-", "null"); |
||||||
|
} |
||||||
|
|
||||||
|
protected static function SubTest_DBCache_ApplyingPendingEdits_Complex() |
||||||
|
{ |
||||||
|
local array<DBCache.PendingEdit> result; |
||||||
|
|
||||||
|
Issue("Pending writes incorrectly apply in complex case."); |
||||||
|
default.cache = DBCache(__().memory.Allocate(class'DBCache')); |
||||||
|
default.cache.Write(__().json.Pointer(P("/B/A/1/B")), __().box.int(777)); |
||||||
|
default.cache.Write(__().json.Pointer(P("/B/A/-")), __().box.bool(true)); |
||||||
|
default.cache.Write(__().json.Pointer(P("/D/5")), __().box.float(1.1)); |
||||||
|
default.cache.Write( |
||||||
|
__().json.Pointer(P("/new")), |
||||||
|
__().collections.EmptyHashTable()); |
||||||
|
default.cache.Write( |
||||||
|
__().json.Pointer(P("/new/sub")), |
||||||
|
__().text.FromString("!SubString!")); |
||||||
|
default.cache.Write( |
||||||
|
__().json.Pointer(P("/D/impossiburu")), |
||||||
|
__().text.FromString("!SubString!")); |
||||||
|
result = default.cache.SetRealData(MakeTestJSONObject()); |
||||||
|
CheckLocation("/B/A/1/B", "777"); |
||||||
|
CheckLocation("/B/A/3", "true"); |
||||||
|
CheckLocation("/D/5", "1.1"); |
||||||
|
CheckLocation("/D/2", "null"); |
||||||
|
CheckLocation("/new", "{\"sub\":\"!SubString!\"}"); |
||||||
|
TEST_ExpectTrue(result[0].successful); |
||||||
|
TEST_ExpectTrue(result[1].successful); |
||||||
|
TEST_ExpectTrue(result[2].successful); |
||||||
|
TEST_ExpectTrue(result[3].successful); |
||||||
|
TEST_ExpectTrue(result[4].successful); |
||||||
|
TEST_ExpectFalse(result[5].successful); |
||||||
|
} |
||||||
|
|
||||||
|
protected static function Test_DBCache_ApplyingPendingEdit_AllOperations() |
||||||
|
{ |
||||||
|
local ArrayList newArrayData; |
||||||
|
local array<DBCache.PendingEdit> result; |
||||||
|
|
||||||
|
newArrayData = __().collections.EmptyArrayList(); |
||||||
|
newArrayData = newArrayData.AddInt(45).AddItem(none).AddString("lol"); |
||||||
|
|
||||||
|
Issue("Pending increments and removals incorrectly apply in complex case."); |
||||||
|
default.cache = DBCache(__().memory.Allocate(class'DBCache')); |
||||||
|
default.cache.Increment(__().json.Pointer(P("/B/A/1/A")), |
||||||
|
__().text.FromString("oi")); |
||||||
|
default.cache.Remove(__().json.Pointer(P("/B/A/1/B"))); |
||||||
|
default.cache.Remove(__().json.Pointer(P("/B/A/1/"))); |
||||||
|
default.cache.Increment(__().json.Pointer(P("/B/A")), newArrayData); |
||||||
|
default.cache.Increment(__().json.Pointer(P("/C")), __().box.float(34.5)); |
||||||
|
default.cache.Increment(__().json.Pointer(P("/C")), __().box.bool(true)); |
||||||
|
default.cache.Increment(__().json.Pointer(P("/D")), newArrayData); |
||||||
|
default.cache.Increment(__().json.Pointer(P("/D")), newArrayData); |
||||||
|
default.cache.Remove(__().json.Pointer(P("/B/A/Y"))); |
||||||
|
default.cache.Increment(__().json.Pointer(P("/B/A/1/A")), |
||||||
|
__().text.FromString("! Yeah!")); |
||||||
|
default.cache.Write(__().json.Pointer(P("/D")), newArrayData); |
||||||
|
result = default.cache.SetRealData(MakeTestJSONObject()); |
||||||
|
TEST_ExpectTrue(result.length == 9); |
||||||
|
TEST_ExpectTrue(result[0].successful); |
||||||
|
TEST_ExpectTrue(result[1].successful); |
||||||
|
TEST_ExpectTrue(result[2].successful); |
||||||
|
TEST_ExpectTrue(result[3].successful); |
||||||
|
TEST_ExpectTrue(result[4].successful); |
||||||
|
TEST_ExpectFalse(result[5].successful); |
||||||
|
TEST_ExpectFalse(result[6].successful); |
||||||
|
TEST_ExpectTrue(result[7].successful); |
||||||
|
TEST_ExpectTrue(result[8].successful); |
||||||
|
} |
||||||
|
|
||||||
|
protected static function Test_DBCache_BasicWriteRead_AfterLoading() |
||||||
|
{ |
||||||
|
local AcediaObject complexData; |
||||||
|
|
||||||
|
Issue("Simple write/read sequence not working."); |
||||||
|
default.cache = DBCache(__().memory.Allocate(class'DBCache')); |
||||||
|
complexData = MakeTestJSONObject(); |
||||||
|
TEST_ExpectTrue(default.cache.SetRealData(complexData).length == 0); |
||||||
|
default.cache.Write(__().json.Pointer(P("/B/A/1/B")), __().box.int(777)); |
||||||
|
default.cache.Write(__().json.Pointer(P("/B/A/-")), __().box.bool(true)); |
||||||
|
default.cache.Write(__().json.Pointer(P("/D/5")), __().box.float(1.1)); |
||||||
|
default.cache.Write( |
||||||
|
__().json.Pointer(P("/new")), |
||||||
|
__().collections.EmptyHashTable()); |
||||||
|
default.cache.Write( |
||||||
|
__().json.Pointer(P("/new/sub")), |
||||||
|
__().text.FromString("!SubString!")); |
||||||
|
default.cache.Write( |
||||||
|
__().json.Pointer(P("/D/impossiburu")), |
||||||
|
__().text.FromString("!SubString!")); |
||||||
|
CheckLocation("/B/A/1/B", "777"); |
||||||
|
CheckLocation("/B/A/3", "true"); |
||||||
|
CheckLocation("/D/5", "1.1"); |
||||||
|
CheckLocation("/new", "{\"sub\":\"!SubString!\"}"); |
||||||
|
|
||||||
|
Issue("Simple write/read are affecting data when they shouldn't."); |
||||||
|
CheckLocation("/D/2", "null"); |
||||||
|
default.cache.Write(__().json.Pointer(P("")), __().box.float(1.1)); |
||||||
|
default.cache.Write(__().json.Pointer(P("/hm?")), __().box.float(2.2)); |
||||||
|
CheckLocation("", "1.1"); |
||||||
|
} |
||||||
|
|
||||||
|
protected static function Test_DBCache_AllOperations_AfterLoading() |
||||||
|
{ |
||||||
|
local ArrayList newArrayData; |
||||||
|
|
||||||
|
newArrayData = __().collections.EmptyArrayList(); |
||||||
|
newArrayData = newArrayData.AddInt(45).AddItem(none).AddString("lol"); |
||||||
|
|
||||||
|
Issue("Increment/remove/read sequence not working correctly."); |
||||||
|
default.cache = DBCache(__().memory.Allocate(class'DBCache')); |
||||||
|
default.cache.SetRealData(MakeTestJSONObject()); |
||||||
|
TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/B/A/1/A")), |
||||||
|
__().text.FromString("oi"))); |
||||||
|
TEST_ExpectTrue(default.cache.Remove(__().json.Pointer(P("/B/A/1/B")))); |
||||||
|
TEST_ExpectTrue(default.cache.Remove(__().json.Pointer(P("/B/A/1/")))); |
||||||
|
TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/B/A")), |
||||||
|
newArrayData)); |
||||||
|
TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/C")), |
||||||
|
__().box.float(34.5))); |
||||||
|
TEST_ExpectFalse(default.cache.Increment(__().json.Pointer(P("/C")), |
||||||
|
__().box.bool(true))); |
||||||
|
TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/D")), |
||||||
|
newArrayData)); |
||||||
|
TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/D")), |
||||||
|
newArrayData)); |
||||||
|
TEST_ExpectFalse(default.cache.Remove(__().json.Pointer(P("/B/A/Y")))); |
||||||
|
TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/B/A/1/A")), |
||||||
|
__().text.FromString("! Yeah!"))); |
||||||
|
default.cache.Write(__().json.Pointer(P("/D")), newArrayData); |
||||||
|
CheckLocation("/B/A/1/A", "simpleValueoi! Yeah!"); |
||||||
|
CheckLocation("/B/A", |
||||||
|
"[true,{\"A\":\"simpleValueoi! Yeah!\"},\"huh\",45,null,\"lol\"]"); |
||||||
|
CheckLocation("/C", "29.5"); |
||||||
|
CheckLocation("/D", "[45,null,\"lol\"]"); |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
caseGroup = "Database" |
||||||
|
caseName = "DBConnection related tests" |
||||||
|
} |
@ -0,0 +1,40 @@ |
|||||||
|
/** |
||||||
|
* Signal class for `PersistentDataManager`'s `OnPersistentDataReady()` signal. |
||||||
|
* Copyright 2023 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class PersistentDataManager_OnPersistentDataReady_Signal extends Signal |
||||||
|
dependson(DBConnection); |
||||||
|
|
||||||
|
public final function Emit(UserID id, bool online) |
||||||
|
{ |
||||||
|
local Slot nextSlot; |
||||||
|
StartIterating(); |
||||||
|
nextSlot = GetNextSlot(); |
||||||
|
while (nextSlot != none) |
||||||
|
{ |
||||||
|
PersistentDataManager_OnPersistentDataReady_Slot(nextSlot) |
||||||
|
.connect(id, online); |
||||||
|
nextSlot = GetNextSlot(); |
||||||
|
} |
||||||
|
CleanEmptySlots(); |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
relatedSlotClass = class'PersistentDataManager_OnPersistentDataReady_Slot' |
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
/** |
||||||
|
* Slot class for `PersistentDataManager`'s `OnPersistentDataReady()` signal. |
||||||
|
* Copyright 2023 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class PersistentDataManager_OnPersistentDataReady_Slot extends Slot |
||||||
|
dependson(DBConnection); |
||||||
|
|
||||||
|
delegate connect(UserID id, bool online) |
||||||
|
{ |
||||||
|
DummyCall(); |
||||||
|
} |
||||||
|
|
||||||
|
protected function Constructor() |
||||||
|
{ |
||||||
|
connect = none; |
||||||
|
} |
||||||
|
|
||||||
|
protected function Finalizer() |
||||||
|
{ |
||||||
|
super.Finalizer(); |
||||||
|
connect = none; |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
} |
@ -0,0 +1,407 @@ |
|||||||
|
/** |
||||||
|
* This tool is for simplifying writing and reading persistent user data. |
||||||
|
* All it requires is a setup of database + json pointer to data and it will |
||||||
|
* take care of data caching and database connection. |
||||||
|
* Copyright 2023 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class PersistentDataManager extends AcediaObject; |
||||||
|
|
||||||
|
/** |
||||||
|
* # `PersistentDataManager` |
||||||
|
* |
||||||
|
* This tool is for simplifying writing and reading persistent user data. |
||||||
|
* All it requires is a setup of database + json pointer to data and it will |
||||||
|
* take care of data caching and database connection. |
||||||
|
* |
||||||
|
* ## Usage |
||||||
|
* |
||||||
|
* Create an instance and use `Setup()` to connect to the database with |
||||||
|
* persistent data. You can use `Setup()` again on the same object to setup |
||||||
|
* a different database as a source. All data will be automatically reloaded. |
||||||
|
* After that you can use `GetPersistentData()`/`SetPersistentData()` to |
||||||
|
* read/write persistent data for the particular user. |
||||||
|
* Since loading data from the database takes time, you don't have an |
||||||
|
* immediate access to it. |
||||||
|
* But you can use `_.users.OnPersistentDataAvailable()` signal to track |
||||||
|
* whenever new user data from database becomes available. However, you can |
||||||
|
* start writing persistent data (and reading what you've wrote) at any time it |
||||||
|
* - these changes will be reapplied whenever data is actually loaded from |
||||||
|
* database. |
||||||
|
* |
||||||
|
* ## Implementation |
||||||
|
* |
||||||
|
* Implementation consists of simply creating `DBConnection` for every user |
||||||
|
* and storing them in the `HashTable` that maps user IDs into those |
||||||
|
* `DBConnection`s. |
||||||
|
* We also maintain a reverse map to figure out what `DBConnection` belongs |
||||||
|
* to what user when connection signals an update. We borrow the signal that |
||||||
|
* `UsersAPI` provides to inform everyone interested about which users |
||||||
|
* have updated. |
||||||
|
*/ |
||||||
|
|
||||||
|
var private bool initialized; |
||||||
|
var private Database database; |
||||||
|
var private JSONPointer rootPointer; |
||||||
|
var private HashTable userToConnection, connectionToUser; |
||||||
|
|
||||||
|
var private PersistentDataManager_OnPersistentDataReady_Signal onPersistentDataReadySignal; |
||||||
|
|
||||||
|
protected function Constructor() |
||||||
|
{ |
||||||
|
_.players.OnNewPlayer(self).connect = ConnectPersistentData; |
||||||
|
onPersistentDataReadySignal = _.users._getOnReadySignal(); |
||||||
|
} |
||||||
|
|
||||||
|
protected function Finalizer() |
||||||
|
{ |
||||||
|
Reset(); |
||||||
|
_.players.OnNewPlayer(self).Disconnect(); |
||||||
|
} |
||||||
|
|
||||||
|
private final function Reset() |
||||||
|
{ |
||||||
|
_.memory.Free(database); |
||||||
|
_.memory.Free(rootPointer); |
||||||
|
_.memory.Free(userToConnection); |
||||||
|
_.memory.Free(connectionToUser); |
||||||
|
_.memory.Free(onPersistentDataReadySignal); |
||||||
|
database = none; |
||||||
|
rootPointer = none; |
||||||
|
userToConnection = none; |
||||||
|
connectionToUser = none; |
||||||
|
onPersistentDataReadySignal = none; |
||||||
|
initialized = false; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Sets up database and location inside it as a source of users' persistent |
||||||
|
* data. |
||||||
|
* |
||||||
|
* Must be successfully called at least once for the caller |
||||||
|
* `PersistentDataManager` to be usable. |
||||||
|
* |
||||||
|
* @param db Database inside which persistent data is stored. |
||||||
|
* @param location Location inside specified database to the root of |
||||||
|
* persistent data. |
||||||
|
* @return `true` if setup was successful (requires both arguments to be not |
||||||
|
* `none`) and `false` otherwise. |
||||||
|
*/ |
||||||
|
public final function bool Setup(Database db, JSONPointer location) |
||||||
|
{ |
||||||
|
if (db == none) return false; |
||||||
|
if (location == none) return false; |
||||||
|
|
||||||
|
Reset(); |
||||||
|
database = db; |
||||||
|
database.NewRef(); |
||||||
|
rootPointer = location.Copy(); |
||||||
|
userToConnection = _.collections.EmptyHashTable(); |
||||||
|
connectionToUser = _.collections.EmptyHashTable(); |
||||||
|
// Using `userToConnection` as an empty hash table, not related to its |
||||||
|
// actual meaning |
||||||
|
database.IncrementData(location, userToConnection); |
||||||
|
initialized = true; |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Reads specified named persistent data for the specified group. |
||||||
|
* |
||||||
|
* @param id ID of the user to read persistent data from. |
||||||
|
* @param groupName Group to which this persistent data belongs to. |
||||||
|
* Groups are used as namespaces to avoid duplicate persistent variables |
||||||
|
* between mods. If your mod needs several subgroups, its recommended to |
||||||
|
* use the same prefix for them, e.g. "MyAwesomeMod.economy" and |
||||||
|
* "MyAwesomeMod.enemies". |
||||||
|
* @param dataName Name of persistent data variable to read inside |
||||||
|
* `groupName` persistent data group. Not `none` value must be provided. |
||||||
|
* @param data Data to set as persistent value. Must be |
||||||
|
* JSON-compatible. If `none` is passed, returns the all data for |
||||||
|
* the given group. |
||||||
|
* @return Data read from the persistent variable. `none` in case of any kind |
||||||
|
* of failure. |
||||||
|
*/ |
||||||
|
public final function AcediaObject GetPersistentData( |
||||||
|
UserID id, |
||||||
|
BaseText groupName, |
||||||
|
optional BaseText dataName) |
||||||
|
{ |
||||||
|
local AcediaObject result; |
||||||
|
local Text textID; |
||||||
|
local JSONPointer location; |
||||||
|
local DBConnection relevantConnection; |
||||||
|
|
||||||
|
if (!initialized) return none; |
||||||
|
if (id == none) return none; |
||||||
|
if (groupName == none) return none; |
||||||
|
|
||||||
|
textID = id.GetUniqueID(); |
||||||
|
relevantConnection = DBConnection(userToConnection.GetItem(textID)); |
||||||
|
textID.FreeSelf(); |
||||||
|
if (relevantConnection != none) |
||||||
|
{ |
||||||
|
location = _.json.Pointer(); |
||||||
|
location.Push(groupName); |
||||||
|
if (dataName != none) { |
||||||
|
location.Push(dataName); |
||||||
|
} |
||||||
|
result = relevantConnection.ReadDataByJSON(location); |
||||||
|
relevantConnection.FreeSelf(); |
||||||
|
location.FreeSelf(); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Writes specified named persistent data for the specified group. |
||||||
|
* |
||||||
|
* @param id ID of the user to change persistent data of. |
||||||
|
* @param groupName Group to which this persistent data belongs to. |
||||||
|
* Groups are used as namespaces to avoid duplicate persistent variables |
||||||
|
* between mods. If your mod needs several subgroups, its recommended to |
||||||
|
* use the same prefix for them, e.g. "MyAwesomeMod.economy" and |
||||||
|
* "MyAwesomeMod.enemies". |
||||||
|
* @param dataName Name of persistent data variable to change inside |
||||||
|
* `groupName` persistent data group. |
||||||
|
* @param data Data to set as persistent value. Must be |
||||||
|
* JSON-compatible. |
||||||
|
* @return `true` if change succeeded in local cached version of database with |
||||||
|
* persistent values and `false` otherwise. Such local changes can |
||||||
|
* potentially be not applied to the actual database. But successful local |
||||||
|
* changes should persist for the game session. |
||||||
|
*/ |
||||||
|
public final function bool WritePersistentData( |
||||||
|
UserID id, |
||||||
|
BaseText groupName, |
||||||
|
BaseText dataName, |
||||||
|
AcediaObject data) |
||||||
|
{ |
||||||
|
local bool result; |
||||||
|
local Text textID; |
||||||
|
local JSONPointer location; |
||||||
|
local DBConnection relevantConnection; |
||||||
|
local HashTable emptyObject; |
||||||
|
|
||||||
|
if (!initialized) return false; |
||||||
|
if (id == none) return false; |
||||||
|
if (groupName == none) return false; |
||||||
|
if (dataName == none) return false; |
||||||
|
|
||||||
|
textID = id.GetUniqueID(); |
||||||
|
relevantConnection = DBConnection(userToConnection.GetItem(textID)); |
||||||
|
textID.FreeSelf(); |
||||||
|
if (relevantConnection != none) |
||||||
|
{ |
||||||
|
emptyObject = _.collections.EmptyHashTable(); |
||||||
|
location = _.json.Pointer(); |
||||||
|
location.Push(groupName); |
||||||
|
relevantConnection.IncrementDataByJSON(location, emptyObject); |
||||||
|
location.Push(dataName); |
||||||
|
result = relevantConnection.WriteDataByJSON(location, data); |
||||||
|
relevantConnection.FreeSelf(); |
||||||
|
location.FreeSelf(); |
||||||
|
emptyObject.FreeSelf(); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Increments specified named persistent data for the specified group. |
||||||
|
* |
||||||
|
* @param id ID of the user to change persistent data of. |
||||||
|
* @param groupName Group to which this persistent data belongs to. |
||||||
|
* Groups are used as namespaces to avoid duplicate persistent variables |
||||||
|
* between mods. If your mod needs several subgroups, its recommended to |
||||||
|
* use the same prefix for them, e.g. "MyAwesomeMod.economy" and |
||||||
|
* "MyAwesomeMod.enemies". |
||||||
|
* @param dataName Name of persistent data variable to change inside |
||||||
|
* `groupName` persistent data group. |
||||||
|
* @param data Data by which to increment existing persistent value. |
||||||
|
* Must be JSON-compatible. |
||||||
|
* @return `true` if change succeeded in local cached version of database with |
||||||
|
* persistent values and `false` otherwise. Such local changes can |
||||||
|
* potentially be not applied to the actual database. But successful local |
||||||
|
* changes should persist for the game session. |
||||||
|
*/ |
||||||
|
public final function bool IncrementPersistentData( |
||||||
|
UserID id, |
||||||
|
BaseText groupName, |
||||||
|
BaseText dataName, |
||||||
|
AcediaObject data) |
||||||
|
{ |
||||||
|
local bool result; |
||||||
|
local Text textID; |
||||||
|
local JSONPointer location; |
||||||
|
local DBConnection relevantConnection; |
||||||
|
|
||||||
|
if (!initialized) return false; |
||||||
|
if (id == none) return false; |
||||||
|
if (groupName == none) return false; |
||||||
|
if (dataName == none) return false; |
||||||
|
|
||||||
|
textID = id.GetUniqueID(); |
||||||
|
relevantConnection = DBConnection(userToConnection.GetItem(textID)); |
||||||
|
textID.FreeSelf(); |
||||||
|
if (relevantConnection != none) |
||||||
|
{ |
||||||
|
location = _.json.Pointer(); |
||||||
|
location.Push(groupName).Push(dataName); |
||||||
|
result = relevantConnection.IncrementDataByJSON(location, data); |
||||||
|
relevantConnection.FreeSelf(); |
||||||
|
location.FreeSelf(); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Removes specified named persistent data for the specified group. |
||||||
|
* |
||||||
|
* @param id ID of the user to remove persistent data of. |
||||||
|
* @param groupName Group to which this persistent data belongs to. |
||||||
|
* Groups are used as namespaces to avoid duplicate persistent variables |
||||||
|
* between mods. If your mod needs several subgroups, its recommended to |
||||||
|
* use the same prefix for them, e.g. "MyAwesomeMod.economy" and |
||||||
|
* "MyAwesomeMod.enemies". |
||||||
|
* @param dataName Name of persistent data variable to remove inside |
||||||
|
* `groupName` persistent data group. |
||||||
|
* @return `true` if removal succeeded in local cached version of database with |
||||||
|
* persistent values and `false` otherwise. Such local changes can |
||||||
|
* potentially be not applied to the actual database. But successful local |
||||||
|
* changes should persist for the game session. |
||||||
|
*/ |
||||||
|
public final function bool RemovePersistentData( |
||||||
|
UserID id, |
||||||
|
BaseText groupName, |
||||||
|
BaseText dataName) |
||||||
|
{ |
||||||
|
local bool result; |
||||||
|
local Text textID; |
||||||
|
local JSONPointer location; |
||||||
|
local DBConnection relevantConnection; |
||||||
|
|
||||||
|
if (!initialized) return false; |
||||||
|
if (id == none) return false; |
||||||
|
if (groupName == none) return false; |
||||||
|
if (dataName == none) return false; |
||||||
|
|
||||||
|
textID = id.GetUniqueID(); |
||||||
|
relevantConnection = DBConnection(userToConnection.GetItem(textID)); |
||||||
|
textID.FreeSelf(); |
||||||
|
if (relevantConnection != none) |
||||||
|
{ |
||||||
|
location = _.json.Pointer(); |
||||||
|
location.Push(groupName).Push(dataName); |
||||||
|
result = relevantConnection.RemoveDataByJSON(location); |
||||||
|
relevantConnection.FreeSelf(); |
||||||
|
location.FreeSelf(); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Connects and starts synchronizing persistent data for the given player. |
||||||
|
* |
||||||
|
* @param player Player to synchronize persistent data for. |
||||||
|
*/ |
||||||
|
public final function ConnectPersistentData(EPlayer player) |
||||||
|
{ |
||||||
|
local UserID playerID; |
||||||
|
|
||||||
|
if (initialized && player != none) |
||||||
|
{ |
||||||
|
playerID = player.GetUserID(); |
||||||
|
ConnectPersistentDataByID(playerID); |
||||||
|
_.memory.Free(playerID); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Connects and starts synchronizing persistent data for the player given by |
||||||
|
* their ID. |
||||||
|
* |
||||||
|
* @param id User ID for which to synchronize persistent data from |
||||||
|
* the database. |
||||||
|
*/ |
||||||
|
public final function ConnectPersistentDataByID(UserID id) |
||||||
|
{ |
||||||
|
local Text textID; |
||||||
|
local DBConnection newConnection; |
||||||
|
|
||||||
|
if (!initialized) return; |
||||||
|
if (id == none) return; |
||||||
|
|
||||||
|
textID = id.GetUniqueID(); |
||||||
|
if (userToConnection.HasKey(textID)) |
||||||
|
{ |
||||||
|
_.memory.Free(textID); |
||||||
|
return; |
||||||
|
} |
||||||
|
rootPointer.Push(textID); |
||||||
|
newConnection = DBConnection(_.memory.Allocate(class'DBConnection')); |
||||||
|
newConnection.Initialize(database, rootPointer); |
||||||
|
_.memory.Free(rootPointer.Pop()); |
||||||
|
newConnection.Connect(); |
||||||
|
userToConnection.SetItem(textID, newConnection); |
||||||
|
connectionToUser.SetItem(newConnection, textID); |
||||||
|
newConnection.OnStateChanged(self).connect = UserUpdated; |
||||||
|
textID.FreeSelf(); |
||||||
|
newConnection.FreeSelf(); |
||||||
|
} |
||||||
|
|
||||||
|
private final function UserUpdated( |
||||||
|
DBConnection instance, |
||||||
|
DBConnection.DBConnectionState oldState, |
||||||
|
DBConnection.DBConnectionState newState) |
||||||
|
{ |
||||||
|
local UserID id; |
||||||
|
|
||||||
|
if (!initialized) return; |
||||||
|
if (newState == DBCS_Connecting) return; |
||||||
|
if (onPersistentDataReadySignal == none) return; |
||||||
|
if (!onPersistentDataReadySignal.IsAllocated()) return; |
||||||
|
|
||||||
|
id = UserID(connectionToUser.GetItem(instance)); |
||||||
|
if (id != none) |
||||||
|
{ |
||||||
|
onPersistentDataReadySignal.Emit(id, newState == DBCS_Connected); |
||||||
|
id.FreeSelf(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Attempts to start persistent data synchronization for all players currently |
||||||
|
* on the server. |
||||||
|
*/ |
||||||
|
public final function LoadCurrentPlayers() |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local array<EPlayer> currentPlayers; |
||||||
|
|
||||||
|
if (initialized) |
||||||
|
{ |
||||||
|
currentPlayers = _.players.GetAll(); |
||||||
|
for (i = 0; i < currentPlayers.length; i += 1) { |
||||||
|
ConnectPersistentData(currentPlayers[i]); |
||||||
|
} |
||||||
|
_.memory.FreeMany(currentPlayers); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
} |
Loading…
Reference in new issue