UNPKG

@nymphjs/client

Version:

Nymph.js - Client

598 lines 20 kB
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