Files
Foundry-VTT-Docker/resources/app/common/abstract/embedded-collection-delta.mjs

248 lines
7.9 KiB
JavaScript
Raw Normal View History

2025-01-04 00:34:03 +01:00
import EmbeddedCollection from "./embedded-collection.mjs";
import {deepClone, randomID} from "../utils/helpers.mjs";
/**
* An embedded collection delta contains delta source objects that can be compared against other objects inside a base
* embedded collection, and generate new embedded Documents by combining them.
*/
export default class EmbeddedCollectionDelta extends EmbeddedCollection {
/**
* Maintain a list of IDs that are managed by this collection delta to distinguish from those IDs that are inherited
* from the base collection.
* @type {Set<string>}
*/
#managedIds = new Set();
/* -------------------------------------------- */
/**
* Maintain a list of IDs that are tombstone Documents.
* @type {Set<string>}
*/
#tombstones = new Set();
/* -------------------------------------------- */
/**
* A convenience getter to return the corresponding base collection.
* @type {EmbeddedCollection}
*/
get baseCollection() {
return this.model.getBaseCollection?.(this.name);
}
/* -------------------------------------------- */
/**
* A convenience getter to return the corresponding synthetic collection.
* @type {EmbeddedCollection}
*/
get syntheticCollection() {
return this.model.syntheticActor?.getEmbeddedCollection(this.name);
}
/* -------------------------------------------- */
/** @override */
createDocument(data, context={}) {
return new this.documentClass(data, {
...context,
parent: this.model.syntheticActor ?? this.model,
parentCollection: this.name,
pack: this.model.pack
});
}
/* -------------------------------------------- */
/** @override */
initialize({full=false, ...options} = {}) {
// Repeat initialization.
if ( this._initialized && !full ) return;
// First-time initialization.
this.clear();
if ( !this.baseCollection ) return;
// Initialize the deltas.
for ( const d of this._source ) {
if ( d._tombstone ) this.#tombstones.add(d._id);
else this._initializeDocument(d, options);
this.#managedIds.add(d._id);
}
// Include the Documents from the base collection.
for ( const d of this.baseCollection._source ) {
if ( this.has(d._id) || this.isTombstone(d._id) ) continue;
this._initializeDocument(deepClone(d), options);
}
this._initialized = true;
}
/* -------------------------------------------- */
/** @override */
_initializeDocument(data, context) {
if ( !data._id ) data._id = randomID(16);
let doc;
if ( this.syntheticCollection ) doc = this.syntheticCollection.get(data._id);
else {
try {
doc = this.createDocument(data, context);
} catch(err) {
this._handleInvalidDocument(data._id, err, context);
}
}
if ( doc ) super.set(doc.id, doc, {modifySource: false});
}
/* -------------------------------------------- */
/** @override */
_createOrUpdate(data, options) {
if ( options.recursive === false ) {
if ( data._tombstone ) return this.delete(data._id);
else if ( this.isTombstone(data._id) ) return this.set(data._id, this.createDocument(data));
}
else if ( this.isTombstone(data._id) || data._tombstone ) return;
let doc = this.get(data._id);
if ( doc ) doc.updateSource(data, options);
else doc = this.createDocument(data);
this.set(doc.id, doc);
}
/* -------------------------------------------- */
/**
* Determine whether a given ID is managed directly by this collection delta or inherited from the base collection.
* @param {string} key The Document ID.
* @returns {boolean}
*/
manages(key) {
return this.#managedIds.has(key);
}
/* -------------------------------------------- */
/**
* Determine whether a given ID exists as a tombstone Document in the collection delta.
* @param {string} key The Document ID.
* @returns {boolean}
*/
isTombstone(key) {
return this.#tombstones.has(key);
}
/* -------------------------------------------- */
/**
* Restore a Document so that it is no longer managed by the collection delta and instead inherits from the base
* Document.
* @param {string} id The Document ID.
* @returns {Promise<Document>} The restored Document.
*/
async restoreDocument(id) {
const docs = await this.restoreDocuments([id]);
return docs.shift();
}
/* -------------------------------------------- */
/**
* Restore the given Documents so that they are no longer managed by the collection delta and instead inherit directly
* from their counterparts in the base Actor.
* @param {string[]} ids The IDs of the Documents to restore.
* @returns {Promise<Document[]>} An array of updated Document instances.
*/
async restoreDocuments(ids) {
if ( !this.model.syntheticActor ) return [];
const baseActor = this.model.parent.baseActor;
const embeddedName = this.documentClass.documentName;
const {deltas, tombstones} = ids.reduce((obj, id) => {
if ( !this.manages(id) ) return obj;
const doc = baseActor.getEmbeddedCollection(this.name).get(id);
if ( this.isTombstone(id) ) obj.tombstones.push(doc.toObject());
else obj.deltas.push(doc.toObject());
return obj;
}, {deltas: [], tombstones: []});
// For the benefit of downstream CRUD workflows, we emulate events from the perspective of the synthetic Actor.
// Restoring an Item to the version on the base Actor is equivalent to updating that Item on the synthetic Actor
// with the version of the Item on the base Actor.
// Restoring an Item that has been deleted on the synthetic Actor is equivalent to creating a new Item on the
// synthetic Actor with the contents of the version on the base Actor.
// On the ActorDelta, those Items are removed from this collection delta so that they are once again 'linked' to the
// base Actor's Item, as though they had never been modified from the original in the first place.
let updated = [];
if ( deltas.length ) {
updated = await this.model.syntheticActor.updateEmbeddedDocuments(embeddedName, deltas, {
diff: false, recursive: false, restoreDelta: true
});
}
let created = [];
if ( tombstones.length ) {
created = await this.model.syntheticActor.createEmbeddedDocuments(embeddedName, tombstones, {
keepId: true, restoreDelta: true
});
}
return updated.concat(created);
}
/* -------------------------------------------- */
/** @inheritdoc */
set(key, value, options={}) {
super.set(key, value, options);
this.syntheticCollection?.set(key, value, options);
}
/* -------------------------------------------- */
/** @override */
_set(key, value, {restoreDelta=false}={}) {
if ( restoreDelta ) {
this._source.findSplice(entry => entry._id === key);
this.#managedIds.delete(key);
this.#tombstones.delete(key);
return;
}
if ( this.manages(key) ) this._source.findSplice(d => d._id === key, value._source);
else this._source.push(value._source);
this.#managedIds.add(key);
}
/* -------------------------------------------- */
/** @inheritdoc */
delete(key, options={}) {
super.delete(key, options);
this.syntheticCollection?.delete(key, options);
}
/* -------------------------------------------- */
/** @override */
_delete(key, {restoreDelta=false}={}) {
if ( !this.baseCollection ) return;
// Remove the document from this collection, if it exists.
if ( this.manages(key) ) {
this._source.findSplice(entry => entry._id === key);
this.#managedIds.delete(key);
this.#tombstones.delete(key);
}
// If the document exists in the base collection, push a tombstone in its place.
if ( !restoreDelta && this.baseCollection.has(key) ) {
this._source.push({_id: key, _tombstone: true});
this.#managedIds.add(key);
this.#tombstones.add(key);
}
}
}