794 lines
28 KiB
Ucode
794 lines
28 KiB
Ucode
/**
|
|
* 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 BaseJSONPointer 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(BaseJSONPointer 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(
|
|
BaseJSONPointer 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(
|
|
BaseJSONPointer 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(BaseJSONPointer pointer)
|
|
{
|
|
if (pointer == none) {
|
|
return false;
|
|
}
|
|
if (localCache.Remove(pointer))
|
|
{
|
|
RemoveDataInDatabase(pointer);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private final function ModifyDataInDatabase(
|
|
BaseJSONPointer pointer,
|
|
AcediaObject data,
|
|
bool increment)
|
|
{
|
|
local MutableJSONPointer dataPointer;
|
|
|
|
if (currentState != DBCS_Connected) {
|
|
return;
|
|
}
|
|
dataPointer = rootPointer.MutableCopy();
|
|
dataPointer.Append(pointer);
|
|
// `dataPointer` is consumed by `RegisterNextRequestID()` method
|
|
if (increment)
|
|
{
|
|
dbInstance
|
|
.IncrementData(
|
|
dataPointer,
|
|
data,
|
|
RegisterNextRequestID(/*take*/ dataPointer.Copy()))
|
|
.connect = EditDataHandler;
|
|
_.memory.Free(dataPointer);
|
|
}
|
|
else
|
|
{
|
|
dbInstance
|
|
.WriteData(dataPointer, data, RegisterNextRequestID(/*take*/ dataPointer.Copy()))
|
|
.connect = EditDataHandler;
|
|
_.memory.Free(dataPointer);
|
|
}
|
|
}
|
|
|
|
private final function RemoveDataInDatabase(BaseJSONPointer pointer)
|
|
{
|
|
local MutableJSONPointer dataPointer;
|
|
|
|
if (currentState != DBCS_Connected) {
|
|
return;
|
|
}
|
|
dataPointer = rootPointer.MutableCopy();
|
|
dataPointer.Append(pointer);
|
|
// `dataPointer` is consumed by `RegisterNextRequestID()` method
|
|
dbInstance
|
|
.RemoveData(dataPointer, RegisterNextRequestID(/*take*/ dataPointer.Copy()))
|
|
.connect = EditDataHandler;
|
|
_.memory.Free(dataPointer);
|
|
}
|
|
|
|
/**
|
|
* 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.")
|
|
} |