1092 lines
42 KiB
Ucode
1092 lines
42 KiB
Ucode
/**
|
|
* Object designed to allow for locally caching database's data and tracking
|
|
* all applied changes, even if database is yet to respond/rejected them.
|
|
* This includes tracking changes even *before* database's data is available,
|
|
* storing them inside as a series to edits to apply.
|
|
* 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 DBCache extends AcediaObject;
|
|
|
|
/**
|
|
* # `DBCache`
|
|
*
|
|
* Object designed to allow for locally caching database's data and tracking
|
|
* all applied changes, even if database is yet to respond/rejected them.
|
|
* This includes tracking changes even *before* database's data is available,
|
|
* storing them inside as a series to edits to apply.
|
|
*
|
|
* ## Usage
|
|
*
|
|
* You can simply read and write JSON data with `Read(BaseJSONPointer)` and
|
|
* `Write(BaseJSONPointer, AcediaObject)` right after `DBCache`'s creation.
|
|
* Once real database's data has arrived, you can set it with `SetRealData()`.
|
|
* Data recorded before the `SetRealData()` call is an *approximation* and
|
|
* might not function as a real JSON value/database. Because `DBCache` doesn't
|
|
* yet know the real data in the database and even if you expect there to be
|
|
* a certain hierarchy of objects/arrays - `DBCache` cannot perform checks that
|
|
* they are there. This is why it simply lets you write any data at any path
|
|
* like "/A/B/C" in hopes that it data will be there after `SetRealData()`
|
|
* call.
|
|
* You can also "pre-create" such data by calling `Increment()` method with
|
|
* empty `Collection`s:
|
|
*
|
|
* ```unrealscript
|
|
* local DBCache cache;
|
|
* local JSONPointer dataLocation;
|
|
* local HashTable emptyObject;
|
|
*
|
|
* cache = DBCache(_.memory.Allocate(class'DBCache'));
|
|
* emptyObject = _.collections.EmptyHashTable();
|
|
* dataLocation = _.json.Pointer();
|
|
* cache.Increment(dataLocation, emptyObject);
|
|
* cache.Push(P("A"));
|
|
* cache.Increment(dataLocation, emptyObject);
|
|
* cache.Push(P("B"));
|
|
* cache.Increment(dataLocation, emptyObject);
|
|
* cache.Push(P("C"));
|
|
* _.memory.Free(emptyObject);
|
|
* _.memory.Free(dataLocation);
|
|
* ```
|
|
*
|
|
* After `SetRealData()` edits you've made prior will be reapplied to
|
|
* provided data and you'll get report on what edits were attempted and what
|
|
* have failed:
|
|
*
|
|
* ```unrealscript
|
|
* local array<DBCache.PendingEdit> completedEdits;
|
|
* // ...
|
|
* completedEdits = localCache.SetRealData(data);
|
|
* for (i = 0; i < completedEdits.length; i += 1)
|
|
* {
|
|
* if (completedEdits[i].successful) {
|
|
* // Do something joyful!
|
|
* }
|
|
* else {
|
|
* // Wail in your misery!
|
|
* }
|
|
* _.memory.Free(completedEdits[i].location);
|
|
* _.memory.Free(completedEdits[i].data);
|
|
* }
|
|
* ```
|
|
*
|
|
* One more example of appending arrays using "-" JSON pointer component.
|
|
*
|
|
* ```unrealscript
|
|
* local DBCache cache;
|
|
* local JSONPointer newDataLocation, arrayLocation;
|
|
* local Text data;
|
|
*
|
|
* cache = DBCache(_.memory.Allocate(class'DBCache'));
|
|
* // "-" in JSON pointer allows us to append to array from the end
|
|
* newDataLocation = _.json.Pointer_S("/array/-");
|
|
* data = _.text.FromString("Just new data!");
|
|
* cache.Write(newDataLocation, data);
|
|
*
|
|
* // Now add database's data: supposing `dbData` was a following JSON object:
|
|
* // {"array": [1, 3, true], "tag": "db data!"}
|
|
* cache.SetRealData(dbData);
|
|
*
|
|
* // Now read changes back:t
|
|
* arrayLocation = _.json.Pointer_S("/array");
|
|
* cache.ReadData(arrayLocation);
|
|
* // ^ returns `ArrayList` with following contents:
|
|
* [1, 3, true, "Just new data!"]
|
|
* ```
|
|
*
|
|
* ## Implementation
|
|
*
|
|
* Cache can be in two distinct states: before (`cachedData` is `false`)
|
|
* and after (`cachedData` is `true`) obtaining database's actual data.
|
|
* In the second state it simply stores `AcediaObject` that represents stored
|
|
* JSON value and applies all changes to it directly / reads from it directly.
|
|
* In case database's data wasn't yet obtained - stores all valid `Write()`
|
|
* requests as an array of edits `pendingEdits`. Any `Read()` causes us to go
|
|
* through that array until we:
|
|
*
|
|
* 1. Find an edit that could've written a data user;
|
|
* 2. Obtain necessary data from that edit (we might want some folded
|
|
* sub-object);
|
|
* 3. Reapply all later edits to that data and return it.
|
|
*
|
|
* We also use similar process when adding new edits during `Write()`:
|
|
* if we know for a fact that at "/a/b/c" is a non-container
|
|
* (like a JSON string or number), then we simply reject any writes to its
|
|
* sub-data like "/a/b/c/d", since it is impossible to write anything inside.
|
|
* This also applies to the JSON arrays if we want to write into them using
|
|
* non-numeric keys.
|
|
* Additionally, for the sake of efficiency, `DBCache` erases old edits in
|
|
* case their data gets completely overwritten by new ones: if we first write
|
|
* something inside "/array/1" and then rewrite the whole "/array" - we no
|
|
* longer need to store the first edit for anything.
|
|
*
|
|
* ## Remarks
|
|
*
|
|
* Before `SetRealData()` is called, the collection inside `DBCache` is mostly
|
|
* faked. In most practical cases it shouldn't noticeable and the most notable
|
|
* issue one can stumble on is that `DBCache` allows to write data at paths, it
|
|
* is not sure even exist, leading to weird behavior:
|
|
*
|
|
* ```unrealscript
|
|
* local DBCache cache;
|
|
* local JSONPointer newDataLocation, objectLocation;
|
|
* local Text data;
|
|
*
|
|
* cache = DBCache(_.memory.Allocate(class'DBCache'));
|
|
* // "-" in JSON pointer allows us to append to array from the end
|
|
* newDataLocation = _.json.Pointer_S("/subObject/field");
|
|
* data = _.text.FromString("Just new data!");
|
|
* cache.Write(newDataLocation, data);
|
|
*
|
|
* // Without adding database's data with `SetRealData()` we get:
|
|
* objectLocation = _.json.Pointer_S("/subObject");
|
|
* // returns `none`, since we have no info about data at that path
|
|
* cache.ReadData(objectLocation); // none
|
|
* // But still remembers that this is "Just new data!"
|
|
* cache.ReadData(newDataLocation); // "Just new data!"
|
|
* ```
|
|
*/
|
|
|
|
enum DBCacheEditType
|
|
{
|
|
DBCET_Write,
|
|
DBCET_Increment,
|
|
DBCET_Remove
|
|
};
|
|
|
|
// Represents a single edit made as a result of `Write()` call
|
|
struct PendingEdit
|
|
{
|
|
var public DBCacheEditType type;
|
|
var public JSONPointer location;
|
|
var public AcediaObject data;
|
|
var public bool successful;
|
|
};
|
|
// All valid edits made so far (minus impossible and overwritten ones) in
|
|
// order they were made: the lower index, the older the edit.
|
|
var private array<PendingEdit> pendingEdits;
|
|
|
|
// Was data already cached?
|
|
// We cannot simply use `cachedData == none`, since `none` is a valid value.
|
|
var private bool isDataCached;
|
|
// Data, obtained from the database
|
|
var private AcediaObject cachedData;
|
|
|
|
protected function Finalizer()
|
|
{
|
|
local int i;
|
|
|
|
for (i = 0; i < pendingEdits.length; i += 1) {
|
|
FreePendingWrite(pendingEdits[i]);
|
|
}
|
|
pendingEdits.length = 0;
|
|
_.memory.Free(cachedData);
|
|
cachedData = none;
|
|
isDataCached = false;
|
|
}
|
|
|
|
/**
|
|
* Reads data from `DBCache` stored at the pointer `location`.
|
|
* If no data is recorded at `location`, returns `none`.
|
|
*
|
|
* NOTE: If the real database's data wasn't yet set with `SetRealData()`,
|
|
* then this method can return `none` for path like "/a/b", even if value
|
|
* "/a/b/c" was already set. This is because `DBCache` doesn't try to guess
|
|
* types of containers on the way to the recorded data: if 'b' were to
|
|
* be numeric - then we'd have no idea whether it is an array of an object.
|
|
*
|
|
* @param location Location inside `DBCache`'s stored data, from which to
|
|
* read data of interest.
|
|
* @return Data stored at location given by `location`, `none` if nothing is
|
|
* stored there.
|
|
*/
|
|
public final function AcediaObject Read(BaseJSONPointer location)
|
|
{
|
|
local Collection cachedCollection;
|
|
|
|
if (location == none) {
|
|
return none;
|
|
}
|
|
if (!isDataCached) {
|
|
return ReadPending(location);
|
|
}
|
|
if (location.IsEmpty()) {
|
|
return _.json.Copy(cachedData);
|
|
}
|
|
// For non-empty pointers, `cachedCollection` must be a `Collection`
|
|
cachedCollection = Collection(cachedData);
|
|
if (cachedCollection != none) {
|
|
return cachedCollection.GetItemByJSON(location);
|
|
}
|
|
return none;
|
|
}
|
|
|
|
/**
|
|
* Writes data into `DBCache` at the given location.
|
|
*
|
|
* This method can work differently depending on whether `SetRealData()` call
|
|
* was already made:
|
|
*
|
|
* 1. Before the `SetRealData()` call it basically knows nothing about
|
|
* object inside database (the real data) and will freely write at any
|
|
* location as long as that operation won't contradict previous edits
|
|
* (it will attempt to recognize and prevent removing sub-values inside
|
|
* null, booleans, strings and numbers or non-numeric keys into
|
|
* arrays).
|
|
* 2. After the `SetRealData()` call it already knows what data was stored
|
|
* inside before edits were made and would only allow to write data at
|
|
* "/path/to/value" if "/path/to" (i.e. path without its last
|
|
* component) corresponds to an appropriate collection (JSON object or
|
|
* JSON array if last component is numeric or "-").
|
|
*
|
|
* @param location Location into which to write new data.
|
|
* @param data Data to write, expected to consist only of
|
|
* the JSON-compatible types. It will be copied with `_.json.Copy()`,
|
|
* so the reference won't be kept.
|
|
* @return `true` if write was successful and `false` otherwise. Note that
|
|
* operations made before `SetRealData()` can be reported as successful,
|
|
* but then rejected after the real data is set if they're incompatible
|
|
* with its structure (@see `SetRealData()` for more information).
|
|
*/
|
|
public final function bool Write(BaseJSONPointer location, AcediaObject data)
|
|
{
|
|
local Collection cachedCollection;
|
|
|
|
if (location == none) {
|
|
return false;
|
|
}
|
|
if (!isDataCached) {
|
|
return AddPendingEdit(location, data, DBCET_Write);
|
|
}
|
|
if (location.IsEmpty())
|
|
{
|
|
_.memory.Free(cachedData);
|
|
cachedData = _.json.Copy(data);
|
|
return true;
|
|
}
|
|
cachedCollection = Collection(cachedData);
|
|
// At this point `EditJSONCollection()`'s contract of
|
|
// `cachedCollection != none` and `location` isn't `none` or empty is
|
|
// satisfied.
|
|
if (cachedCollection != none)
|
|
{
|
|
return EditJSONCollection(
|
|
cachedCollection,
|
|
location,
|
|
data,
|
|
DBCET_Write);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Removes data from `DBCache` at the given location.
|
|
*
|
|
* This method can work differently depending on whether `SetRealData()` call
|
|
* was already made:
|
|
*
|
|
* 1. Before the `SetRealData()` call it basically knows nothing about
|
|
* object inside database (the real data) and will freely perform
|
|
* removal at any location as long as that operation won't obviously
|
|
* contradict previous edits (it will attempt to recognize and prevent
|
|
* removing sub-values inside null, booleans, strings and numbers or
|
|
* non-numeric keys into arrays).
|
|
* 2. After the `SetRealData()` call it already knows what data was stored
|
|
* inside before edits were made and would only allow to write data at
|
|
* "/path/to/value" if "/path/to" (i.e. path without its last
|
|
* component) corresponds to an appropriate collection (JSON object or
|
|
* JSON array if last component is numeric or "-").
|
|
*
|
|
* @param location Location from which to remove all data.
|
|
* @return `true` if removal was successful and `false` otherwise. Note that
|
|
* operations made before `SetRealData()` can be reported as successful,
|
|
* but then rejected after the real data is set if they're incompatible
|
|
* with its structure (@see `SetRealData()` for more information).
|
|
*/
|
|
public final function bool Remove(BaseJSONPointer location)
|
|
{
|
|
local Collection cachedCollection;
|
|
|
|
if (location == none) {
|
|
return false;
|
|
}
|
|
if (!isDataCached) {
|
|
return AddPendingEdit(location, none, DBCET_Remove);
|
|
}
|
|
if (location.IsEmpty())
|
|
{
|
|
_.memory.Free(cachedData);
|
|
cachedData = none;
|
|
return true;
|
|
}
|
|
cachedCollection = Collection(cachedData);
|
|
// At this point `EditJSONCollection()`'s contract of
|
|
// `cachedCollection != none` and `location` isn't `none` or empty is
|
|
// satisfied.
|
|
if (cachedCollection != none)
|
|
{
|
|
return EditJSONCollection(
|
|
cachedCollection,
|
|
location,
|
|
none,
|
|
DBCET_Remove);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Increments data inside `DBCache` at the given location.
|
|
*
|
|
* @see `_.json.Increment()`.
|
|
*
|
|
* This method can work differently depending on whether `SetRealData()` call
|
|
* was already made:
|
|
*
|
|
* 1. Before the `SetRealData()` call it basically knows nothing about
|
|
* object inside database (the real data) and will freely increment at
|
|
* any location as long as that operation won't contradict previous
|
|
* edits (it will attempt to recognize and prevent removing sub-values
|
|
* inside null, booleans, strings and numbers or non-numeric keys into
|
|
* arrays).
|
|
* 2. After the `SetRealData()` call it already knows what data was stored
|
|
* inside before edits were made and would only allow to incrementing
|
|
* data at "/path/to/value" if "/path/to" (i.e. path without its last
|
|
* component) corresponds to an appropriate collection (JSON object or
|
|
* JSON array if last component is numeric or "-").
|
|
*
|
|
* @param location Location of the value to increment with new, given data.
|
|
* @param data Data to increment with, expected to consist only of
|
|
* the JSON-compatible types. It will be copied with `_.json.Copy()`,
|
|
* so the reference won't be kept.
|
|
* @return `true` if increment was successful and `false` otherwise. Note that
|
|
* operations made before `SetRealData()` can be reported as successful,
|
|
* but then rejected after the real data is set if they're incompatible
|
|
* with its structure (@see `SetRealData()` for more information).
|
|
*/
|
|
public final function bool Increment(BaseJSONPointer location, AcediaObject data)
|
|
{
|
|
local AcediaObject incrementedRoot;
|
|
local Collection cachedCollection;
|
|
|
|
if (location == none) {
|
|
return false;
|
|
}
|
|
if (!isDataCached) {
|
|
return AddPendingEdit(location, data, DBCET_Increment);
|
|
}
|
|
cachedCollection = Collection(cachedData);
|
|
if (cachedCollection != none) {
|
|
return EditJSONCollection(
|
|
cachedCollection,
|
|
location,
|
|
data,
|
|
DBCET_Increment);
|
|
}
|
|
else if (location.IsEmpty())
|
|
{
|
|
incrementedRoot = _.json.increment(cachedData, data);
|
|
_.memory.Free(cachedData);
|
|
cachedData = incrementedRoot;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Checks whether `SetRealData()` was called.
|
|
*
|
|
* @return `true` if `SetRealData()` was called and `DBCache` is in
|
|
* second mode, working on the given, cached data instead of the edits.
|
|
* `false` otherwise.
|
|
*/
|
|
public final function bool IsRealDataSet()
|
|
{
|
|
return isDataCached;
|
|
}
|
|
|
|
/**
|
|
* Sets real data that `DBCache` must use as basis for all of its changes.
|
|
*
|
|
* Any valid changes (for which `Write()` has previously returned `true` and
|
|
* which weren't overwritten by later changes) will be reapplied to given
|
|
* object and whether applying each edit ended in success of failure will be
|
|
* reported in the returned value.
|
|
*
|
|
* Can only be called once, all subsequent call will do nothing and will return
|
|
* empty array.
|
|
*
|
|
* @param realData Data to use as basis, expected to consist only of
|
|
* the JSON-compatible types. It will be copied with `_.json.Copy()`,
|
|
* so the reference won't be kept.
|
|
* @return All valid edits made so far (minus impossible and overwritten ones)
|
|
* in order they were made: the lower index, the older the edit. Includes
|
|
* a boolean flag that indicates whether each particular edit was
|
|
* successfully applied to given data.
|
|
*/
|
|
public final function array<PendingEdit> SetRealData(AcediaObject realData)
|
|
{
|
|
local int i;
|
|
local Collection cachedCollection;
|
|
local array<PendingEdit> pendingEditsCopy;
|
|
|
|
if (isDataCached) {
|
|
return pendingEdits;
|
|
}
|
|
cachedData = _.json.Copy(realData);
|
|
for (i = 0; i < pendingEdits.length; i += 1)
|
|
{
|
|
cachedCollection = Collection(cachedData);
|
|
if ( pendingEdits[i].location.IsEmpty()
|
|
&& pendingEdits[i].type != DBCET_Increment)
|
|
{
|
|
// Generally `pendingEdits[i].location.IsEmpty()` should be `true`
|
|
// for non-incrementing operations only for one index, since all
|
|
// edits would get overwritten be overwritten by the newest one,
|
|
// but let's do these changes just in case.
|
|
_.memory.Free(cachedData);
|
|
if (pendingEdits[i].type == DBCET_Write)
|
|
{
|
|
pendingEdits[i].successful = true;
|
|
cachedData = _.json.Copy(pendingEdits[i].data);
|
|
}
|
|
else // (pendingEdits[i].type == DBCET_Remove)
|
|
{
|
|
pendingEdits[i].successful = (cachedData != none);
|
|
cachedData = none;
|
|
}
|
|
}
|
|
else if (cachedCollection != none)
|
|
{
|
|
// Any other edits affect sub-objects and can, therefore, only be
|
|
// applied to `Collection`s (JSON objects and arrays).
|
|
pendingEdits[i].successful = EditJSONCollection(
|
|
cachedCollection,
|
|
pendingEdits[i].location,
|
|
pendingEdits[i].data,
|
|
pendingEdits[i].type);
|
|
}
|
|
}
|
|
pendingEditsCopy = pendingEdits;
|
|
pendingEdits.length = 0;
|
|
isDataCached = true;
|
|
return pendingEditsCopy;
|
|
}
|
|
|
|
// For reading data when before the `SetRealData()` call.
|
|
private final function AcediaObject ReadPending(BaseJSONPointer location)
|
|
{
|
|
local int nextEditIndex;
|
|
local int newestOverridingEdit;
|
|
|
|
if (location == none) {
|
|
return none;
|
|
}
|
|
// Go from the newest to the latest edit and find newest edit that
|
|
// *completely overwrites* data at `location`.
|
|
// This can be any newest pointer that serves as prefix to `location`
|
|
// that *writes* or *removes* data.
|
|
// If there are only *append* edits, we need to take the oldest one,
|
|
// since it is possible for several appending edits to stuck on top of each
|
|
// other.
|
|
newestOverridingEdit = -1;
|
|
nextEditIndex = pendingEdits.length - 1;
|
|
while (nextEditIndex >= 0)
|
|
{
|
|
|
|
if (location.StartsWith(pendingEdits[nextEditIndex].location))
|
|
{
|
|
newestOverridingEdit = nextEditIndex;
|
|
if (pendingEdits[nextEditIndex].type != DBCET_Increment) {
|
|
break;
|
|
}
|
|
}
|
|
nextEditIndex -= 1;
|
|
}
|
|
if (newestOverridingEdit >= 0) {
|
|
return ReconstructFromEdit(location, newestOverridingEdit);
|
|
}
|
|
return none;
|
|
}
|
|
|
|
// Takes data from the given edit from `pendingEdits` as a basis and
|
|
// reapplies newer applicable edits to it.
|
|
// Assumes `location` is not `none`.
|
|
// Assumes `pendingEdits[editIndex].location` is prefix of `location`.
|
|
private final function AcediaObject ReconstructFromEdit(
|
|
BaseJSONPointer location,
|
|
int editIndex)
|
|
{
|
|
local int startIndex;
|
|
local AcediaObject result;
|
|
local Collection outerCollection;
|
|
|
|
outerCollection = Collection(pendingEdits[editIndex].data);
|
|
if (location.GetLength() == pendingEdits[editIndex].location.GetLength()) {
|
|
// In case pending edit was made at the exactly `location`, simply copy
|
|
// its data, since `location` is pointing right at it.
|
|
result = _.json.Copy(pendingEdits[editIndex].data);
|
|
}
|
|
else if (outerCollection != none)
|
|
{
|
|
// Otherwise `location` is pointing deeper than
|
|
// `pendingEdits[editIndex].location` and we need to return
|
|
// a sub-object. This means that `data` has to be a `Collection`.
|
|
// First find that `Collection` (stored inside `outerCollection`),
|
|
// pointed by `location` inside `pendingEdits[editIndex].data`
|
|
// (with removed `pendingEdits[editIndex].location` prefix).
|
|
startIndex = pendingEdits[editIndex].location.GetLength();
|
|
result = ApplyPointer(
|
|
outerCollection,
|
|
location,
|
|
startIndex,
|
|
location.GetLength());
|
|
// We can safely release our rights to keep `result` reference and
|
|
// still use it, since it is still stored `outerCollection` and won't
|
|
// get deallocated.
|
|
_.memory.Free(result);
|
|
// `startIndex` is an `out` variable that records how far
|
|
// `ApplyPointer()` was able to travel along `location`.
|
|
// We are only successful if we reached the end.
|
|
if (startIndex == location.GetLength()) {
|
|
result = _.json.Copy(result);
|
|
}
|
|
else {
|
|
result = none;
|
|
}
|
|
}
|
|
ApplyCorrectingWrites(result, location, editIndex + 1);
|
|
return result;
|
|
}
|
|
|
|
// Attempts to apply all sufficiently new (at least with index
|
|
// `startIndex`) edits to the `target`.
|
|
// Assumes `locationToCorrect` in not `none`.
|
|
private final function ApplyCorrectingWrites(
|
|
out AcediaObject target,
|
|
BaseJSONPointer locationToCorrect,
|
|
int startIndex)
|
|
{
|
|
local int i;
|
|
local Collection targetAsCollection;
|
|
local JSONPointer subLocation, nextLocation;
|
|
|
|
if (target == none) {
|
|
return;
|
|
}
|
|
for (i = startIndex; i < pendingEdits.length; i += 1)
|
|
{
|
|
nextLocation = pendingEdits[i].location;
|
|
if (!nextLocation.StartsWith(locationToCorrect)) {
|
|
continue;
|
|
}
|
|
targetAsCollection = Collection(target);
|
|
// `Collection`s (JSON arrays or objects) have to be handled
|
|
// differently, since we might need to change values stored deep within
|
|
// the `Collection`, while for other variables we can change `target`
|
|
// directly.
|
|
if (targetAsCollection != none)
|
|
{
|
|
subLocation = nextLocation.Copy(locationToCorrect.GetLength());
|
|
EditJSONCollection(
|
|
Collection(target),
|
|
subLocation,
|
|
pendingEdits[i].data,
|
|
pendingEdits[i].type);
|
|
subLocation.FreeSelf();
|
|
}
|
|
else if (nextLocation.GetLength() == locationToCorrect.GetLength())
|
|
{
|
|
EditJSONSimpleValue(
|
|
target,
|
|
pendingEdits[i].data,
|
|
pendingEdits[i].type);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Applies operation of type `editType` to the given object.
|
|
// Assumes that `target` isn't `none`.
|
|
// Makes a copy of the `value`.
|
|
private final function bool EditJSONSimpleValue(
|
|
out AcediaObject target,
|
|
AcediaObject value,
|
|
DBCacheEditType editType)
|
|
{
|
|
local AcediaObject newTarget;
|
|
|
|
if (editType == DBCET_Write) {
|
|
newTarget = _.json.Copy(value);
|
|
}
|
|
else if (editType == DBCET_Remove) {
|
|
newTarget = none;
|
|
}
|
|
else
|
|
{
|
|
newTarget = _.json.Increment(target, value);
|
|
if (newTarget == none) {
|
|
return false;
|
|
}
|
|
}
|
|
_.memory.Free(target);
|
|
target = newTarget;
|
|
}
|
|
|
|
// Applies operation of type `editType` to the object stored inside
|
|
// given `Collection`, given by `location`.
|
|
// Makes a copy of the `value`.
|
|
// Assumes that `location` can only be an empty pointer if `editType` is
|
|
// `DBCET_Increment`.
|
|
// Assumes that `target` isn't `none`.
|
|
private final function bool EditJSONCollection(
|
|
Collection target,
|
|
BaseJSONPointer location,
|
|
AcediaObject value,
|
|
DBCacheEditType editType)
|
|
{
|
|
local bool success;
|
|
local Text key;
|
|
local ArrayList arrayCollection;
|
|
local HashTable objectCollection;
|
|
local Collection innerCollection;
|
|
local MutableJSONPointer poppedLocation;
|
|
local AcediaObject valueCopy;
|
|
|
|
// Empty pointer is only allowed if we're incrementing;
|
|
if (location.IsEmpty())
|
|
{
|
|
return (editType == DBCET_Increment
|
|
&& IncrementCollection(target, value));
|
|
}
|
|
// First get `Collection` that stores data, pointed by `location`
|
|
// (which is data pointed by `location` without the last segment).
|
|
// Last segment will serve as a key in that `Collection`, so also
|
|
// keep it.
|
|
poppedLocation = location.MutableCopy();
|
|
key = poppedLocation.Pop();
|
|
innerCollection = target.GetCollectionByJSON(poppedLocation);
|
|
// Then, depending on the collection, get the actual data
|
|
arrayCollection = ArrayList(innerCollection);
|
|
objectCollection = HashTable(innerCollection);
|
|
valueCopy = _.json.Copy(value);
|
|
if (arrayCollection != none)
|
|
{
|
|
success = EditArrayList(
|
|
arrayCollection,
|
|
key,
|
|
value,
|
|
editType,
|
|
location.PeekNumeric());
|
|
}
|
|
if (objectCollection != none) {
|
|
success = EditHashTable(objectCollection, key, value, editType);
|
|
}
|
|
_.memory.Free(innerCollection);
|
|
_.memory.Free(poppedLocation);
|
|
_.memory.Free(key);
|
|
_.memory.Free(valueCopy);
|
|
return success;
|
|
}
|
|
|
|
// Assumes `collection != none` and `key != none`
|
|
// Assumes value is already copied and won't be stored anywhere else
|
|
private final function bool EditArrayList(
|
|
ArrayList collection,
|
|
Text key,
|
|
AcediaObject value,
|
|
DBCacheEditType editType,
|
|
int numericKey)
|
|
{
|
|
local AcediaObject incrementedValue;
|
|
|
|
// Only valid case of `numericKey < 0` is when `key` is "-", which can only
|
|
// be used with `DBCET_Write` to append to `collection`
|
|
if (numericKey < 0 && editType != DBCET_Write) {
|
|
return false;
|
|
}
|
|
if (editType == DBCET_Write)
|
|
{
|
|
if (numericKey >= 0) {
|
|
collection.SetItem(numericKey, value);
|
|
}
|
|
else if (key.IsEqual(P("-"))) {
|
|
collection.AddItem(value);
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
}
|
|
else if (editType == DBCET_Remove)
|
|
{
|
|
if (numericKey >= collection.GetLength()) {
|
|
return false;
|
|
}
|
|
collection.RemoveIndex(numericKey);
|
|
return true;
|
|
}
|
|
else // if (editType == DBCET_Increment)
|
|
{
|
|
if (value == none)
|
|
{
|
|
if (numericKey >= collection.GetLength()) {
|
|
collection.SetItem(numericKey, none);
|
|
}
|
|
// Incrementing by `none` is a success for any reachable value
|
|
// (including a missing one, if the immediate parent is present)
|
|
return true;
|
|
}
|
|
incrementedValue =
|
|
EfficientIncrement(collection.GetItem(numericKey), value);
|
|
if (incrementedValue != none) {
|
|
collection.SetItem(numericKey, incrementedValue);
|
|
}
|
|
_.memory.Free(incrementedValue); // `none` or moved into `collection`
|
|
return (incrementedValue != none);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Assumes `collection != none` and `key != none`
|
|
// Assumes value is already copied and won't be stored anywhere else
|
|
private final function bool EditHashTable(
|
|
HashTable collection,
|
|
Text key,
|
|
AcediaObject value,
|
|
DBCacheEditType editType)
|
|
{
|
|
local AcediaObject incrementedValue;
|
|
|
|
if (editType == DBCET_Write) {
|
|
collection.SetItem(key, value);
|
|
}
|
|
else if (editType == DBCET_Remove)
|
|
{
|
|
if (!collection.HasKey(key)) {
|
|
return false;
|
|
}
|
|
collection.RemoveItem(key);
|
|
return true;
|
|
}
|
|
else // if (editType == DBCET_Increment)
|
|
{
|
|
if (value == none)
|
|
{
|
|
if (!collection.HasKey(key)) {
|
|
collection.SetItem(key, none);
|
|
}
|
|
// Incrementing by `none` is a success for any reachable value
|
|
// (including a missing one, if the immediate parent is present)
|
|
return true;
|
|
}
|
|
incrementedValue = EfficientIncrement(collection.GetItem(key), value);
|
|
if (incrementedValue != none) {
|
|
collection.SetItem(key, incrementedValue);
|
|
}
|
|
_.memory.Free(incrementedValue); // `none` or moved into `collection`
|
|
return (incrementedValue != none);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// This method is supposed to be more efficient than
|
|
// `_.json.Increment()` because it can skip copying `valueToIncrement`
|
|
// in case it's a collection and append to it directly.
|
|
// Assumes `increment` is not `none`.
|
|
// Returning `none` means increment has failed (could only happen possible
|
|
// if `increment` is `none`, which is impossible)
|
|
private final function AcediaObject EfficientIncrement(
|
|
/*take*/ AcediaObject valueToIncrement,
|
|
AcediaObject increment)
|
|
{
|
|
local AcediaObject incrementedValue;
|
|
|
|
if (valueToIncrement == none) {
|
|
return _.json.Copy(increment);
|
|
}
|
|
// This is the "efficient part": we first try to directly append
|
|
// `increment` to `valueToIncrement`, since it can avoid unnecessary
|
|
// copying of huge collections
|
|
if ( Collection(valueToIncrement) != none
|
|
&& valueToIncrement.class == increment.class)
|
|
{
|
|
// If we're inside, then we are sure that both arguments are either
|
|
// `ArrayList`s or `HashTable`s (and not `none`!)
|
|
IncrementCollection(valueToIncrement, increment);
|
|
// We reuse `valueToIncrementAsHashTable`, so simply return reference
|
|
// we took ownership of
|
|
return valueToIncrement;
|
|
}
|
|
// Since all correct `Collection` cases were handled above, we only need to
|
|
// do the normal, "inefficient" incrementing when both arguments aren't
|
|
// `Collection`s
|
|
if (Collection(valueToIncrement) == none && Collection(increment) == none) {
|
|
incrementedValue = _.json.Increment(valueToIncrement, increment);
|
|
}
|
|
// We do not reuse either `valueToIncrement`, so we should release it
|
|
_.memory.Free(valueToIncrement);
|
|
// This will be `none` in case `_.json.Increment()` wasn't called
|
|
return incrementedValue;
|
|
|
|
}
|
|
|
|
// Increments `valueToIncrement` (changing its value) by `increment`.
|
|
// Only does work if both arguments are the same type of `Collection`.
|
|
// Returns `true` if it actually incremented `valueToIncrement` and `false`
|
|
// otherwise.
|
|
// If arguments have different types - does nothing.
|
|
private final function bool IncrementCollection(
|
|
AcediaObject valueToIncrement,
|
|
AcediaObject increment)
|
|
{
|
|
local ArrayList valueToIncrementAsArrayList, incrementAsArrayList;
|
|
local HashTable valueToIncrementAsHashTable, incrementAsHashTable;
|
|
|
|
valueToIncrementAsArrayList = ArrayList(valueToIncrement);
|
|
if (valueToIncrementAsArrayList != none)
|
|
{
|
|
incrementAsArrayList = ArrayList(increment);
|
|
if (incrementAsArrayList != none)
|
|
{
|
|
valueToIncrementAsArrayList.Append(incrementAsArrayList);
|
|
return true;
|
|
}
|
|
}
|
|
valueToIncrementAsHashTable = HashTable(valueToIncrement);
|
|
if (valueToIncrementAsHashTable != none)
|
|
{
|
|
incrementAsHashTable = HashTable(increment);
|
|
if (incrementAsHashTable != none)
|
|
{
|
|
valueToIncrementAsHashTable.Append(incrementAsHashTable);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// For writing data when before the `SetRealData()` call.
|
|
// Assumes `location` isn't `none`.
|
|
private final function bool AddPendingEdit(
|
|
BaseJSONPointer location,
|
|
AcediaObject data,
|
|
DBCacheEditType type)
|
|
{
|
|
local int i, index;
|
|
local bool isIncrementing;
|
|
local AcediaObject leafItem;
|
|
local PendingEdit newWrite;
|
|
|
|
// We basically just want to add new edit struct into `pendingEdits`
|
|
// array, but there's three additional consideration:
|
|
//
|
|
// 1. Some edits can be decided to be *impossible*: if, at the earlier
|
|
// stage, we wrote a simple type (not JSON array or object) at
|
|
// some location "/a/b" and then try to write a sub-object at
|
|
// the longer path "/a/b/c". This action is impossible and should
|
|
// be rejected outright, to prevent reading methods from reading
|
|
// data that will obviously be rejected on real object.
|
|
// NOTE: We only catch some of such cases, checking against
|
|
// the very first edit in the chain that build up data at
|
|
// `location`, since checking checking against data written by
|
|
// the other edits on top would require us to do too much work.
|
|
// 2. Some new edits can overwrite older ones: if we wrote something
|
|
// at location "/a/b/c" and then write something at "/a/b" - we can
|
|
// completely disregard and remove older edit at "/a/b/c".
|
|
// 3. Whenever we're doing incrementing edit, we want to be able to
|
|
// keep several of such edits at once (possibly on top of some
|
|
// writing edit), without them overwriting each other (as per
|
|
// previous point). There is also a special case for when we're
|
|
// writing into JSON array with pointer ending in "-" - it
|
|
// indicates adding a new element, which also makes it incrementing
|
|
// operation.
|
|
|
|
// This variable will store whether `location` ends with "-" (writing
|
|
// operation corresponds to what was discussed in point 3)
|
|
isIncrementing = (type == DBCET_Increment)
|
|
|| IsPointerAppendingToArray(location);
|
|
while (i < pendingEdits.length)
|
|
{
|
|
if (pendingEdits[i].location.StartsWith(location))
|
|
{
|
|
// Here we're in situation described in point 2, where new edit
|
|
// will overwrite `pendingEdits[i].location`.
|
|
/*isSameLength =
|
|
(location.GetLength() == pendingEdits[i].location.GetLength());*/
|
|
// Since `location` is prefix for `pendingEdits[i].location`,
|
|
// then it is either shorter or the same length. Same length here
|
|
// also means that these pointers are *identical*.
|
|
// We can prevent removal of the old rule only in situation of
|
|
// point 3, where new edit wants to increment an item (guaranteed
|
|
// by `isIncrementing`).
|
|
//
|
|
// NOTE: Here we make no check for whether we're writing into
|
|
// an object, which can result in us keeping several redundant
|
|
// edits. But we expect this to be a rare case and a fine trade off
|
|
// for skipping additional costly checks.
|
|
if (!isIncrementing)
|
|
{
|
|
FreePendingWrite(pendingEdits[i]);
|
|
pendingEdits.Remove(i, 1);
|
|
continue;
|
|
}
|
|
}
|
|
else if (pendingEdits[i].type == DBCET_Write
|
|
&& location.StartsWith(pendingEdits[i].location))
|
|
{
|
|
// Here we perform checks described in point 1: follow along
|
|
// the `location` pointer as far as possible and check if last
|
|
// known structure can at least in theory be explored by the rest
|
|
// of `location` after database's real data is loaded and set.
|
|
index = pendingEdits[i].location.GetLength();
|
|
leafItem = ApplyPointer(
|
|
pendingEdits[i].data,
|
|
location,
|
|
index,
|
|
location.GetLength() - 1);
|
|
if ( Collection(leafItem) == none
|
|
|| !IsKeyAcceptable(Collection(leafItem), location, index))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
i += 1;
|
|
}
|
|
// After all checks have passed and all older irrelevant edits were
|
|
// filtered out - add the new one.
|
|
newWrite.location = location.Copy();
|
|
newWrite.data = _.json.Copy(data);
|
|
newWrite.type = type;
|
|
pendingEdits[pendingEdits.length] = newWrite;
|
|
return true;
|
|
}
|
|
|
|
// Checks if `pointer`'s last component is "-", which denotes appending
|
|
// new item to the JSON array.
|
|
// Assumes `pointer != none`.
|
|
private final function bool IsPointerAppendingToArray(BaseJSONPointer pointer)
|
|
{
|
|
local int lastComponentIndex;
|
|
|
|
lastComponentIndex = pointer.GetLength() - 1;
|
|
if (!pointer.IsComponentArrayApplicable(lastComponentIndex)) {
|
|
return false;
|
|
}
|
|
return (pointer.GetNumericComponent(lastComponentIndex) < 0);
|
|
}
|
|
|
|
// Checks whether given key is acceptable for given collection.
|
|
// To avoid unnecessary copying the key is specified as a component of
|
|
// JSON pointer `path` with index `keyIndex`.
|
|
private final function bool IsKeyAcceptable(
|
|
Collection target,
|
|
BaseJSONPointer path,
|
|
int keyIndex)
|
|
{
|
|
local ArrayList arrayCollection;
|
|
|
|
if (HashTable(target) != none) return true;
|
|
arrayCollection = ArrayList(target);
|
|
if (arrayCollection == none) return false;
|
|
if (keyIndex >= path.GetLength()) return true;
|
|
if (path.IsComponentArrayApplicable(keyIndex)) return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
// Finds item inside `data` by using part of given JSON pointer as its own
|
|
// pointer. Part is defined as pointer given by components with indices inside
|
|
// `[from; to - 1]`.
|
|
// `from` is an `out` argument that will return index of pointer's
|
|
// component after that one used to obtained return value.
|
|
// E.g. if pointer is "/a/b/c/d" and we returned value at "/a/b", then
|
|
// `from` will contain index `2` of the component "c".
|
|
private final function AcediaObject ApplyPointer(
|
|
AcediaObject data,
|
|
BaseJSONPointer pointer,
|
|
out int from,
|
|
int to)
|
|
{
|
|
local int nextNumericKey;
|
|
local Text nextKey;
|
|
local ArrayList nextArray;
|
|
local HashTable nextObject;
|
|
|
|
if (from < 0 || from > to) return none;
|
|
if (data == none) return none;
|
|
if (pointer == none) return none;
|
|
if (to > pointer.GetLength()) return none;
|
|
|
|
// At each iteration in the `while` cycle below, `data` stores some
|
|
// reference to the next collection to "dig in" with our JSON pointer.
|
|
// This collection is normally obtained by `GetItem()` method on one of
|
|
// the iterations and, therefore, we own a reference to it that we must
|
|
// release.
|
|
// However, on the first iteration it is the same as passed argument
|
|
// and so we do not own it and cannot release it yet. We take ownership of
|
|
// it here yo hack around that issue.
|
|
data.NewRef();
|
|
while (from < to)
|
|
{
|
|
nextObject = HashTable(data);
|
|
nextArray = ArrayList(data);
|
|
// Safe use `data` (and, therefore, both `nextObject` and `nextArray`)
|
|
// after this release, since `data` is either:
|
|
// 1. an argument and was added a reference before the loop
|
|
// 2. or is stored in another collection and, therefore, has
|
|
// another reference that way.
|
|
// Choice of `_.memory.Free()` instead of `self.FreeSelf()` is
|
|
// important here, since `data` can also be equal to `none`.
|
|
//_.memory.Free(data);
|
|
if (nextObject != none)
|
|
{
|
|
nextKey = pointer.GetComponent(from);
|
|
if (!nextObject.HasKey(nextKey))
|
|
{
|
|
_.memory.Free(nextKey);
|
|
return nextObject;
|
|
}
|
|
data.FreeSelf();
|
|
data = nextObject.GetItem(nextKey);
|
|
nextkey.FreeSelf();
|
|
}
|
|
else if (nextArray != none)
|
|
{
|
|
nextNumericKey = pointer.GetNumericComponent(from);
|
|
if (nextNumericKey < 0 || nextNumericKey >= nextArray.GetLength()) {
|
|
return nextArray;
|
|
}
|
|
data.FreeSelf();
|
|
data = nextArray.GetItem(nextNumericKey);
|
|
}
|
|
else {
|
|
// Not a collection => we cannot "go in"
|
|
return data;
|
|
}
|
|
from += 1;
|
|
}
|
|
return data;
|
|
}
|
|
|
|
// Proper clean up of `PendingEdit`
|
|
private final function FreePendingWrite(PendingEdit edit)
|
|
{
|
|
_.memory.Free(edit.location);
|
|
_.memory.Free(edit.data);
|
|
}
|
|
|
|
defaultproperties
|
|
{
|
|
} |