datastores/dom/DOMDataStore.js

  1. /**
  2. * DOMDataStore - Store as DOM elements
  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. * A general purpose storage that serializes Concepts and Properties into DOM-elements and their attributes
  30. * and stores them in the document.
  31. *
  32. * For webstrates-based documents this allows synchronizing with other programs
  33. * across the web as modifications made to the storage structure also make notifications
  34. * back into the program.
  35. *
  36. * ### Options
  37. * * storageName - The name of the element to store that data below (Default: "varv-data")
  38. * * storageWebstrate - The name of the webstrate to store the data at (Default: the current webstrate)
  39. *
  40. * @memberOf Datastores
  41. */
  42. class DOMDataStore extends DirectDatastore {
  43. constructor(name, options = {}) {
  44. super(name, options);
  45. this.deleteCallbacks = [];
  46. }
  47. destroy() {
  48. if(this.iframeTransient != null) {
  49. this.iframeTransient.remove();
  50. }
  51. this.stopObserver();
  52. this.deleteCallbacks.forEach((deleteCallback)=>{
  53. deleteCallback.delete();
  54. });
  55. }
  56. async init() {
  57. const self = this;
  58. let storageName = "varv-data";
  59. if(this.options.storageName != null) {
  60. storageName = this.options.storageName;
  61. }
  62. let topElement = document;
  63. //If webstrate is specified, find topElement inside iframe
  64. if(this.options.storageWebstrate != null) {
  65. if(DOMDataStore.DEBUG) {
  66. console.log("Opening storage webstrate:", this.options.storageWebstrate);
  67. }
  68. let transient = document.createElement("transient");
  69. transient.style.display = "none";
  70. transient.setAttribute("name", "storageWebstrate-"+this.options.storageWebstrate);
  71. let iframe = document.createElement("iframe");
  72. transient.appendChild(iframe);
  73. document.body.appendChild(transient);
  74. iframe.src = "/"+this.options.storageWebstrate;
  75. await Observer.waitForTransclude(iframe);
  76. if(DOMDataStore.DEBUG) {
  77. console.log("Storage webstrate ready:", this.options.storageWebstrate);
  78. }
  79. topElement = iframe.contentDocument;
  80. this.iframeTransient = transient;
  81. }
  82. // Try to find an existing one
  83. this.backingElement = topElement.querySelector(storageName);
  84. this.queryCache = new QuerySelectorCache(this.backingElement);
  85. // None exists, create one
  86. if (!this.backingElement) {
  87. this.backingElement = topElement.createElement(storageName, {approved: true});
  88. topElement.body.appendChild(this.backingElement);
  89. // TODO: Check if webstrates race condition happened here and remedy it
  90. }
  91. // Add an observer to data backing element
  92. this.observer = new MutationObserver((mutations)=>{
  93. self.mutationCallback(mutations);
  94. });
  95. self.startObserver();
  96. //Setup disappeared listener?
  97. this.deleteCallbacks.push(VarvEngine.registerEventCallback("disappeared", async (context)=>{
  98. if(DOMDataStore.DEBUG) {
  99. console.log("Saw disappeared UUID (DOMDataStore):", context.target);
  100. }
  101. let conceptDom = this.backingElement.querySelector("concept[uuid='"+context.target+"']");
  102. if(conceptDom !== null) {
  103. self.executeObserverless(()=>{
  104. conceptDom.remove();
  105. });
  106. }
  107. }));
  108. this.deleteCallbacks.push(VarvEngine.registerEventCallback("appeared", async (context)=>{
  109. if(DOMDataStore.DEBUG) {
  110. console.log("Saw appeared UUID (DOMDataStore):", context.target);
  111. }
  112. let mark = VarvPerformance.start();
  113. if (self.isConceptMapped(context.concept)) {
  114. this.executeObserverless(() => {
  115. self.getConceptElementOrCreate(context.target, context.concept);
  116. });
  117. }
  118. VarvPerformance.stop("DOMDataStore.registerEventCallback.appeared", mark);
  119. }));
  120. }
  121. async addConcept(node, uuid) {
  122. let propertyChangedNodes = [];
  123. let self = this;
  124. // Check if already exists (this would be a bit weird but could happen in multi-backed concepts where the other backing already registered their part)
  125. let conceptByUUID = await VarvEngine.getConceptFromUUID(uuid);
  126. // Check if a duplicate already exists in the DOM marshalled data, since that is definitely a mistake
  127. let foundConcepts = self.backingElement.querySelectorAll('concept[uuid="'+uuid+'"]');
  128. if (foundConcepts.length > 1){
  129. console.warn("Warning: More than one DOM concept node found for "+conceptByUUID.name +" - only one element is allowed per uuid, this is bad, deleting extra");
  130. self.executeObserverless(()=>{
  131. //Clean everything but the first occurrence?
  132. Array.from(foundConcepts).slice(1).forEach((conceptElement)=>{
  133. conceptElement.remove();
  134. });
  135. });
  136. return;
  137. }
  138. // Check if the concept type is available and mapped
  139. let conceptType = node.getAttribute("type");
  140. if (conceptType===null) {
  141. console.warn("DOM concept node added without type, ignoring for now - not sure how to handle it");
  142. return;
  143. }
  144. let concept = VarvEngine.getConceptFromType(conceptType);
  145. if (!concept){
  146. console.warn("Warning: DOM concept node added for concept of unknown type '"+conceptType+"', ignoring");
  147. return;
  148. }
  149. if (conceptByUUID && concept.name !== conceptByUUID.name){
  150. console.warn("Warning: DOM concept node added which specified different type than the one registered in the current mapping, ignoring it");
  151. return;
  152. }
  153. if (!self.isConceptMapped(concept)){
  154. console.warn("Warning: DOM concept node added for concept for which there are no DOM-mapped properties in the current mapping, ignoring it");
  155. return;
  156. }
  157. // Everything checks out, let's add it then'
  158. if(DOMDataStore.DEBUG) {
  159. console.log("DOM saw " + uuid + " of type "+conceptType);
  160. }
  161. self.registerConceptFromUUID(uuid, concept);
  162. // Concepts can only exist as top-level but when added they can already carry properties as children nodes
  163. Array.from(node.children).forEach((childNode)=>{
  164. if (childNode.tagName==="PROPERTY"){
  165. // Make sure to import those property values if they exist
  166. propertyChangedNodes.push(childNode);
  167. }
  168. });
  169. // Signal that someone made a new concept instance appear for the first time
  170. if(conceptByUUID == null) {
  171. await concept.appeared(uuid);
  172. }
  173. return propertyChangedNodes;
  174. }
  175. async removeConcept(node, uuid) {
  176. let self = this;
  177. let concept = self.getConceptFromUUID(uuid);
  178. if (!concept ){
  179. console.warn("Notice: DOM concept node removed for concept with uuid "+uuid+" that we didn't know about, this inconsistency is odd");
  180. return;
  181. }
  182. let foundConcepts = self.backingElement.querySelectorAll('concept[uuid="'+uuid+'"]');
  183. if(foundConcepts.length > 0) {
  184. console.warn("Notice: Node with uuid "+uuid+" still exists in DOM, not calling disappear");
  185. return;
  186. }
  187. // Someone deleted this concept instance, let's tell everyone to delete it here too
  188. await concept.disappeared(uuid);
  189. }
  190. async mutationCallback(mutationList) {
  191. const self = this;
  192. if(DOMDataStore.DEBUG) {
  193. console.log("Got remote mutation", mutationList);
  194. }
  195. let addedConcepts = new Map();
  196. let removedConcepts = new Map();
  197. let propertyChangedNodes = [];
  198. for(let mutation of mutationList) {
  199. switch (mutation.type) {
  200. case 'childList':
  201. // Look for newly added concept instances first
  202. for(let node of mutation.addedNodes) {
  203. try {
  204. if (node.tagName==="CONCEPT"){
  205. let uuid = node.getAttribute("uuid");
  206. if (uuid===null) {
  207. console.warn("DOM concept node added without uuid, ignored for now - not sure what to do about it");
  208. continue;
  209. }
  210. addedConcepts.set(uuid, node);
  211. //Since we saw this node added, it is no longer in queue to be removed
  212. removedConcepts.delete(uuid);
  213. }
  214. // A property could also be added (set) directly by someone for the first time
  215. if (node.tagName==="PROPERTY"){
  216. propertyChangedNodes.push(node);
  217. }
  218. } catch (ex){
  219. console.error("Unhandled exception in DOM node adding handler", ex);
  220. }
  221. }
  222. // Removals
  223. for(let node of mutation.removedNodes) {
  224. try {
  225. // Concepts can be removed (deleted) and they appear only as top-level nodes here
  226. if (node.tagName==="CONCEPT"){
  227. let uuid = node.getAttribute("uuid");
  228. if (uuid===null) {
  229. console.warn("DOM concept node removed without uuid, ignored for now - not sure what to do about it");
  230. return;
  231. }
  232. removedConcepts.set(uuid, node);
  233. //Since we just removed this concept, it is no longer added
  234. addedConcepts.delete(uuid);
  235. }
  236. } catch (ex){
  237. console.error("Unhandled exception in DOM node remove handler", ex);
  238. }
  239. }
  240. // Array property had one or more new child nodes added to it
  241. if (mutation.target.tagName==="PROPERTY"){
  242. propertyChangedNodes.push(mutation.target);
  243. }
  244. break;
  245. case 'attributes':
  246. // - Simple property value change
  247. if (mutation.attributeName==="value" && mutation.target.tagName==="PROPERTY"){
  248. propertyChangedNodes.push(mutation.target);
  249. }
  250. // TODO: uuid and/or type added to concept that was previously missing it and was thus ignored
  251. break;
  252. }
  253. }
  254. for(let [uuid, node] of addedConcepts.entries()) {
  255. let possibleChangedPropertyNodes = await self.addConcept(node, uuid);
  256. if(possibleChangedPropertyNodes != null) {
  257. propertyChangedNodes.push(...possibleChangedPropertyNodes);
  258. }
  259. }
  260. for(let entry of removedConcepts.entries()) {
  261. await self.removeConcept(entry[1], entry[0]);
  262. //Filter property changes, don't run the ones where we removed the concept!
  263. propertyChangedNodes = propertyChangedNodes.filter((propertyNode)=>{
  264. let conceptElement = propertyNode.parentElement;
  265. let conceptUUID = conceptElement.getAttribute("uuid");
  266. return conceptUUID != entry[0];
  267. });
  268. }
  269. DOMDataStore.isSyncingPropertyNodes = true;
  270. for(let propertyNode of propertyChangedNodes) {
  271. try {
  272. await self.synchronizePropertyElementFromDOM(propertyNode);
  273. } catch(e) {
  274. console.error("Error synchronizing property element from dom: ", e);
  275. }
  276. }
  277. DOMDataStore.isSyncingPropertyNodes = false;
  278. }
  279. createBackingStore(concept, property) {
  280. if(DOMDataStore.DEBUG) {
  281. console.log("DOMDataStore Mapping "+concept.name+"."+property.name);
  282. }
  283. const self = this;
  284. if (!concept)
  285. throw new Error('Cannot map invalid concept to DOM: ' + concept);
  286. if (!property)
  287. throw new Error('Cannot map invalid property to DOM for concept: ' + concept + "." + property);
  288. if (this.isPropertyMapped(concept, property))
  289. throw new Error('Already mapped a DOM backing store for: ' + concept.name + "." + property.name);
  290. if (!this.isConceptMapped(concept)){
  291. // TODO: This is the first time we hear about this concept, add some create/delete or appear/disappear events as well
  292. }
  293. let getter = (uuid) => {
  294. if(DOMDataStore.DEBUG) {
  295. console.log("DOMDataStore getter: "+concept.name+"."+property.name);
  296. }
  297. let mark = VarvPerformance.start();
  298. let conceptElement = self.queryCache.querySelector("concept[uuid='" + uuid + "']");
  299. if (!conceptElement)
  300. throw new Error("No DOM data stored at all for "+concept.name+" with UUID "+uuid+" while getting "+concept.name+"."+property.name);
  301. let propertyElement = conceptElement.querySelector("property[name='" + property.name + "']");
  302. if (!propertyElement)
  303. throw new Error('No DOM data for property ' + concept.name + "." + property.name + " stored yet with UUID "+uuid);
  304. let result = self.getPropertyFromDOM(concept, propertyElement, property);
  305. VarvPerformance.stop("DOMDataStore.getter.nonCached", mark);
  306. return result;
  307. }
  308. let setter = (uuid, value) => {
  309. let mark = VarvPerformance.start();
  310. if(DOMDataStore.DEBUG) {
  311. console.log("DOMDataStore setter: "+concept.name+"."+property.name);
  312. }
  313. this.executeObserverless(()=>{
  314. let conceptElement = self.getConceptElement(uuid, concept);
  315. if (!conceptElement){
  316. throw new Error("Cannot set property "+property.name+" on "+concept.name+" "+uuid+" because it has not appeared or was deleted");
  317. }
  318. let propertyElement = conceptElement.querySelector("property[name='" + property.name + "']");
  319. if (!propertyElement) {
  320. propertyElement = document.createElement("property", { approved: true });
  321. propertyElement.setAttribute("name", property.name, { approved: true });
  322. conceptElement.appendChild(propertyElement);
  323. }
  324. let oldValue;
  325. try {
  326. oldValue = property.typeCast(getter(uuid));
  327. } catch(e) {
  328. //Ignore
  329. }
  330. if(property.isSame(value, oldValue)) {
  331. //This value was already set in DOM, dont set it again
  332. if(DOMDataStore.DEBUG) {
  333. console.log("Skipping because same value...");
  334. }
  335. return;
  336. }
  337. if (Array.isArray(value)) {
  338. let entryElement = document.createElement("temp");
  339. value.forEach((entryValue) => {
  340. let entry = document.createElement("entry", {approved: true});
  341. if (Array.isArray(entryValue))
  342. throw new Error('Nested arrays not supported yet'); // TODO
  343. entry.setAttribute("value", entryValue);
  344. entryElement.appendChild(entry);
  345. });
  346. propertyElement.innerHTML = entryElement.innerHTML;
  347. } else {
  348. propertyElement.setAttribute("value", value, { approved: true });
  349. }
  350. });
  351. VarvPerformance.stop("DOMDataStore.setter", mark);
  352. }
  353. property.addSetCallback(setter);
  354. property.addGetCallback(getter);
  355. // Check if concept already is mapped, if not, register it
  356. this.internalAddPropertyMapping(concept, property, {setter: setter, getter: getter});
  357. }
  358. getConceptElement(uuid){
  359. return this.queryCache.querySelector("concept[uuid='" + uuid + "']");
  360. }
  361. getConceptElementOrCreate(uuid, concept) {
  362. let mark = VarvPerformance.start();
  363. let conceptElement = this.getConceptElement(uuid);
  364. if (!conceptElement) {
  365. conceptElement = document.createElement("concept", {approved: true});
  366. conceptElement.setAttribute("type", concept.name, { approved: true });
  367. conceptElement.setAttribute("uuid", uuid, { approved: true });
  368. this.backingElement.appendChild(conceptElement);
  369. }
  370. VarvPerformance.stop("DOMDataStore.getConceptElementOrCreate", mark);
  371. return conceptElement;
  372. }
  373. removeBackingStore(concept, property) {
  374. if (!concept)
  375. throw new Error('Cannot unmap invalid concept from DOM: ' + concept);
  376. if (!property)
  377. throw new Error('Cannot unmap invalid property from DOM for concept: ' + concept + "." + property);
  378. if (!this.isConceptMapped(concept))
  379. throw new Error('Cannot unmap property from concept not managed by DOM: ' + concept.name);
  380. if (!this.isPropertyMapped(concept, property))
  381. throw new Error('Cannot unmap property on managed DOM concept because the property was not mapped: ' + concept.name + "." + property.name);
  382. let trackingData = this.internalPropertyTrackingData(concept, property);
  383. property.removeSetCallback(trackingData.setter);
  384. property.removeGetCallback(trackingData.getter);
  385. // TODO: If this was the last mapping for this concept, also remove delete/create or appear/disappear events, we no longer care
  386. this.internalRemovePropertyMapping(concept, property);
  387. }
  388. /**
  389. * Loads all concept instances currently registered as backed from serialized state
  390. *
  391. * @returns {undefined}
  392. */
  393. async loadBackingStore() {
  394. // We restore the state by faking that someone else just added all the contents of the
  395. // backing element to our DOM
  396. let fakeAddMutationList = [{
  397. type: "childList",
  398. target: this.backingElement,
  399. addedNodes: Array.from(this.backingElement.children),
  400. removedNodes: []
  401. }];
  402. await this.mutationCallback(fakeAddMutationList);
  403. }
  404. /**
  405. * Starts this DOM datastore's mutation observer
  406. * @ignore
  407. * @protected
  408. */
  409. startObserver() {
  410. this.observer.observe(this.backingElement, {
  411. attributes: true,
  412. childList: true,
  413. subtree: true,
  414. attributeOldValue: true,
  415. characterData: false,
  416. characterDataOldValue: false
  417. });
  418. }
  419. /**
  420. * Stops this DOM datastore's mutation observer, handling any mutations that is queued before stopping.
  421. * @ignore
  422. * @protected
  423. */
  424. stopObserver() {
  425. let mutations = this.observer.takeRecords();
  426. if (mutations.length > 0) {
  427. this.mutationCallback(mutations);
  428. }
  429. this.observer.disconnect();
  430. }
  431. /**
  432. * Run the given method without triggering the mutation observer
  433. * @ignore
  434. * @protected
  435. * @param {Function} method - Method to call. Important: must not be async, the observer will be restarted as soon as this promise returns.
  436. */
  437. executeObserverless(method) {
  438. this.stopObserver();
  439. //Run our method, potentially adding mutations
  440. method();
  441. this.startObserver();
  442. }
  443. /**
  444. * Reconstruct a value from DOM
  445. * @param {Concept} concept
  446. * @param {Element} propertyElement
  447. * @param {Property} propertyObject
  448. * @returns {any}
  449. */
  450. getPropertyFromDOM(concept, propertyElement, propertyObject){
  451. // Reconstruct the value
  452. if (propertyObject.type === "array") {
  453. // Unpack as array property
  454. let value = [];
  455. propertyElement.querySelectorAll(":scope > entry").forEach((childNode) => {
  456. let entryValue = childNode.getAttribute("value");
  457. if (entryValue === null)
  458. throw new Error("Illegal array entry stored in DOM, cannot unmarshal " + concept.name + "." + propertyObject.name);
  459. value.push(entryValue);
  460. });
  461. return value;
  462. } else {
  463. // Unpack as flat property
  464. let value = propertyElement.getAttribute("value");
  465. if (value === null)
  466. throw new Error('No actual value stored in DOM backed property for ' + concept.name + "." + propertyObject.name);
  467. return value;
  468. }
  469. }
  470. /**
  471. * Takes the element and looks up everything else from that and pushes its state
  472. * to the concept
  473. * @param {element} propertyElement The element with the property to push to concept
  474. */
  475. synchronizePropertyElementFromDOM(propertyElement){
  476. const self = this;
  477. // Lookup concept
  478. let conceptElement = propertyElement.parentElement;
  479. if(conceptElement.parentElement == null) {
  480. console.warn("Parent was not inside dom, skipping synchronize!");
  481. return;
  482. }
  483. if(DOMDataStore.DEBUG) {
  484. console.log("Synchronizing:", conceptElement, propertyElement);
  485. }
  486. let conceptInstance = this.getConceptInstanceFromConceptElement(conceptElement);
  487. // Lookup property
  488. let propertyName = propertyElement.getAttribute("name");
  489. if (propertyName === null) throw new Error("No property name on DOM property node "+conceptInstance.concept.name+" "+propertyElement);
  490. let propertyObject = conceptInstance.concept.getProperty(propertyName);
  491. return new Promise((resolve)=>{
  492. // Fire a set on the property which in turn calls our setter method while pausing our observer to sync with other datastores
  493. let value = self.getPropertyFromDOM(conceptInstance.concept, propertyElement, propertyObject);
  494. if(DOMDataStore.DEBUG) {
  495. console.log("DOM: Pushing remote change to " + conceptInstance.uuid + " " + conceptInstance.concept.name + "." + propertyObject.name + "=" + value);
  496. }
  497. propertyObject.setValue(conceptInstance.uuid, propertyObject.typeCast(value)).then(()=>{
  498. resolve();
  499. }).catch(()=>{
  500. //Unable to synchronize from dom, as dom did not validate
  501. resolve();
  502. });
  503. });
  504. }
  505. /**
  506. * Lookup concept and uuid from a concept element
  507. * @param {Element} conceptElement
  508. * @returns {any}
  509. */
  510. getConceptInstanceFromConceptElement(conceptElement){
  511. if (!conceptElement) throw new Error("Cannot get instance from undefined/null element");
  512. // TODO: Consider if manually added elements should get autogenerated uuid somehow, for now ignore it
  513. let uuid = conceptElement.getAttribute("uuid");
  514. if (uuid===null) throw new Error("Incomplete null concept instance in DOM");
  515. let type = conceptElement.getAttribute("type");
  516. if (type===null) throw new Error("Incomplete concept instance in DOM ignored, missing type: "+conceptElement.innerHTML);
  517. if (!this.isConceptTypeMapped(type)) throw new Error("DOM storage contains data for unmapped type, ignoring: "+type);
  518. // Lookup Concept by type through registry
  519. let concept = VarvEngine.getConceptFromType(type);
  520. if (!concept) throw new Error("DOM storage contains data for mapped type that is not registered in the system: "+type);
  521. return {concept: concept, uuid: uuid};
  522. }
  523. }
  524. DOMDataStore.DEBUG = false;
  525. window.DOMDataStore = DOMDataStore;
  526. //Register default dom datastore
  527. Datastore.registerDatastoreType("dom", DOMDataStore);
  528. class QuerySelectorCache {
  529. constructor(optionalParent) {
  530. this.parent = optionalParent!=null?optionalParent:document;
  531. this.cache = new Map();
  532. this.reverseLookup = new Map();
  533. this.setupObserver();
  534. }
  535. setupObserver() {
  536. const self = this;
  537. this.observer = new MutationObserver((mutations)=>{
  538. mutations.forEach((mutation)=>{
  539. mutation.removedNodes.forEach((node)=>{
  540. let selectors = self.reverseLookup.get(node);
  541. if(selectors != null) {
  542. selectors.forEach((selector)=>{
  543. self.cache.delete(selector);
  544. });
  545. self.reverseLookup.delete(node);
  546. if(DOMDataStore.DEBUG) {
  547. console.log("Updated cache for: ", node);
  548. }
  549. }
  550. });
  551. });
  552. });
  553. this.observer.observe(this.parent, {
  554. childList: true
  555. });
  556. }
  557. querySelector(selector) {
  558. let mark = VarvPerformance.start();
  559. let cacheEntry = this.cache.get(selector);
  560. if(cacheEntry != null) {
  561. VarvPerformance.stop("DOMDataStore.querySelector.cached", mark);
  562. return cacheEntry;
  563. }
  564. let result = this.parent.querySelector(selector);
  565. if(result != null) {
  566. this.cache.set(selector, result);
  567. let selectors = this.reverseLookup.get(result);
  568. if(selectors == null) {
  569. selectors = new Set();
  570. this.reverseLookup.set(result, selectors);
  571. }
  572. selectors.add(selector);
  573. }
  574. VarvPerformance.stop("DOMDataStore.querySelector.nonCached", mark);
  575. return result;
  576. }
  577. }