@nymphjs/client
Version:
Nymph.js - Client
598 lines • 20 kB
JavaScript
import { difference, isEqual } from 'lodash-es';
import { uniqueStrings, entitiesToReferences, referencesToEntities, sortObj, } from './utils.js';
export default class Entity {
/**
* The instance of Nymph to use for queries.
*/
static nymph = {};
/**
* The lookup name for this entity.
*
* This is used for reference arrays (and sleeping references) and server
* requests.
*/
static class = 'Entity';
/**
* The instance of Nymph to use for queries.
*/
$nymph;
/**
* The entity's Globally Unique ID.
*/
guid = null;
/**
* The creation date of the entity as a high precision Unix timestamp.
*/
cdate = null;
/**
* The modified date of the entity as a high precision Unix timestamp.
*/
mdate = null;
/**
* Array of the entity's tags.
*/
tags = [];
/**
* Array of the entity's original tags (for patch).
*/
$originalTags = [];
/**
* A map of props to whether they're dirty (for patch).
*/
$dirty = {};
/**
* The data proxy handler.
*/
$dataHandler;
/**
* The actual data store.
*/
$dataStore;
/**
* The data proxy object.
*/
$data;
/**
* Whether this instance is a sleeping reference.
*/
$isASleepingReference = false;
/**
* The reference to use to wake.
*/
$sleepingReference = null;
/**
* A promise that resolved when the entity's data is wake.
*/
$wakePromise = null;
/**
* Initialize an entity.
*/
constructor(..._rest) {
this.$nymph = this.constructor.nymph;
this.$dataHandler = {
has: (data, name) => {
if (typeof name !== 'symbol' && this.$isASleepingReference) {
console.error(`Tried to check data on a sleeping reference: ${name}`);
return false;
}
return name in data;
},
get: (data, name) => {
if (typeof name !== 'symbol' && this.$isASleepingReference) {
console.error(`Tried to get data on a sleeping reference: ${name}`);
return undefined;
}
if (data.hasOwnProperty(name)) {
return data[name];
}
return undefined;
},
set: (data, name, value) => {
if (typeof name !== 'symbol' && this.$isASleepingReference) {
console.error(`Tried to set data on a sleeping reference: ${name}`);
return false;
}
if (typeof name !== 'symbol') {
this.$dirty[name] = true;
}
data[name] = value;
return true;
},
deleteProperty: (data, name) => {
if (typeof name !== 'symbol' && this.$isASleepingReference) {
console.error(`Tried to delete data on a sleeping reference: ${name}`);
return false;
}
if (data.hasOwnProperty(name)) {
this.$dirty[name] = true;
return delete data[name];
}
return true;
},
defineProperty: (data, name, descriptor) => {
if (typeof name !== 'symbol' && this.$isASleepingReference) {
console.error(`Tried to define data on a sleeping reference: ${name}`);
return false;
}
if (typeof name !== 'symbol') {
this.$dirty[name] = true;
}
Object.defineProperty(data, name, descriptor);
return true;
},
getOwnPropertyDescriptor: (data, name) => {
if (typeof name !== 'symbol' && this.$isASleepingReference) {
console.error(`Tried to get property descriptor on a sleeping reference: ${name}`);
return undefined;
}
return Object.getOwnPropertyDescriptor(data, name);
},
ownKeys: (data) => {
if (this.$isASleepingReference) {
console.error(`Tried to enumerate data on a sleeping reference.`);
return undefined;
}
return Object.getOwnPropertyNames(data);
},
};
this.$dataStore = {};
this.$data = new Proxy(this.$dataStore, this.$dataHandler);
return new Proxy(this, {
has(entity, name) {
if (typeof name !== 'string' ||
name in entity ||
name.substring(0, 1) === '$') {
return name in entity;
}
return name in entity.$data;
},
get(entity, name) {
if (typeof name !== 'string' ||
name in entity ||
name.substring(0, 1) === '$') {
return entity[name];
}
if (name in entity.$data) {
return entity.$data[name];
}
return undefined;
},
set(entity, name, value) {
if (typeof name !== 'string' ||
name in entity ||
name.substring(0, 1) === '$') {
entity[name] = value;
}
else {
entity.$data[name] = value;
}
return true;
},
deleteProperty(entity, name) {
if (name in entity) {
return delete entity[name];
}
else if (name in entity.$data) {
return delete entity.$data[name];
}
return true;
},
getPrototypeOf(entity) {
return entity.constructor.prototype;
},
defineProperty(entity, name, descriptor) {
if (typeof name !== 'string' ||
name in entity ||
name.substring(0, 1) === '$') {
Object.defineProperty(entity, name, descriptor);
}
else {
Object.defineProperty(entity.$data, name, descriptor);
}
return true;
},
getOwnPropertyDescriptor(entity, name) {
if (typeof name !== 'string' ||
name in entity ||
name.substring(0, 1) === '$') {
return Object.getOwnPropertyDescriptor(entity, name);
}
else {
return Object.getOwnPropertyDescriptor(entity.$data, name);
}
},
ownKeys(entity) {
return Object.getOwnPropertyNames(entity).concat(Object.getOwnPropertyNames(entity.$data));
},
});
}
/**
* Create or retrieve a new entity instance.
*
* Note that this will always return an entity, even if the GUID is not found.
*
* @param guid An optional GUID to retrieve.
*/
static async factory(guid) {
const cacheEntity = (guid
? this.nymph.getEntityFromCache(this, guid)
: null);
if (cacheEntity) {
return cacheEntity;
}
const entity = new this();
if (guid != null) {
entity.guid = guid;
entity.$isASleepingReference = true;
entity.$sleepingReference = [
'nymph_entity_reference',
guid,
this.class,
];
await entity.$wake();
}
return entity;
}
/**
* Create a new entity instance.
*/
static factorySync() {
return new this();
}
/**
* Create a new sleeping reference instance.
*
* Sleeping references won't retrieve their data from the server until they
* are readied with `$wake()` or a parent's `$wakeAll()`.
*
* @param reference The Nymph Entity Reference to use to wake.
* @returns The new instance.
*/
static factoryReference(reference) {
const cacheEntity = (reference[1]
? this.nymph.getEntityFromCache(this, reference[1])
: null);
const entity = cacheEntity || new this();
if (!cacheEntity) {
entity.$referenceSleep(reference);
}
return entity;
}
/**
* Call a static method on the server version of this entity.
*
* @param method The name of the method.
* @param params The parameters to call the method with.
* @returns The value that the method on the server returned.
*/
static async serverCallStatic(method, params) {
const data = await this.nymph.serverCallStatic(this.class, method,
// Turn the params into a real array, in case an arguments object was
// passed.
Array.prototype.slice.call(params));
return data.return;
}
/**
* Call a static iterator method on the server version of this entity.
*
* @param method The name of the method.
* @param params The parameters to call the method with.
* @returns An iterator that iterates over values that the method on the server yields.
*/
static async serverCallStaticIterator(method, params) {
return await this.nymph.serverCallStaticIterator(this.class, method,
// Turn the params into a real array, in case an arguments object was
// passed.
Array.prototype.slice.call(params));
}
toJSON() {
this.$check();
const obj = {
class: this.constructor.class,
guid: this.guid,
cdate: this.cdate,
mdate: this.mdate,
tags: [...this.tags],
data: {},
};
for (let [key, value] of Object.entries(this.$dataStore)) {
obj.data[key] = entitiesToReferences(value);
}
return obj;
}
$init(entityJson) {
if (entityJson == null) {
return this;
}
this.$isASleepingReference = false;
this.$sleepingReference = null;
this.guid = entityJson.guid;
this.cdate = entityJson.cdate;
this.mdate = entityJson.mdate;
this.tags = entityJson.tags;
this.$originalTags = entityJson.tags.slice(0);
this.$dirty = {};
this.$dataStore = Object.entries(entityJson.data)
.map(([key, value]) => {
this.$dirty[key] = false;
return { key, value: referencesToEntities(value, this.$nymph) };
})
.reduce((obj, { key, value }) => Object.assign(obj, { [key]: value }), {});
this.$data = new Proxy(this.$dataStore, this.$dataHandler);
this.$nymph.setEntityToCache(this.constructor, this);
return this;
}
$addTag(...tags) {
this.$check();
if (tags.length < 1) {
return;
}
this.tags = uniqueStrings([...this.tags, ...tags]);
}
$arraySearch(array, strict = false) {
this.$check();
if (!Array.isArray(array)) {
return -1;
}
for (let i = 0; i < array.length; i++) {
const curEntity = array[i];
if (strict ? this.$equals(curEntity) : this.$is(curEntity)) {
return i;
}
}
return -1;
}
async $delete() {
this.$check();
const guid = this.guid;
return (await this.$nymph.deleteEntity(this)) === guid;
}
$equals(object) {
this.$check();
if (!(object instanceof Entity)) {
return false;
}
if (this.guid || object.guid) {
if (this.guid !== object.guid) {
return false;
}
}
if (object.cdate !== this.cdate) {
return false;
}
if (object.mdate !== this.mdate) {
return false;
}
const obData = sortObj(object.toJSON());
obData.tags?.sort();
// obData.data = sortObj(obData.data);
const myData = sortObj(this.toJSON());
myData.tags?.sort();
// myData.data = sortObj(myData.data);
return isEqual(obData, myData);
}
$getPatch() {
this.$check();
if (this.guid == null) {
throw new InvalidStateError("You can't make a patch from an unsaved entity.");
}
const patch = {
guid: this.guid,
mdate: this.mdate,
class: this.constructor.class,
addTags: this.tags.filter((tag) => this.$originalTags.indexOf(tag) === -1),
removeTags: this.$originalTags.filter((tag) => this.tags.indexOf(tag) === -1),
unset: [],
set: {},
};
for (let [key, dirty] of Object.entries(this.$dirty)) {
if (dirty) {
if (key in this.$data) {
patch.set[key] = entitiesToReferences(this.$data[key]);
}
else {
patch.unset.push(key);
}
}
}
return patch;
}
$hasTag(...tags) {
this.$check();
if (!tags.length) {
return false;
}
for (let i = 0; i < tags.length; i++) {
if (this.tags.indexOf(tags[i]) === -1) {
return false;
}
}
return true;
}
$isDirty(property) {
return property in this.$dirty ? this.$dirty[property] : null;
}
$inArray(array, strict = false) {
return this.$arraySearch(array, strict) !== -1;
}
$is(object) {
this.$check();
if (!(object instanceof Entity)) {
return false;
}
if (this.guid || object.guid) {
return this.guid === object.guid;
}
else if (typeof object.toJSON !== 'function') {
return false;
}
else {
const obData = sortObj(object.toJSON());
obData.tags?.sort();
// obData.data = sortObj(obData.data);
const myData = sortObj(this.toJSON());
myData.tags?.sort();
// myData.data = sortObj(myData.data);
return isEqual(obData, myData);
}
}
async $patch() {
this.$check();
const mdate = this.mdate;
await this.$nymph.patchEntity(this);
return mdate !== this.mdate;
}
/**
* Check if this is a sleeping reference and throw an error if so.
*/
$check() {
if (this.$isASleepingReference || this.$sleepingReference != null) {
throw new EntityIsSleepingReferenceError('This entity is in a sleeping reference state. You must use .$wake() to wake it.');
}
}
/**
* Check if this is a sleeping reference.
*/
$asleep() {
return this.$isASleepingReference || this.$sleepingReference != null;
}
/**
* Wake from a sleeping reference.
*/
$wake() {
if (!this.$isASleepingReference) {
this.$wakePromise = null;
return Promise.resolve(this);
}
if (this.$sleepingReference?.[1] == null) {
throw new InvalidStateError('Tried to wake a sleeping reference with no GUID.');
}
if (!this.$wakePromise) {
this.$wakePromise = this.$nymph
.getEntityData({ class: this.constructor }, { type: '&', guid: this.$sleepingReference[1] })
.then((data) => {
if (data == null) {
const errObj = { data, textStatus: 'No data returned.' };
return Promise.reject(errObj);
}
return this.$init(data);
})
.finally(() => {
this.$wakePromise = null;
});
}
return this.$wakePromise;
}
$wakeAll(level) {
return new Promise((resolve, reject) => {
// Run this once this entity is awake.
const wakeProps = () => {
let newLevel;
// If level is undefined, keep going forever, otherwise, stop once we've
// gone deep enough.
if (level !== undefined) {
newLevel = level - 1;
}
if (newLevel !== undefined && newLevel < 0) {
resolve(this);
return;
}
const promises = [];
// Go through data looking for entities to wake.
for (let [key, value] of Object.entries(this.$data)) {
if (value instanceof Entity && value.$isASleepingReference) {
promises.push(value.$wakeAll(newLevel));
}
else if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
if (value[i] instanceof Entity &&
value[i].$isASleepingReference) {
promises.push(value[i].$wakeAll(newLevel));
}
}
}
}
if (promises.length) {
Promise.all(promises).then(() => resolve(this), (errObj) => reject(errObj));
}
else {
resolve(this);
}
};
if (this.$isASleepingReference) {
this.$wake().then(wakeProps, (errObj) => reject(errObj));
}
else {
wakeProps();
}
});
}
$referenceSleep(reference) {
this.$isASleepingReference = true;
this.guid = reference[1];
this.$sleepingReference = [...reference];
}
async $refresh() {
if (this.$isASleepingReference) {
await this.$wake();
return true;
}
if (this.guid == null) {
return false;
}
const data = await this.$nymph.getEntityData({
class: this.constructor,
}, {
type: '&',
guid: this.guid,
});
this.$init(data);
return this.guid == null ? 0 : true;
}
$removeTag(...tags) {
this.$check();
this.tags = difference(this.tags, tags);
}
async $save() {
this.$check();
await this.$nymph.saveEntity(this);
return !!this.guid;
}
async $serverCall(method, params, stateless = false) {
this.$check();
// Turn the params into a real array, in case an arguments object was
// passed.
const paramArray = Array.prototype.slice.call(params);
const data = await this.$nymph.serverCall(this, method, paramArray, stateless);
if (!stateless && data.entity) {
this.$init(data.entity);
}
return data.return;
}
$toReference() {
if (this.$isASleepingReference && this.$sleepingReference) {
return this.$sleepingReference;
}
if (this.guid == null) {
return this;
}
return [
'nymph_entity_reference',
this.guid,
this.constructor.class,
];
}
}
export class EntityIsSleepingReferenceError extends Error {
constructor(message) {
super(message);
this.name = 'EntityIsSleepingReferenceError';
}
}
export class InvalidStateError extends Error {
constructor(message) {
super(message);
this.name = 'InvalidStateError';
}
}
//# sourceMappingURL=Entity.js.map