Migrated loadCreatures to nested sets

This commit is contained in:
ThaumRystra
2023-09-28 23:00:36 +02:00
parent ece4a9391a
commit 60f542e64e
9 changed files with 106 additions and 110 deletions

View File

@@ -1,7 +1,7 @@
import { findLast, difference, intersection, filter } from 'lodash'; import { findLast, difference, intersection, filter } from 'lodash';
import applyProperty from '../applyProperty'; import applyProperty from '../applyProperty';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers'; 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 CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import { softRemove } from '/imports/api/parenting/softRemove'; import { softRemove } from '/imports/api/parenting/softRemove';
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags'; import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags';
@@ -21,7 +21,7 @@ export default function applyBuffRemover(node, actionContext) {
// Remove buffs // Remove buffs
if (prop.targetParentBuff) { if (prop.targetParentBuff) {
// Remove nearest ancestor buff // 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'); const nearestBuff = findLast(ancestors, ancestor => ancestor.type === 'buff');
if (!nearestBuff) { if (!nearestBuff) {
actionContext.addLog({ actionContext.addLog({

View File

@@ -1,4 +1,4 @@
import { getPropertyDecendants } from '/imports/api/engine/loadCreatures'; import { getPropertyDescendants } from '/imports/api/engine/loadCreatures';
import applyProperty from '../applyProperty'; import applyProperty from '../applyProperty';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers'; import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers';
import { docsToForest as nodeArrayToTree } from '/imports/api/parenting/parentingFunctions'; 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 // The item node should come without children, since it is not part of the original action tree
const prop = node.doc const prop = node.doc
// Get all the item's descendant properties // 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); properties.sort((a, b) => a.order - b.order);
const propertyForest = nodeArrayToTree(properties); const propertyForest = nodeArrayToTree(properties);

View File

@@ -1,6 +1,6 @@
import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation'; import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation';
import recalculateInlineCalculations from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateInlineCalculations'; 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 { TreeNode, docsToForest as nodeArrayToTree } from '/imports/api/parenting/parentingFunctions';
import applyProperty from '/imports/api/engine/actions/applyProperty'; import applyProperty from '/imports/api/engine/actions/applyProperty';
import { difference, intersection } from 'lodash'; import { difference, intersection } from 'lodash';
@@ -68,7 +68,7 @@ export function applyTrigger(trigger, prop, actionContext) {
if (!trigger.silent) actionContext.addLog(content); if (!trigger.silent) actionContext.addLog(content);
// Get all the trigger's properties and apply them // 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); properties.sort((a, b) => a.order - b.order);
const propertyForest = nodeArrayToTree(properties); const propertyForest = nodeArrayToTree(properties);
propertyForest.forEach(node => { propertyForest.forEach(node => {

View File

@@ -4,7 +4,7 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions';
import { docsToForest } from '/imports/api/parenting/parentingFunctions'; import { docsToForest } from '/imports/api/parenting/parentingFunctions';
import { import {
getProperyAncestors, getPropertyDecendants getPropertyAncestors, getPropertyDescendants
} from '/imports/api/engine/loadCreatures'; } from '/imports/api/engine/loadCreatures';
import Creatures from '/imports/api/creature/creatures/Creatures'; import Creatures from '/imports/api/creature/creatures/Creatures';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
@@ -49,10 +49,10 @@ const doAction = new ValidatedMethod({
assertEditPermission(target, this.userId); assertEditPermission(target, this.userId);
}); });
const ancestors = getProperyAncestors(creatureId, action._id); const ancestors = getPropertyAncestors(creatureId, action._id);
ancestors.sort((a, b) => a.order - b.order); ancestors.sort((a, b) => a.order - b.order);
const properties = getPropertyDecendants(creatureId, action._id); const properties = getPropertyDescendants(creatureId, action._id);
properties.push(action); properties.push(action);
properties.sort((a, b) => a.order - b.order); properties.sort((a, b) => a.order - b.order);

View File

@@ -3,7 +3,7 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/creatures/Creatures'; import Creatures from '/imports/api/creature/creatures/Creatures';
import { import {
getProperyAncestors, getPropertyDecendants getPropertyAncestors, getPropertyDescendants
} from '/imports/api/engine/loadCreatures'; } from '/imports/api/engine/loadCreatures';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions';
@@ -57,10 +57,10 @@ const doAction = new ValidatedMethod({
assertEditPermission(target, this.userId); assertEditPermission(target, this.userId);
}); });
const ancestors = getProperyAncestors(creatureId, spell._id); const ancestors = getPropertyAncestors(creatureId, spell._id);
ancestors.sort((a, b) => a.order - b.order); ancestors.sort((a, b) => a.order - b.order);
const properties = getPropertyDecendants(creatureId, spell._id); const properties = getPropertyDescendants(creatureId, spell._id);
properties.push(spell); properties.push(spell);
properties.sort((a, b) => a.order - b.order); properties.sort((a, b) => a.order - b.order);

View File

@@ -1,17 +1,18 @@
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import Creatures from '/imports/api/creature/creatures/Creatures'; import Creatures from '/imports/api/creature/creatures/Creatures';
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables'; 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 computeCreature from './computeCreature';
import { getFilter } from '/imports/api/parenting/parentingFunctions';
const COMPUTE_DEBOUNCE_TIME = 100; // ms const COMPUTE_DEBOUNCE_TIME = 100; // ms
export const loadedCreatures = new Map(); // creatureId => {creature, properties, etc.} export const loadedCreatures: Map<string, LoadedCreature> = new Map(); // creatureId => {creature, properties, etc.}
// TODO: migrate to nested sets // TODO: migrate to nested sets
export function loadCreature(creatureId, subscription) { export function loadCreature(creatureId: string, subscription: Tracker.Computation) {
if (!creatureId) throw 'creatureId is required'; if (!creatureId) throw 'creatureId is required';
let creature = loadedCreatures.get(creatureId); let creature = loadedCreatures.get(creatureId);
if (loadedCreatures.has(creatureId)) { if (creature) {
creature.subs.add(subscription); creature.subs.add(subscription);
} else { } else {
creature = new LoadedCreature(subscription, creatureId); creature = new LoadedCreature(subscription, creatureId);
@@ -33,75 +34,67 @@ function unloadCreature(creatureId, subscription) {
} }
} }
export function getSingleProperty(creatureId, propertyId) { export function getSingleProperty(creatureId: string, propertyId: string) {
if (loadedCreatures.has(creatureId)) { const creature = loadedCreatures.get(creatureId)
const creature = loadedCreatures.get(creatureId); const property = creature?.properties.get(propertyId);
const property = creature.properties.get(propertyId); if (property) {
const cloneProp = EJSON.clone(property); return EJSON.clone(property);
return cloneProp;
} }
// console.time(`Cache miss on creature properties: ${creatureId}`) // console.time(`Cache miss on creature properties: ${creatureId}`)
const prop = CreatureProperties.findOne({ const prop = CreatureProperties.findOne({
_id: propertyId, _id: propertyId,
'ancestors.id': creatureId, 'root.id': creatureId,
'removed': { $ne: true }, 'removed': { $ne: true },
}, {
sort: { order: 1 },
}); });
// console.timeEnd(`Cache miss on creature properties: ${creatureId}`); // console.timeEnd(`Cache miss on creature properties: ${creatureId}`);
return prop; return prop;
} }
export function getProperties(creatureId) { 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 props = Array.from(creature.properties.values());
const cloneProps = EJSON.clone(props); return EJSON.clone(props);
return cloneProps
} }
// console.time(`Cache miss on creature properties: ${creatureId}`) // console.time(`Cache miss on creature properties: ${creatureId}`)
const props = CreatureProperties.find({ const props = CreatureProperties.find({
'ancestors.id': creatureId, 'root.id': creatureId,
'removed': { $ne: true }, 'removed': { $ne: true },
}, { }, {
sort: { order: 1 }, sort: { left: 1 },
}).fetch(); }).fetch();
// console.timeEnd(`Cache miss on creature properties: ${creatureId}`); // console.timeEnd(`Cache miss on creature properties: ${creatureId}`);
return props; return props;
} }
export function getPropertiesOfType(creatureId, propType) { export function getPropertiesOfType(creatureId, propType) {
if (loadedCreatures.has(creatureId)) { const creature = loadedCreatures.get(creatureId);
const creature = loadedCreatures.get(creatureId); if (creature) {
const props = [] const props: CreatureProperty[] = []
for (const prop of creature.properties.values()) { for (const prop of creature.properties.values()) {
if (prop.type === propType) { if (prop.type === propType) {
props.push(prop); props.push(prop);
} }
} }
const cloneProps = EJSON.clone(props); return EJSON.clone(props);
return cloneProps
} }
// console.time(`Cache miss on creature properties: ${creatureId}`) // console.time(`Cache miss on creature properties: ${creatureId}`)
const props = CreatureProperties.find({ const props = CreatureProperties.find({
'ancestors.id': creatureId, 'root.id': creatureId,
'removed': { $ne: true }, 'removed': { $ne: true },
'type': propType, 'type': propType,
}, { }, {
sort: { order: 1 }, sort: { left: 1 },
}).fetch(); }).fetch();
// console.timeEnd(`Cache miss on creature properties: ${creatureId}`); // console.timeEnd(`Cache miss on creature properties: ${creatureId}`);
return props; return props;
} }
export function getCreature(creatureId) { export function getCreature(creatureId) {
if (loadedCreatures.has(creatureId)) { const loadedCreature = loadedCreatures.get(creatureId);
const loadedCreature = loadedCreatures.get(creatureId); const loadedCreatureDoc = loadedCreature?.creature;
const creature = loadedCreature.creature; if (loadedCreatureDoc) {
if (creature) { return EJSON.clone(loadedCreatureDoc);
const cloneCreature = EJSON.clone(creature);
return cloneCreature;
}
} }
// console.time(`Cache miss on Creature: ${creatureId}`); // console.time(`Cache miss on Creature: ${creatureId}`);
const creature = Creatures.findOne(creatureId); const creature = Creatures.findOne(creatureId);
@@ -110,13 +103,10 @@ export function getCreature(creatureId) {
} }
export function getVariables(creatureId) { export function getVariables(creatureId) {
if (loadedCreatures.has(creatureId)) { const loadedCreature = loadedCreatures.get(creatureId);
const loadedCreature = loadedCreatures.get(creatureId); const loadedVariables = loadedCreature?.variables;
const variables = loadedCreature.variables; if (loadedVariables) {
if (variables) { return EJSON.clone(loadedVariables);
const cloneVarables = EJSON.clone(variables);
return cloneVarables;
}
} }
// console.time(`Cache miss on variables: ${creatureId}`); // console.time(`Cache miss on variables: ${creatureId}`);
const variables = CreatureVariables.findOne({ _creatureId: creatureId }); const variables = CreatureVariables.findOne({ _creatureId: creatureId });
@@ -124,49 +114,44 @@ export function getVariables(creatureId) {
return variables; return variables;
} }
export function getProperyAncestors(creatureId, propertyId) { export function getPropertyAncestors(creatureId: string, propertyId: string) {
const prop = getSingleProperty(creatureId, propertyId); const prop = getSingleProperty(creatureId, propertyId);
if (!prop) return []; if (!prop) return [];
const ancestorIds = []; const loadedCreature = loadedCreatures.get(creatureId);
prop.ancestors.forEach(ref => { if (loadedCreature) {
if (ref.collection === 'creatureProperties') {
ancestorIds.push(ref.id);
}
});
if (loadedCreatures.has(creatureId)) {
// Get the ancestor properties from the cache // Get the ancestor properties from the cache
const creature = loadedCreatures.get(creatureId); const props: CreatureProperty[] = [];
const props = []; let currentProp: CreatureProperty | undefined = prop;
ancestorIds.forEach(id => { // Iterate through parent chain to get all linked ancestors
const prop = creature.properties.get(id); while (currentProp?.parentId) {
if (prop) { currentProp = getSingleProperty(creatureId, currentProp.parentId);
props.push(prop); if (currentProp) props.push(currentProp);
} }
}); return EJSON.clone(props);
const cloneProps = EJSON.clone(props);
return cloneProps
} else { } else {
// Fetch from database // Fetch from database
return CreatureProperties.find({ return CreatureProperties.find({
_id: { $in: ancestorIds }, ...getFilter.ancestors(prop),
removed: { $ne: true }, removed: { $ne: true },
}, { }, {
sort: { order: 1 }, sort: { left: 1 }
}).fetch(); }).fetch();
} }
} }
export function getPropertyDecendants(creatureId, propertyId) { export function getPropertyDescendants(creatureId, propertyId) {
const property = getSingleProperty(creatureId, propertyId); const property = getSingleProperty(creatureId, propertyId);
if (!property) return []; 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)) { if (loadedCreatures.has(creatureId)) {
const creature = loadedCreatures.get(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()) { 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); props.push(prop);
} }
} }
@@ -174,23 +159,30 @@ export function getPropertyDecendants(creatureId, propertyId) {
return cloneProps return cloneProps
} else { } else {
return CreatureProperties.find({ return CreatureProperties.find({
'ancestors.id': propertyId, ...getFilter.descendants(property),
removed: { $ne: true }, removed: { $ne: true },
}, { }, {
sort: { order: 1 }, sort: { left: 1 },
}).fetch(); }).fetch();
} }
} }
class LoadedCreature { class LoadedCreature {
subs: Set<Tracker.Computation>;
propertyObserver: Meteor.LiveQueryHandle;
creatureObserver: Meteor.LiveQueryHandle;
variablesObserver: Meteor.LiveQueryHandle;
properties: Map<string, CreatureProperty>;
creature: any;
variables: any;
constructor(sub, creatureId) { constructor(sub, creatureId) {
const self = this;
// This may be called from a subscription, but we don't want the observers // 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 // to be destroyed with it, so use a non-reactive context to observe
// the required documents // the required documents
const self = this;
Tracker.nonreactive(() => { Tracker.nonreactive(() => {
self.subs = new Set([sub]); self.subs = new Set([sub]);
const compute = debounce(Meteor.bindEnvironment(() => { const compute = debounce(Meteor.bindEnvironment(() => {
computeCreature(creatureId); computeCreature(creatureId);
}), COMPUTE_DEBOUNCE_TIME); }), COMPUTE_DEBOUNCE_TIME);
@@ -231,8 +223,8 @@ class LoadedCreature {
self.changeCreature(id, fields); self.changeCreature(id, fields);
if (fields.dirty) compute(); if (fields.dirty) compute();
}, },
removed(id) { removed() {
self.removeCreature(id); self.removeCreature();
}, },
}); });
@@ -249,8 +241,8 @@ class LoadedCreature {
changed(id, fields) { changed(id, fields) {
self.changeVariables(id, fields); self.changeVariables(id, fields);
}, },
removed(id) { removed() {
self.removeVariables(id); self.removeVariables();
}, },
}); });
}); });
@@ -293,7 +285,7 @@ class LoadedCreature {
} }
static changeDoc(doc, fields) { static changeDoc(doc, fields) {
if (!doc) return; if (!doc) return;
for (let key in fields) { for (const key in fields) {
if (key === undefined) { if (key === undefined) {
delete doc[key]; delete doc[key];
} else { } else {

View File

@@ -1,6 +1,19 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; 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({ const RefSchema = new SimpleSchema({
id: { id: {
type: String, 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 = { export const treeDocFields = {
_id: 1, _id: 1,
root: 1, root: 1,

View File

@@ -80,7 +80,7 @@ type FilteredDoc = {
export default async function filterToForest( export default async function filterToForest(
collection: Mongo.Collection<TreeDoc>, collection: Mongo.Collection<TreeDoc>,
rootId: string, rootId: string,
filter: Mongo.Query<TreeDoc>, filter: Mongo.Selector<TreeDoc>,
options: Mongo.Options<object> = {}, options: Mongo.Options<object> = {},
includeFilteredDocAncestors = false, includeFilteredDocAncestors = false,
includeFilteredDocDescendants = false includeFilteredDocDescendants = false
@@ -200,14 +200,14 @@ export const getFilter = {
* @param doc A document or array of documents that share a root * @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) * @returns A query filter that finds all the ancestors of the doc(s)
*/ */
ancestors(doc: TreeDoc): Mongo.Query<TreeDoc> { ancestors(doc: TreeDoc) {
return { return {
'root.id': doc.root.id, 'root.id': doc.root.id,
left: { $lt: doc.left }, left: { $lt: doc.left },
right: { $gt: doc.right }, right: { $gt: doc.right },
}; };
}, },
ancestorsOfAll(docs: Array<TreeDoc>): Mongo.Query<TreeDoc> { ancestorsOfAll(docs: Array<TreeDoc>) {
// The ancestors of no documents is a query that returns nothing // The ancestors of no documents is a query that returns nothing
if (docs.length === 0) { if (docs.length === 0) {
return { _id: '' }; return { _id: '' };
@@ -229,14 +229,14 @@ export const getFilter = {
}); });
return filter; return filter;
}, },
descendants(doc: TreeDoc): Mongo.Query<TreeDoc> { descendants(doc: TreeDoc) {
return { return {
'root.id': doc.root.id, 'root.id': doc.root.id,
left: { $gt: doc.left }, left: { $gt: doc.left },
right: { $lt: doc.right }, right: { $lt: doc.right },
}; };
}, },
descendantsOfAll(docs: Array<TreeDoc>): Mongo.Query<TreeDoc> { descendantsOfAll(docs: Array<TreeDoc>) {
// The descendants of no documents is a query that returns nothing // The descendants of no documents is a query that returns nothing
if (docs.length === 0) { if (docs.length === 0) {
return { _id: '' }; return { _id: '' };
@@ -248,7 +248,10 @@ export const getFilter = {
// Build a filter that selects all descendants // Build a filter that selects all descendants
const filter = { const filter = {
'root.id': docs[0].root.id, 'root.id': docs[0].root.id,
$or: <object[]>[], $or: <{
left: { $gt: number },
right: { $lt: number },
}[]>[],
}; };
docs.forEach(doc => { docs.forEach(doc => {
filter.$or.push({ filter.$or.push({
@@ -258,13 +261,13 @@ export const getFilter = {
}); });
return filter; return filter;
}, },
children(doc: TreeDoc): Mongo.Query<TreeDoc> { children(doc: TreeDoc) {
return { return {
'root.id': doc.root.id, 'root.id': doc.root.id,
parentId: doc._id, parentId: doc._id,
}; };
}, },
parent(doc: TreeDoc): Mongo.Query<TreeDoc> { parent(doc: TreeDoc) {
return { return {
_id: doc.parentId, _id: doc.parentId,
}; };

View File

@@ -123,7 +123,8 @@
"quotes": [ "quotes": [
"error", "error",
"single" "single"
] ],
"@typescript-eslint/no-this-alias": "off"
} }
} }
} }