/**
* 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 .
*/
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 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 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 SetRealData(AcediaObject realData)
{
local int i;
local Collection cachedCollection;
local array 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
{
}