From 60f542e64e7e8566be7c579afcc97e7ca980079e Mon Sep 17 00:00:00 2001 From: ThaumRystra <9525416+ThaumRystra@users.noreply.github.com> Date: Thu, 28 Sep 2023 23:00:36 +0200 Subject: [PATCH] Migrated loadCreatures to nested sets --- .../applyPropertyByType/applyBuffRemover.js | 4 +- .../applyPropertyByType/applyItemAsAmmo.js | 4 +- .../api/engine/actions/applyTriggers.ts | 4 +- app/imports/api/engine/actions/doAction.js | 6 +- app/imports/api/engine/actions/doCastSpell.js | 6 +- .../{loadCreatures.js => loadCreatures.ts} | 142 +++++++++--------- app/imports/api/parenting/ChildSchema.ts | 26 ++-- .../api/parenting/parentingFunctions.ts | 19 ++- app/package.json | 5 +- 9 files changed, 106 insertions(+), 110 deletions(-) rename app/imports/api/engine/{loadCreatures.js => loadCreatures.ts} (69%) diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyBuffRemover.js b/app/imports/api/engine/actions/applyPropertyByType/applyBuffRemover.js index 9e19e3d3..3b69c230 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyBuffRemover.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyBuffRemover.js @@ -1,7 +1,7 @@ import { findLast, difference, intersection, filter } from 'lodash'; import applyProperty from '../applyProperty'; import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers'; -import { getProperyAncestors, getPropertiesOfType } from '/imports/api/engine/loadCreatures'; +import { getPropertyAncestors, getPropertiesOfType } from '/imports/api/engine/loadCreatures'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; import { softRemove } from '/imports/api/parenting/softRemove'; import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags'; @@ -21,7 +21,7 @@ export default function applyBuffRemover(node, actionContext) { // Remove buffs if (prop.targetParentBuff) { // Remove nearest ancestor buff - const ancestors = getProperyAncestors(actionContext.creature._id, prop._id); + const ancestors = getPropertyAncestors(actionContext.creature._id, prop._id); const nearestBuff = findLast(ancestors, ancestor => ancestor.type === 'buff'); if (!nearestBuff) { actionContext.addLog({ diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyItemAsAmmo.js b/app/imports/api/engine/actions/applyPropertyByType/applyItemAsAmmo.js index a63822b8..9f25fd7c 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyItemAsAmmo.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyItemAsAmmo.js @@ -1,4 +1,4 @@ -import { getPropertyDecendants } from '/imports/api/engine/loadCreatures'; +import { getPropertyDescendants } from '/imports/api/engine/loadCreatures'; import applyProperty from '../applyProperty'; import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers'; import { docsToForest as nodeArrayToTree } from '/imports/api/parenting/parentingFunctions'; @@ -8,7 +8,7 @@ export default function applyItemAsAmmo(node, actionContext) { // The item node should come without children, since it is not part of the original action tree const prop = node.doc // Get all the item's descendant properties - const properties = getPropertyDecendants(actionContext.creature._id, prop._id); + const properties = getPropertyDescendants(actionContext.creature._id, prop._id); properties.sort((a, b) => a.order - b.order); const propertyForest = nodeArrayToTree(properties); diff --git a/app/imports/api/engine/actions/applyTriggers.ts b/app/imports/api/engine/actions/applyTriggers.ts index e1de4586..60f9d555 100644 --- a/app/imports/api/engine/actions/applyTriggers.ts +++ b/app/imports/api/engine/actions/applyTriggers.ts @@ -1,6 +1,6 @@ import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation'; import recalculateInlineCalculations from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateInlineCalculations'; -import { getPropertyDecendants } from '/imports/api/engine/loadCreatures'; +import { getPropertyDescendants } from '/imports/api/engine/loadCreatures'; import { TreeNode, docsToForest as nodeArrayToTree } from '/imports/api/parenting/parentingFunctions'; import applyProperty from '/imports/api/engine/actions/applyProperty'; import { difference, intersection } from 'lodash'; @@ -68,7 +68,7 @@ export function applyTrigger(trigger, prop, actionContext) { if (!trigger.silent) actionContext.addLog(content); // Get all the trigger's properties and apply them - const properties = getPropertyDecendants(actionContext.creature._id, trigger._id); + const properties = getPropertyDescendants(actionContext.creature._id, trigger._id); properties.sort((a, b) => a.order - b.order); const propertyForest = nodeArrayToTree(properties); propertyForest.forEach(node => { diff --git a/app/imports/api/engine/actions/doAction.js b/app/imports/api/engine/actions/doAction.js index e714d2c9..e07bcf50 100644 --- a/app/imports/api/engine/actions/doAction.js +++ b/app/imports/api/engine/actions/doAction.js @@ -4,7 +4,7 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions'; import { docsToForest } from '/imports/api/parenting/parentingFunctions'; import { - getProperyAncestors, getPropertyDecendants + getPropertyAncestors, getPropertyDescendants } from '/imports/api/engine/loadCreatures'; import Creatures from '/imports/api/creature/creatures/Creatures'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; @@ -49,10 +49,10 @@ const doAction = new ValidatedMethod({ assertEditPermission(target, this.userId); }); - const ancestors = getProperyAncestors(creatureId, action._id); + const ancestors = getPropertyAncestors(creatureId, action._id); ancestors.sort((a, b) => a.order - b.order); - const properties = getPropertyDecendants(creatureId, action._id); + const properties = getPropertyDescendants(creatureId, action._id); properties.push(action); properties.sort((a, b) => a.order - b.order); diff --git a/app/imports/api/engine/actions/doCastSpell.js b/app/imports/api/engine/actions/doCastSpell.js index 0586114d..022000bb 100644 --- a/app/imports/api/engine/actions/doCastSpell.js +++ b/app/imports/api/engine/actions/doCastSpell.js @@ -3,7 +3,7 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import Creatures from '/imports/api/creature/creatures/Creatures'; import { - getProperyAncestors, getPropertyDecendants + getPropertyAncestors, getPropertyDescendants } from '/imports/api/engine/loadCreatures'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions'; @@ -57,10 +57,10 @@ const doAction = new ValidatedMethod({ assertEditPermission(target, this.userId); }); - const ancestors = getProperyAncestors(creatureId, spell._id); + const ancestors = getPropertyAncestors(creatureId, spell._id); ancestors.sort((a, b) => a.order - b.order); - const properties = getPropertyDecendants(creatureId, spell._id); + const properties = getPropertyDescendants(creatureId, spell._id); properties.push(spell); properties.sort((a, b) => a.order - b.order); diff --git a/app/imports/api/engine/loadCreatures.js b/app/imports/api/engine/loadCreatures.ts similarity index 69% rename from app/imports/api/engine/loadCreatures.js rename to app/imports/api/engine/loadCreatures.ts index 6394d43f..65b51093 100644 --- a/app/imports/api/engine/loadCreatures.js +++ b/app/imports/api/engine/loadCreatures.ts @@ -1,17 +1,18 @@ import { debounce } from 'lodash'; import Creatures from '/imports/api/creature/creatures/Creatures'; import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import CreatureProperties, { CreatureProperty } from '/imports/api/creature/creatureProperties/CreatureProperties'; import computeCreature from './computeCreature'; +import { getFilter } from '/imports/api/parenting/parentingFunctions'; const COMPUTE_DEBOUNCE_TIME = 100; // ms -export const loadedCreatures = new Map(); // creatureId => {creature, properties, etc.} +export const loadedCreatures: Map = new Map(); // creatureId => {creature, properties, etc.} // TODO: migrate to nested sets -export function loadCreature(creatureId, subscription) { +export function loadCreature(creatureId: string, subscription: Tracker.Computation) { if (!creatureId) throw 'creatureId is required'; let creature = loadedCreatures.get(creatureId); - if (loadedCreatures.has(creatureId)) { + if (creature) { creature.subs.add(subscription); } else { creature = new LoadedCreature(subscription, creatureId); @@ -33,75 +34,67 @@ function unloadCreature(creatureId, subscription) { } } -export function getSingleProperty(creatureId, propertyId) { - if (loadedCreatures.has(creatureId)) { - const creature = loadedCreatures.get(creatureId); - const property = creature.properties.get(propertyId); - const cloneProp = EJSON.clone(property); - return cloneProp; +export function getSingleProperty(creatureId: string, propertyId: string) { + const creature = loadedCreatures.get(creatureId) + const property = creature?.properties.get(propertyId); + if (property) { + return EJSON.clone(property); } // console.time(`Cache miss on creature properties: ${creatureId}`) const prop = CreatureProperties.findOne({ _id: propertyId, - 'ancestors.id': creatureId, + 'root.id': creatureId, 'removed': { $ne: true }, - }, { - sort: { order: 1 }, }); // console.timeEnd(`Cache miss on creature properties: ${creatureId}`); return prop; } export function getProperties(creatureId) { - if (loadedCreatures.has(creatureId)) { - const creature = loadedCreatures.get(creatureId); + const creature = loadedCreatures.get(creatureId); + if (creature) { const props = Array.from(creature.properties.values()); - const cloneProps = EJSON.clone(props); - return cloneProps + return EJSON.clone(props); } // console.time(`Cache miss on creature properties: ${creatureId}`) const props = CreatureProperties.find({ - 'ancestors.id': creatureId, + 'root.id': creatureId, 'removed': { $ne: true }, }, { - sort: { order: 1 }, + sort: { left: 1 }, }).fetch(); // console.timeEnd(`Cache miss on creature properties: ${creatureId}`); return props; } export function getPropertiesOfType(creatureId, propType) { - if (loadedCreatures.has(creatureId)) { - const creature = loadedCreatures.get(creatureId); - const props = [] + const creature = loadedCreatures.get(creatureId); + if (creature) { + const props: CreatureProperty[] = [] for (const prop of creature.properties.values()) { if (prop.type === propType) { props.push(prop); } } - const cloneProps = EJSON.clone(props); - return cloneProps + return EJSON.clone(props); } // console.time(`Cache miss on creature properties: ${creatureId}`) const props = CreatureProperties.find({ - 'ancestors.id': creatureId, + 'root.id': creatureId, 'removed': { $ne: true }, 'type': propType, }, { - sort: { order: 1 }, + sort: { left: 1 }, }).fetch(); // console.timeEnd(`Cache miss on creature properties: ${creatureId}`); return props; } export function getCreature(creatureId) { - if (loadedCreatures.has(creatureId)) { - const loadedCreature = loadedCreatures.get(creatureId); - const creature = loadedCreature.creature; - if (creature) { - const cloneCreature = EJSON.clone(creature); - return cloneCreature; - } + const loadedCreature = loadedCreatures.get(creatureId); + const loadedCreatureDoc = loadedCreature?.creature; + if (loadedCreatureDoc) { + return EJSON.clone(loadedCreatureDoc); } // console.time(`Cache miss on Creature: ${creatureId}`); const creature = Creatures.findOne(creatureId); @@ -110,13 +103,10 @@ export function getCreature(creatureId) { } export function getVariables(creatureId) { - if (loadedCreatures.has(creatureId)) { - const loadedCreature = loadedCreatures.get(creatureId); - const variables = loadedCreature.variables; - if (variables) { - const cloneVarables = EJSON.clone(variables); - return cloneVarables; - } + const loadedCreature = loadedCreatures.get(creatureId); + const loadedVariables = loadedCreature?.variables; + if (loadedVariables) { + return EJSON.clone(loadedVariables); } // console.time(`Cache miss on variables: ${creatureId}`); const variables = CreatureVariables.findOne({ _creatureId: creatureId }); @@ -124,49 +114,44 @@ export function getVariables(creatureId) { return variables; } -export function getProperyAncestors(creatureId, propertyId) { +export function getPropertyAncestors(creatureId: string, propertyId: string) { const prop = getSingleProperty(creatureId, propertyId); if (!prop) return []; - const ancestorIds = []; - prop.ancestors.forEach(ref => { - if (ref.collection === 'creatureProperties') { - ancestorIds.push(ref.id); - } - }); - if (loadedCreatures.has(creatureId)) { + const loadedCreature = loadedCreatures.get(creatureId); + if (loadedCreature) { // Get the ancestor properties from the cache - const creature = loadedCreatures.get(creatureId); - const props = []; - ancestorIds.forEach(id => { - const prop = creature.properties.get(id); - if (prop) { - props.push(prop); - } - }); - const cloneProps = EJSON.clone(props); - return cloneProps + const props: CreatureProperty[] = []; + let currentProp: CreatureProperty | undefined = prop; + // Iterate through parent chain to get all linked ancestors + while (currentProp?.parentId) { + currentProp = getSingleProperty(creatureId, currentProp.parentId); + if (currentProp) props.push(currentProp); + } + return EJSON.clone(props); } else { // Fetch from database return CreatureProperties.find({ - _id: { $in: ancestorIds }, + ...getFilter.ancestors(prop), removed: { $ne: true }, }, { - sort: { order: 1 }, + sort: { left: 1 } }).fetch(); } } -export function getPropertyDecendants(creatureId, propertyId) { +export function getPropertyDescendants(creatureId, propertyId) { const property = getSingleProperty(creatureId, propertyId); if (!property) return []; - // This prop will always appear at the same position in the ancestor array - // of its decendants, so only check there - const expectedAncestorPostition = property.ancestors.length; if (loadedCreatures.has(creatureId)) { const creature = loadedCreatures.get(creatureId); - const props = []; + if (!creature) return []; + const props: CreatureProperty[] = []; + // Loop through all properties and find ones that match the nested set condition for (const prop of creature.properties.values()) { - if (prop.ancestors[expectedAncestorPostition]?.id === propertyId) { + if ( + prop.left > property.left + && prop.right < property.right + ) { props.push(prop); } } @@ -174,23 +159,30 @@ export function getPropertyDecendants(creatureId, propertyId) { return cloneProps } else { return CreatureProperties.find({ - 'ancestors.id': propertyId, + ...getFilter.descendants(property), removed: { $ne: true }, }, { - sort: { order: 1 }, + sort: { left: 1 }, }).fetch(); } } class LoadedCreature { + subs: Set; + propertyObserver: Meteor.LiveQueryHandle; + creatureObserver: Meteor.LiveQueryHandle; + variablesObserver: Meteor.LiveQueryHandle; + properties: Map; + creature: any; + variables: any; + constructor(sub, creatureId) { + const self = this; // This may be called from a subscription, but we don't want the observers // to be destroyed with it, so use a non-reactive context to observe // the required documents - const self = this; Tracker.nonreactive(() => { self.subs = new Set([sub]); - const compute = debounce(Meteor.bindEnvironment(() => { computeCreature(creatureId); }), COMPUTE_DEBOUNCE_TIME); @@ -231,8 +223,8 @@ class LoadedCreature { self.changeCreature(id, fields); if (fields.dirty) compute(); }, - removed(id) { - self.removeCreature(id); + removed() { + self.removeCreature(); }, }); @@ -249,8 +241,8 @@ class LoadedCreature { changed(id, fields) { self.changeVariables(id, fields); }, - removed(id) { - self.removeVariables(id); + removed() { + self.removeVariables(); }, }); }); @@ -293,7 +285,7 @@ class LoadedCreature { } static changeDoc(doc, fields) { if (!doc) return; - for (let key in fields) { + for (const key in fields) { if (key === undefined) { delete doc[key]; } else { diff --git a/app/imports/api/parenting/ChildSchema.ts b/app/imports/api/parenting/ChildSchema.ts index 4c293f8a..027c1db0 100644 --- a/app/imports/api/parenting/ChildSchema.ts +++ b/app/imports/api/parenting/ChildSchema.ts @@ -1,6 +1,19 @@ import SimpleSchema from 'simpl-schema'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +export interface Reference { + collection: string, + id: string, +} + +export interface TreeDoc { + _id: string, + root: Reference, + parentId?: string, + left: number, + right: number, +} + const RefSchema = new SimpleSchema({ id: { type: String, @@ -57,19 +70,6 @@ const ChildSchema = new SimpleSchema({ } }); -export interface Reference { - collection: string, - id: string, -} - -export interface TreeDoc { - _id: string, - root: Reference, - parentId?: string, - left: number, - right: number, -} - export const treeDocFields = { _id: 1, root: 1, diff --git a/app/imports/api/parenting/parentingFunctions.ts b/app/imports/api/parenting/parentingFunctions.ts index 355cda7c..223c7a53 100644 --- a/app/imports/api/parenting/parentingFunctions.ts +++ b/app/imports/api/parenting/parentingFunctions.ts @@ -80,7 +80,7 @@ type FilteredDoc = { export default async function filterToForest( collection: Mongo.Collection, rootId: string, - filter: Mongo.Query, + filter: Mongo.Selector, options: Mongo.Options = {}, includeFilteredDocAncestors = false, includeFilteredDocDescendants = false @@ -200,14 +200,14 @@ export const getFilter = { * @param doc A document or array of documents that share a root * @returns A query filter that finds all the ancestors of the doc(s) */ - ancestors(doc: TreeDoc): Mongo.Query { + ancestors(doc: TreeDoc) { return { 'root.id': doc.root.id, left: { $lt: doc.left }, right: { $gt: doc.right }, }; }, - ancestorsOfAll(docs: Array): Mongo.Query { + ancestorsOfAll(docs: Array) { // The ancestors of no documents is a query that returns nothing if (docs.length === 0) { return { _id: '' }; @@ -229,14 +229,14 @@ export const getFilter = { }); return filter; }, - descendants(doc: TreeDoc): Mongo.Query { + descendants(doc: TreeDoc) { return { 'root.id': doc.root.id, left: { $gt: doc.left }, right: { $lt: doc.right }, }; }, - descendantsOfAll(docs: Array): Mongo.Query { + descendantsOfAll(docs: Array) { // The descendants of no documents is a query that returns nothing if (docs.length === 0) { return { _id: '' }; @@ -248,7 +248,10 @@ export const getFilter = { // Build a filter that selects all descendants const filter = { 'root.id': docs[0].root.id, - $or: [], + $or: <{ + left: { $gt: number }, + right: { $lt: number }, + }[]>[], }; docs.forEach(doc => { filter.$or.push({ @@ -258,13 +261,13 @@ export const getFilter = { }); return filter; }, - children(doc: TreeDoc): Mongo.Query { + children(doc: TreeDoc) { return { 'root.id': doc.root.id, parentId: doc._id, }; }, - parent(doc: TreeDoc): Mongo.Query { + parent(doc: TreeDoc) { return { _id: doc.parentId, }; diff --git a/app/package.json b/app/package.json index 4a97e9e8..8510dfc8 100644 --- a/app/package.json +++ b/app/package.json @@ -123,7 +123,8 @@ "quotes": [ "error", "single" - ] + ], + "@typescript-eslint/no-this-alias": "off" } } -} +} \ No newline at end of file