@pilotlab/data
Version:
A luxurious user experience framework, developed by your friends at Pilot.
281 lines (222 loc) • 10.8 kB
text/typescript
import is from "@pilotlab/is";
import { NodeType } from '@pilotlab/nodes';
import { Debug } from '@pilotlab/debug';
import { ProgressTracker } from '@pilotlab/progress';
import { IPromise, Result } from '@pilotlab/result';
import { Signal } from '@pilotlab/signals';
import { DataType } from './attributes/attributeEnums';
import IAttribute from './attributes/interfaces/iAttribute';
import IAttributes from './attributes/interfaces/iAttributes';
import IAttributeChangeOptions from './attributes/interfaces/iAttributeChangeOptions';
import IAttributeUpdateTracker from './attributes/interfaces/iAttributeUpdateTracker';
import AttributeBase from './attributes/attributeBase';
import AttributeEventArgs from './attributes/attributeEventArgs';
import AttributeRoot from './attributes/attributeRoot';
import Attributes from './attributes/attributes';
import AttributeChangeOptions from './attributes/attributeChangeOptions';
import AttributeCreateOptions from './attributes/attributeCreateOptions';
import IData from './interfaces/iData';
import IDataStore from './interfaces/iDataStore';
import { Identifier } from '@pilotlab/ids';
import { KeyAutoGenerationType } from "./dataEnums";
import { SHA1 } from '@pilotlab/sha1';
import { ThrottlingCommand } from '@pilotlab/commands';
Debug.setCategoryMode('data_save', false);
export class Data
extends AttributeBase<Attributes, AttributeBase<any, any, any, any>, Attributes, AttributeRoot>
implements IData {
constructor(
data?:(Data | IAttributes | Object | string),
isStoreKey:boolean = true,
dataStore?:IDataStore,
isSaveDataTypes:boolean = false,
isAutoSave:boolean = false,
key?:string,
label?:string,
progressTracker?:ProgressTracker,
isInitialize:boolean = true
) {
super(AttributeRoot.create, data, DataType.COLLECTION, label, key, NodeType.ROOT, false);
this.isStoreKey = isStoreKey;
if (isInitialize) this.initialize(data, dataStore, isSaveDataTypes, isAutoSave, progressTracker);
}
initialize(
data?:(Data | IAttributes | Object | string),
dataStore?:IDataStore,
isSaveDataTypes:boolean = false,
isAutoSave:boolean = false,
progressTracker?:ProgressTracker
):IPromise<any> {
return super.p_initialize([data, dataStore, isSaveDataTypes, isAutoSave, progressTracker]);
}
protected p_onInitializeStarted(result:Result<any>, args:any[]):IPromise<any> {
const data:(Data | Attributes | Object | string) = args[0];
const dataStore:IDataStore = args[1];
const isSaveDataTypes:boolean = args[2];
const isAutoSave:boolean = args[3];
const progressTracker:ProgressTracker = args[4];
super.p_onInitializeStarted(new Result<any>(), [data, DataType.COLLECTION, NodeType.ROOT]).then(() => {
this.p_value.changed.listen((args:AttributeEventArgs<Data>) => { this.p_onChanged(args); });
if (is.notEmpty(dataStore)) this.store = dataStore;
if (!this.isStoreKey) {
if (this.p_keyAutoGenerationType === KeyAutoGenerationType.SESSION_UNIQUE) {
this.p_key = Identifier.getSessionUniqueInteger().toString();
} else this.p_key = Identifier.generate();
} else {
this.p_key = Identifier.generate();
}
this._isSaveDataTypes = isSaveDataTypes;
this._isAutoSave = isAutoSave;
this._saveCommand = new ThrottlingCommand(
():IPromise<any> => {
let result:Result<boolean> = new Result<boolean>();
let data:any = this.toObject(this.isSaveDataTypes);
if (is.empty(data) || is.empty(this.key) || typeof this.key !== 'string') return result.resolve(false);
//----- Reset _isAutoSaveQueued
this._isAutoSaveQueued = false;
if (!this.hash.omitFromDataStore && this.p_isAutoHash) {
SHA1.hash(JSON.stringify(data)).then((hash:string) => {
data['hash'] = hash;
Debug.log('saving to database', 'Data._saveCommand', 'data_save');
Debug.log(data, 'Data._saveCommand', 'data_save');
this._dataStore.save(data, <string>this.key).then((response:any) => {
if (is.notEmpty(response) && typeof response === 'string') this._revision = response;
result.resolve(response);
this.saved.dispatch(response);
});
});
} else {
Debug.log('saving to database', 'Data._saveCommand', 'data_save');
Debug.log(data, 'Data._saveCommand', 'data_save');
this._dataStore.save(data, <string>this.key).then((response:any) => {
if (is.notEmpty(response) && typeof response === 'string') this._revision = response;
result.resolve(response);
this.saved.dispatch(response);
});
}
return result;
},
null, this, this._saveDebounceDelayInMilliseconds
);
this.p_value.update(data, AttributeChangeOptions.signal.durationZero, progressTracker).then(() => result.resolve(data));
});
return result;
}
/*====================================================================*
START: Properties
*====================================================================*/
/**
* If the key is stored, this will be a globally unique ID, used to identify the data
* object associated with this entity in the data store.
*/
set key(value:string) {
this.p_key = value;
if (this.isStoreKey) this.save();
}
isStoreKey:boolean = true;
protected p_keyAutoGenerationType:KeyAutoGenerationType = KeyAutoGenerationType.SESSION_UNIQUE;
/**
* The revision ID, which may be managed by the database and can be used to determine
* if the data associated with this entity is up-to-date.
*/
get revision():string { return this._revision; }
private _revision:string;
/**
* An SHA-1 hash representing the precise contents of the associated blob or text.
* Can be used to check the uniqueness of a data object to avoid storing duplicates.
*/
get hash():IAttribute {
return this.p_value.get('hash', new AttributeCreateOptions('', DataType.STRING, null, null, null, AttributeChangeOptions.zero));
}
get isAutoHash():boolean { return this.p_isAutoHash; }
set isAutoHash(value:boolean) { this.p_isAutoHash = value; }
protected p_isAutoHash:boolean = false;
//===== DataStore
/**
* The data store where the data associated with this entity will be stored.
*/
get store():IDataStore { return this._dataStore; }
set store(value:IDataStore) {
this._dataStore = value;
this.storeSet.dispatch(value);
}
private _dataStore:IDataStore;
//===== Save
/**
* Determines whether a dataType property is saved with the data objects,
* which can be used to verify the data's type when the object is deserialized.
*/
get isSaveDataTypes():boolean { return this._isSaveDataTypes; }
set isSaveDataTypes(value:boolean) { this._isSaveDataTypes = value; }
private _isSaveDataTypes:boolean;
/**
* Toggles auto-save functionality.
* Can be used to pause auto-saving while changes to the data are batched.
*/
get isAutoSave():boolean { return this._isAutoSave; }
set isAutoSave(value:boolean) {
if (this._isAutoSave === value) return;
this._isAutoSave = value;
if (value && this._isAutoSaveQueued) this.save();
}
private _isAutoSave:boolean = false;
/**
* This will be set to true if autoSave() was called while isAutoSave was false.
* This triggers a save of the data immediately after isAutoSave is set to true.
*/
private _isAutoSaveQueued:boolean = false;
/*====================================================================*
START: Signals
*====================================================================*/
storeSet:Signal<IDataStore> = new Signal<IDataStore>(true);
/**
* Dispatched after a response is received back from the data store when data has been saved.
* Completes with the response from the data store.
*/
saved:Signal<any> = new Signal<any>(false);
protected p_onChanged(args:AttributeEventArgs<Data>):void {
if (is.empty(args)) return;
if (args.changeOptions.isSave && is.notEmpty(this.key)) {
this.autoSave();
this.saveTriggered.dispatch(args);
}
}
/*====================================================================*
START: Signals
*====================================================================*/
saveTriggered:Signal<AttributeEventArgs<Data>> = new Signal<AttributeEventArgs<Data>>(false);
/*====================================================================*
START: Public Methods
*====================================================================*/
update(
data:(Data | Attributes | Object | string),
changeOptions:IAttributeChangeOptions = AttributeChangeOptions.default,
progressTracker?:ProgressTracker
):IAttributeUpdateTracker {
return this.p_value.update(data, changeOptions, progressTracker);
}
//===== Save
private _saveDebounceDelayInMilliseconds:number = 125;
private _saveCommand:ThrottlingCommand;
save():IPromise<any> {
if (is.empty(this._dataStore)) {
//----- Save immediately after the data store has been set.
this.storeSet.listenOnce(() => this.save());
return Result.resolve<any>(null);
}
return this._saveCommand.debounce();
}
autoSave():void {
if (!this.isAutoSave) this._isAutoSaveQueued = true;
else if (is.notEmpty(this.key)) this.save();
}
delete():IPromise<boolean> {
if (is.empty(this.key) || typeof this.key !== 'string') return Result.resolve<boolean>(false);
let result = new Result<boolean>();
this._dataStore.delete(<string>this.key).then((isSuccess:boolean) => {
result.resolve(isSuccess);
}, (e:Error) => { result.reject(e); });
return result;
}
} // End of class
export default Data;