core/Concept.js

  1. /**
  2. * Concept - The central part of the Varv language
  3. *
  4. * This code is licensed under the MIT License (MIT).
  5. *
  6. * Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI,
  7. * Center for Advanced Visualization and Interaction, Aarhus University
  8. *
  9. * Permission is hereby granted, free of charge, to any person obtaining a copy
  10. * of this software and associated documentation files (the “Software”), to deal
  11. * in the Software without restriction, including without limitation the rights
  12. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. * copies of the Software, and to permit persons to whom the Software is
  14. * furnished to do so, subject to the following conditions:
  15. *
  16. * The above copyright notice and this permission notice shall be included in
  17. * all copies or substantial portions of the Software.
  18. *
  19. * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  25. * THE SOFTWARE.
  26. *
  27. */
  28. /**
  29. *
  30. */
  31. class Concept {
  32. constructor(name) {
  33. this.name = name;
  34. this.properties = new Map();
  35. this.actions = new Map();
  36. this.behaviours = new Map();
  37. this.triggers = new Map();
  38. this.mappings = new Map();
  39. this.otherConcepts = new Set();
  40. this.appearedCallbacks = [];
  41. this.disappearedCallbacks = [];
  42. }
  43. addTrigger(trigger, removeOld=false) {
  44. if(removeOld) {
  45. let oldTrigger = this.triggers.get(trigger.name);
  46. if (oldTrigger != null) {
  47. if (Concept.DEBUG) {
  48. console.log("Overwriting trigger:", oldTrigger, trigger);
  49. }
  50. this.removeTrigger(oldTrigger);
  51. }
  52. }
  53. this.triggers.set(trigger.name, trigger);
  54. }
  55. removeTrigger(trigger) {
  56. this.triggers.delete(trigger.name);
  57. trigger.disable(this);
  58. }
  59. getTrigger(name) {
  60. return this.triggers.get(name);
  61. }
  62. addBehaviour(behaviour, removeOld=false) {
  63. if(removeOld) {
  64. let oldBehaviour = this.behaviours.get(behaviour.name);
  65. if (oldBehaviour != null) {
  66. if (Concept.DEBUG) {
  67. console.log("Overwriting behaviour:", oldBehaviour, behaviour);
  68. }
  69. this.removeBehaviour(oldBehaviour);
  70. }
  71. }
  72. this.behaviours.set(behaviour.name, behaviour);
  73. if(behaviour.callableAction) {
  74. this.addAction(behaviour.actionChain);
  75. }
  76. }
  77. removeBehaviour(behaviour) {
  78. this.removeAction(behaviour.actionChain);
  79. this.behaviours.delete(behaviour.name);
  80. behaviour.destroy();
  81. }
  82. getBehaviour(name) {
  83. return this.behaviours.get(name);
  84. }
  85. addAction(action, removeOld=false) {
  86. if(removeOld) {
  87. let oldAction = this.actions.get(action.name);
  88. if (oldAction != null) {
  89. if (Concept.DEBUG) {
  90. console.log("Overwriting action:", oldAction, action);
  91. }
  92. this.removeAction(oldAction);
  93. }
  94. }
  95. this.actions.set(action.name, action);
  96. }
  97. removeAction(action) {
  98. this.actions.delete(action.name);
  99. }
  100. getAction(name) {
  101. return this.actions.get(name);
  102. }
  103. addProperty(property, removeOld=false) {
  104. if(removeOld) {
  105. let oldProperty = this.properties.get(property.name);
  106. if (oldProperty != null) {
  107. if (Concept.DEBUG) {
  108. console.log("Overwriting property:", oldProperty, property);
  109. }
  110. this.removeProperty(oldProperty);
  111. }
  112. }
  113. this.properties.set(property.name, property);
  114. }
  115. removeProperty(property) {
  116. this.unmapProperty(property);
  117. this.properties.delete(property.name);
  118. }
  119. getProperty(name) {
  120. const self = this;
  121. let markStart = VarvPerformance.start();
  122. let property = null;
  123. if(name === "uuid") {
  124. property = {
  125. name: "uuid",
  126. type: "string",
  127. addUpdatedCallback: () => {},
  128. removeUpdatedCallback: () => {},
  129. isConceptType: () => false,
  130. isConceptArrayType: () => false,
  131. getValue: (uuid) => {
  132. return uuid;
  133. }
  134. };
  135. } else if(name === "concept::uuid") {
  136. property = {
  137. name: "uuid",
  138. type: "string",
  139. addUpdatedCallback: ()=>{},
  140. removeUpdatedCallback: ()=>{},
  141. isConceptType: () => false,
  142. isConceptArrayType: () => false,
  143. getValue: (uuid) => {
  144. return uuid;
  145. }
  146. };
  147. } else if(name === "concept::name") {
  148. property = {
  149. name: "name",
  150. type: "string",
  151. addUpdatedCallback: ()=>{},
  152. removeUpdatedCallback: ()=>{},
  153. isConceptType: () => false,
  154. isConceptArrayType: () => false,
  155. getValue: (uuid) => {
  156. return self.name;
  157. }
  158. };
  159. } else {
  160. property = this.properties.get(name);
  161. }
  162. if(property != null) {
  163. VarvPerformance.stop("Concept.getProperty", markStart);
  164. return property;
  165. } else {
  166. VarvPerformance.stop("Concept.getProperty.error", markStart);
  167. throw new Error("No property ["+name+"] on ["+this.name+"]");
  168. }
  169. }
  170. async setPropertyValue(uuid, name, value, skipStateChangeTrigger=false) {
  171. await this.getProperty(name).setValue(uuid, value, skipStateChangeTrigger);
  172. }
  173. getPropertyValue(uuid, name) {
  174. return this.getProperty(name).getValue(uuid);
  175. }
  176. setupTriggers(debug) {
  177. const self = this;
  178. if(debug) {
  179. console.groupCollapsed("Setting up triggers on concept [" + this.name + "]");
  180. }
  181. for(let trigger of this.triggers.values()) {
  182. if(debug) {
  183. console.log("Enabling trigger:", trigger);
  184. }
  185. trigger.enable(this);
  186. }
  187. //We should listen for deleted concepts, and update our property when any we have are deleted...
  188. this.deletedTriggerDeleter = Trigger.registerTriggerEvent("deleted", async (contexts)=>{
  189. for(let context of contexts) {
  190. if(context.target != null) {
  191. let concept = await VarvEngine.getConceptFromUUID(context.target);
  192. if(concept != null) {
  193. //A concept was deleted, check if we have any properties with the given concept
  194. for(let property of self.properties.values()) {
  195. if(property.holdsConceptOfType(concept)) {
  196. await property.removeAllReferences(self.name, context.target);
  197. }
  198. }
  199. }
  200. }
  201. }
  202. });
  203. if(debug) {
  204. console.groupEnd();
  205. }
  206. }
  207. destroyTriggers() {
  208. for(let trigger of this.triggers.values()) {
  209. if(Concept.DEBUG) {
  210. console.log("Disabling trigger:", trigger);
  211. }
  212. trigger.disable(this);
  213. }
  214. this.deletedTriggerDeleter.delete();
  215. }
  216. async create(wantedUUID=null, properties=null){
  217. let mark = VarvPerformance.start();
  218. if(wantedUUID == null) {
  219. let uuidMark = VarvPerformance.start();
  220. wantedUUID = UUIDGenerator.generateUUID("concept");
  221. VarvPerformance.stop("Concept.create.generateUUID", uuidMark);
  222. } else {
  223. // TODO is this correct?
  224. let oldConcept = await VarvEngine.getConceptFromUUID(wantedUUID);
  225. //If already present, just return as if it has been created?
  226. if(oldConcept != null) {
  227. if(oldConcept !== this) {
  228. throw new Error("Trying to create ["+wantedUUID+"] as ["+this.name+"] but it is already registered as a ["+oldConcept.name+"]");
  229. }
  230. throw new Error("Trying to create ["+wantedUUID+"] that already existed, as the same concept type..");
  231. }
  232. }
  233. await VarvEngine.registerConceptFromUUID(wantedUUID, this);
  234. await this.appeared(wantedUUID, true);
  235. if (properties != null) {
  236. for (let key of Object.keys(properties)) {
  237. let value = properties[key];
  238. await this.setPropertyValue(wantedUUID, key, value, false);
  239. }
  240. } else {
  241. //Now trigger stateChanged for all properties
  242. for(let [key, prop] of this.properties) {
  243. let value = prop.getDefaultValue();
  244. await prop.stateChanged(wantedUUID, value);
  245. }
  246. }
  247. await this.created(wantedUUID);
  248. for (let callback of this.appearedCallbacks) {
  249. await callback(wantedUUID, this);
  250. }
  251. VarvPerformance.stop("Concept.create", mark);
  252. return wantedUUID;
  253. }
  254. /**
  255. * Clones the given UUID into a new one
  256. * @param {type} sourceUUID
  257. * @returns {@var;wantedUUID}
  258. */
  259. async clone(sourceUUID, deep=false, alreadyClonedReferences={}){
  260. let clonedProperties = {};
  261. for (const [propertyName, property] of this.properties){
  262. clonedProperties[propertyName] = await property.getValue(sourceUUID);
  263. if (deep){
  264. async function cloneUUID(propertyConcept, uuid){
  265. let propertyActualConcept = await VarvEngine.getConceptFromUUID(uuid);
  266. if (!propertyActualConcept) {
  267. console.warn("Invalid reference to UUID '"+uuid+"' while deep-cloning property "+propertyName+" on "+propertyConcept.name+", the property was left as is (invalid)");
  268. return uuid;
  269. }
  270. if (uuid===sourceUUID) throw new Error("Currently no support for deep cloning of concept instances with properties that contain direct self-references");
  271. // TODO: cycles too
  272. // Check if we already cloned it, if not do so
  273. if (alreadyClonedReferences[uuid]){
  274. return alreadyClonedReferences[uuid];
  275. } else {
  276. let theClone = await propertyConcept.clone(uuid, true);
  277. alreadyClonedReferences[uuid] = theClone;
  278. return theClone;
  279. }
  280. }
  281. // Referenced Concepts and Concept reference lists should also be cloned
  282. if (property.isConceptType()){
  283. let propertyConcept = await VarvEngine.getConceptFromType(property.getType());
  284. clonedProperties[propertyName] = await cloneUUID(propertyConcept, clonedProperties[propertyName]);
  285. }
  286. if (property.isConceptArrayType()){
  287. let propertyConcept = await VarvEngine.getConceptFromType(property.getArrayType());
  288. let newPropertyValue = [];
  289. for(let i = 0; i < clonedProperties[propertyName].length; i++) {
  290. newPropertyValue.push(await cloneUUID(propertyConcept, clonedProperties[propertyName][i]));
  291. }
  292. clonedProperties[propertyName] = newPropertyValue;
  293. }
  294. };
  295. }
  296. return this.create(null, clonedProperties);
  297. }
  298. finishSetup(debug) {
  299. if(debug) {
  300. console.groupCollapsed("Finishing concept:", this.name);
  301. }
  302. //this.finishProperties(debug);
  303. this.finishBehaviours(debug);
  304. this.setupTriggers(debug);
  305. if(debug) {
  306. console.groupEnd();
  307. }
  308. }
  309. doAfterSpecLoadSetup(debug) {
  310. let mark = VarvPerformance.start();
  311. this.finishProperties(debug);
  312. VarvPerformance.stop("Concept.doAfterSpecLoadSetup", mark, this.name);
  313. }
  314. finishProperties(debug) {
  315. let self = this;
  316. if(debug) {
  317. console.group("Properties:");
  318. }
  319. this.properties.forEach((property)=>{
  320. if(debug) {
  321. console.log(property)
  322. }
  323. property.finishSetup(self);
  324. });
  325. if(debug) {
  326. console.groupEnd();
  327. }
  328. }
  329. finishBehaviours(debug) {
  330. if(debug) {
  331. console.group("Behaviours:");
  332. }
  333. this.behaviours.forEach((behaviour, key)=>{
  334. if(debug) {
  335. console.log(behaviour);
  336. }
  337. behaviour.setupEvents();
  338. });
  339. if(debug) {
  340. console.groupEnd();
  341. }
  342. }
  343. omit(omitConfig) {
  344. let self = this;
  345. if(omitConfig.schema != null) {
  346. if(!Array.isArray(omitConfig.schema)) {
  347. omitConfig.schema = [omitConfig.schema];
  348. }
  349. omitConfig.schema.forEach((propertyName)=>{
  350. let property = self.getProperty(propertyName);
  351. if(property != null) {
  352. self.removeProperty(property);
  353. }
  354. });
  355. }
  356. if(omitConfig.actions != null) {
  357. if(!Array.isArray(omitConfig.actions)) {
  358. omitConfig.actions = [omitConfig.actions];
  359. }
  360. omitConfig.actions.forEach((actionName)=>{
  361. let behaviour = self.getBehaviour(actionName);
  362. if(behaviour != null) {
  363. self.removeBehaviour(behaviour);
  364. }
  365. let action = self.getAction(actionName);
  366. if(action != null) {
  367. self.removeAction(action);
  368. }
  369. });
  370. }
  371. }
  372. /**
  373. * Import the otherConcept into this one. This concept will be the combination of both concepts but retains its name.
  374. * In case of clashes the otherConcept will override existing entries in this concept.
  375. *
  376. * @param {Concept} otherConcept Other concept to import into this one
  377. */
  378. join(otherConcept){
  379. if(this === otherConcept) {
  380. console.warn("Attempting to join concept to itself!");
  381. return;
  382. }
  383. if(Concept.DEBUG) {
  384. console.group("Joining:", otherConcept.name, " into ", this.name);
  385. }
  386. const self = this;
  387. otherConcept.properties.forEach((property)=>{
  388. self.addProperty(property.cloneFresh(self), true);
  389. });
  390. for (let [propertyName, mappings] of otherConcept.mappings){
  391. this.mapProperty(this.getProperty(propertyName), mappings);
  392. }
  393. otherConcept.behaviours.forEach((behaviour)=>{
  394. self.addBehaviour(behaviour.cloneFresh(self), true);
  395. });
  396. this.otherConcepts.add(otherConcept.name);
  397. otherConcept.otherConcepts.forEach((otherConceptType)=>{
  398. self.otherConcepts.add(otherConceptType);
  399. })
  400. if(Concept.DEBUG) {
  401. console.groupEnd();
  402. }
  403. }
  404. unmapProperty(property) {
  405. this.mappings.get(property.name).forEach((datastoreName)=>{
  406. let datastore = Datastore.getDatastoreFromName(datastoreName);
  407. if(datastore != null) {
  408. datastore.removeBackingStore(this, property);
  409. } else {
  410. // TODO: Throw an error here?, We might just be unmapping from a join before datastores are even a thing...
  411. }
  412. });
  413. this.mappings.delete(property.name);
  414. }
  415. mapProperty(property, propertyMappings){
  416. this.mappings.set(property.name, propertyMappings);
  417. }
  418. enableMappings(debug = false) {
  419. const self = this;
  420. this.mappings.forEach((propertyMappings, propertyName)=>{
  421. let property = self.getProperty(propertyName);
  422. if(debug) {
  423. console.log(propertyName, propertyMappings);
  424. }
  425. let sharedDataStores = [];
  426. propertyMappings.forEach((datastoreName)=>{
  427. let datastore = Datastore.getDatastoreFromName(datastoreName);
  428. if(datastore != null) {
  429. if(datastore.isShared()) {
  430. sharedDataStores.push(datastoreName);
  431. }
  432. datastore.createBackingStore(this, property);
  433. } else {
  434. if (!Datastore.optionalDatastores.includes(datastoreName)) console.warn("["+self.name+"] is attempting to map ["+propertyName+"] to a non existing datastore ["+datastoreName+"]");
  435. }
  436. });
  437. if(sharedDataStores.length > 1) {
  438. console.log("%c Property "+self.name+"."+propertyName+" is mapped to multiple shared datastores ["+sharedDataStores+"], this can create race conditions in multi user use cases.", "background: yellow; color: red;");
  439. }
  440. });
  441. }
  442. async delete(uuid){
  443. // Trigger deleted() trigger with target set to uuid
  444. await this.deleted(uuid);
  445. await this.disappeared(uuid);
  446. }
  447. addAppearedCallback(callback) {
  448. this.appearedCallbacks.push(callback);
  449. }
  450. addDisappearedCallback(callback) {
  451. this.disappearedCallbacks.push(callback);
  452. }
  453. async deleted(uuid) {
  454. let mark = VarvPerformance.start();
  455. await Trigger.trigger("deleted", {
  456. target: uuid
  457. });
  458. VarvPerformance.stop("Concept.Event.deleted", mark);
  459. }
  460. async created(uuid) {
  461. let mark = VarvPerformance.start();
  462. await Trigger.trigger("created", {
  463. target: uuid
  464. });
  465. VarvPerformance.stop("Concept.Event.created", mark);
  466. }
  467. async appeared(uuid, skipCallback=false) {
  468. let mark = VarvPerformance.start();
  469. // This instance just appeared in at least one datastore
  470. await VarvEngine.sendEvent("appeared", {
  471. target: uuid,
  472. concept: this
  473. });
  474. if(!skipCallback) {
  475. for (let callback of this.appearedCallbacks) {
  476. await callback(uuid, this);
  477. }
  478. }
  479. VarvPerformance.stop("Concept.Event.appeared", mark);
  480. }
  481. async disappeared(uuid) {
  482. let mark = VarvPerformance.start();
  483. // This instance just disappeared in at least one datastore
  484. await VarvEngine.sendEvent("disappeared", {
  485. target: uuid,
  486. concept: this
  487. });
  488. //Unregister the UUID
  489. VarvEngine.deregisterConceptFromUUID(uuid, this);
  490. for(let callback of this.disappearedCallbacks) {
  491. await callback(uuid, this);
  492. }
  493. VarvPerformance.stop("Concept.Event.disappeared", mark);
  494. }
  495. async destroy() {
  496. const self = this;
  497. if(Concept.DEBUG) {
  498. console.log("Destroying:", this);
  499. }
  500. //Destroy triggers
  501. this.destroyTriggers();
  502. //Destroy properties
  503. for(let property of this.properties.values()) {
  504. if(Concept.DEBUG) {
  505. console.log("Derigestering property:", property);
  506. }
  507. //Brute force trying to remove from any datastore known to mankind...
  508. Datastore.datastores.forEach((datastore)=>{
  509. try {
  510. datastore.removeBackingStore(self, property);
  511. } catch(e) {
  512. //Ignore
  513. }
  514. })
  515. }
  516. //Destroy behaviours
  517. for(let behaviour of this.behaviours.values()) {
  518. behaviour.destroy();
  519. }
  520. //Destroy actions
  521. this.actions = null;
  522. this.triggers = null;
  523. this.properties = null;
  524. this.behaviours = null;
  525. if(Concept.DEBUG) {
  526. console.log("Deregistering from VarvEngine...");
  527. }
  528. VarvEngine.deregisterConceptFromType(this.name);
  529. }
  530. isA(conceptType) {
  531. if(conceptType instanceof Concept) {
  532. conceptType = conceptType.name;
  533. }
  534. return this.name === conceptType || this.otherConcepts.has(conceptType);
  535. }
  536. }
  537. Concept.DEBUG = false;
  538. window.Concept = Concept;