/* The MIT License (MIT) Copyright (c) 2014-2020 Nikolai Suslov and the Krestianstvo.org project contributors. (https://github.com/NikolaySuslov/livecodingspace/blob/master/LICENSE.md) Virtual World Framework Apache 2.0 license (https://github.com/NikolaySuslov/livecodingspace/blob/master/licenses/LICENSE_VWF.md) */ /// @module vwf /// vwf.js is the main Virtual World Framework manager. It is constructed as a JavaScript ES6 module /// to isolate it from the rest of the page's JavaScript environment. The vwf module self-creates its own instance when loaded and /// attaches to the global window object as window.vwf. Nothing else should affect the global environment. import { Helpers } from '/core/helpers.js'; import { VirtualTime } from '/core/virtualTime.js'; import { ReflectorClient } from '/core/reflectorClient.js'; import { Luminary } from '/core/luminary.js'; import { Logger } from '/core/vwf/utility/logger.js'; import { KUtility } from '/core/vwf/utility/kutility.js'; import { Utility } from '/core/vwf/utility/utility.js'; import { XPath } from '/core/vwf/utility/xpath.js'; import { Fabric } from '/core/vwf/fabric.js'; import { ViewKernel } from '/core/vwf/view.js'; import { ModelKernel } from '/core/vwf/model.js'; import { Log } from '/core/vwf/model/stage/log.js'; class VWF { constructor(driverConf, proxy, doc, connectionConf) { console.log("VWF constructor"); let self = this; this.helpers = new Helpers; this.driverConfiguration = driverConf || {}; this.proxy = proxy; this.doc = doc; this.isLuminary = connectionConf.luminary; this.isLuminaryGlobalHB = connectionConf.luminaryHB; this.luminaryGlobalHBPath = connectionConf.luminaryGlobalHBPath; this.luminary = new Luminary; this.reflectorClient = new ReflectorClient; this.virtualTime = new VirtualTime; window._Time = this.virtualTime; //vwf.virtualTime = app.virtualTime; this.applicationLoad = undefined; // == Public variables ===================================================================== /// The runtime environment (production, development, testing) and other configuration /// settings appear here. /// /// @name module:vwf.configuration /// /// @private /// Default configuration for all environments. this.configuration = { "log-level": "info", // logger threshold "random-seed": +new Date, // pseudorandom number generator seed "randomize-ids": true, // randomize IDs to discourage assumptions about ID allocation "humanize-ids": true, // append recognizable strings to node IDs "preserve-script-closures": false, // retain method/event closures by not serializing functions (breaks replication, persistence) "load-timeout": 30 // resource load timeout in seconds } //undefined; // require( "vwf/configuration" ).active; // "active" updates in place and changes don't invalidate the reference // TODO: assign here after converting vwf.js to a RequireJS module and listing "vwf/configuration" as a dependency /// Kernel utility functions and objects. /// /// @name module:vwf.utility /// /// @private this.kutility = new KUtility; //this.kutility = undefined; // require( "vwf/kernel/utility" ); // TODO: assign here after converting vwf.js to a RequireJS module and listing "vwf/kernel/utility" as a dependency this.xpath = new XPath; this.utility = new Utility; this.viewModule = new Fabric({ id:"vwf/view" }, 'View'); this.modelModule = new Fabric({ id:"vwf/model" }, 'Model'); /// The kernel logger. /// /// @name module:vwf.logger /// /// @private this.logger = (new Logger).for("vwf", this, this.configuration["log-level"] ); //undefined; // require( "logger" ).for( undefined, this ); // TODO: for( "vwf", ... ), and update existing calls // TODO: assign here after converting vwf.js to a RequireJS module and listing "vwf/logger" as a dependency /// Each model and view module loaded by the main page registers itself here. /// /// @name module:vwf.modules /// /// @private this.modules = []; /// vwf.initialize() creates an instance of each model and view module configured on the main /// page and attaches them here. /// /// @name module:vwf.models /// /// @private this.models = []; /// vwf.initialize() creates an instance of each model and view module configured on the main /// page and attaches them here. /// /// @name module:vwf.views /// /// @private this.views = []; /// this.models is a list of references to the head of each driver pipeline. Define an /// `actual` property that evaluates to a list of references to the pipeline tails. This is /// a list of the actual drivers after any intermediate stages and is useful for debugging. /// /// @name module:vwf.models.actual Object.defineProperty(this.models, "actual", { get: function () { // Map the array to the result. var actual = this.map(function (model) { return last(model); }); // Map the non-integer properties too. for (var propertyName in this) { if (isNaN(Number(propertyName))) { actual[propertyName] = last(this[propertyName]); } } // Follow a pipeline to the last stage. function last(model) { while (model.model) model = model.model; return model; } return actual; } }); /// this.views is a list of references to the head of each driver pipeline. Define an /// `actual` property that evaluates to a list of references to the pipeline tails. This is /// a list of the actual drivers after any intermediate stages and is useful for debugging. /// /// @name module:vwf.views.actual Object.defineProperty(this.views, "actual", { get: function () { // Map the array to the result. var actual = this.map(function (model) { return last(model); }); // Map the non-integer properties too. for (var propertyName in this) { if (isNaN(Number(propertyName))) { actual[propertyName] = last(this[propertyName]); } } // Follow a pipeline to the last stage. function last(model) { while (model.model) model = model.model; return model; } return actual; } }); /// The identifer assigned to the client by the server. /// /// @name module:vwf.moniker_ /// /// @private this.moniker_ = undefined; /// Nodes that are receiving ticks. /// /// @name module:vwf.tickable /// /// @private this.tickable = { // models: [], // views: [], nodeIDs: [], }; // == Private variables ==================================================================== /// @name module:vwf.private /// /// @private this.private = {}; // for debugging /// Components describe the objects that make up the simulation. They may also serve as /// prototype objects for further derived components. External components are identified by /// URIs. Once loaded, we save a mapping here from its URI to the node ID of its prototype so /// that we can find it if it is reused. Components specified internally as object literals /// are anonymous and are not indexed here. /// /// @name module:vwf~components this.components = this.private.components = {}; // maps component node ID => component specification // Each node is assigned an ID as it is created. This is the most recent ID assigned. // Communication between the manager and the models and views uses these IDs to refer to the // nodes. The manager doesn't maintain any particular state for the nodes and knows them // only as their IDs. The models work in federation to provide the meaning to each node. // var lastID = 0; /// Callback functions defined in this scope use this local "vwf" to locate the manager. /// /// @name module:vwf~vwf window.vwf = this; ////////// this.setProperty.entrants = {}; // maps ( nodeID + '-' + propertyName ) => { index: i, value: v } this.getProperty.entrants = {}; // maps ( nodeID + '-' + propertyName ) => { index: i, value: v } // == Private variables ==================================================================== /// Prototype for name-based, unordered collections in the node registry, including /// `node.properties`, `node.methods`, and `node.events`. this.keyedCollectionPrototype = { /// Record that a property, method or event has been created. /// /// @param {String} name /// The member name. /// @param {Boolean} changes /// For patchable nodes, record changes so that `kernel.getNode` may create a patch /// when retrieving the node. /// @param [value] /// An optional value to assign to the record. If `value` is omitted, the record will /// exist in the collection but have the value `undefined`. /// /// @returns {Boolean} /// `true` if the member was successfully added. `false` if a member by that name /// already exists. create: function (name, changes, value) { if (!this.hasOwn(name)) { this.makeOwn("existing"); // Add the member. `Object.defineProperty` is used instead of // `this.existing[name] = ...` since the prototype may be a behavior proxy, and // the accessor properties would prevent normal assignment. Object.defineProperty(this.existing, name, self.configurable(value ? value : undefined)); if (changes) { this.makeOwn("changes"); if (this.changes[name] !== "removed") { this.changes[name] = "added"; } else { this.changes[name] = "changed"; // previously removed, then added } if (this.container && this.containerMember) { this.container.change(this.containerMember); } } return true; } return false; }, /// Record that a member has been deleted. /// /// @param {String} name /// The member name. /// /// @returns {Boolean} /// `true` if the member was successfully removed. `false` if a member by that name /// does not exist. delete: function (name, changes) { if (this.hasOwn(name)) { delete this.existing[name]; if (changes) { this.makeOwn("changes"); if (this.changes[name] !== "added") { this.changes[name] = "removed"; } else { delete this.changes[name]; // previously added, then removed } if (this.container && this.containerMember) { this.container.change(this.containerMember); } } return true; } return false; }, /// Record that a member has changed. /// /// @param {String} name /// The member name. /// /// @returns {Boolean} /// `true` if the change was successfully recorded. `false` if a member by that name /// does not exist. change: function (name, value) { if (this.hasOwn(name)) { this.makeOwn("changes"); if (this.changes[name] !== "added") { this.changes[name] = value ? value : this.changes[name] || "changed"; } if (this.container && this.containerMember) { this.container.change(this.containerMember); } return true; } return false; }, /// Determine if a node has a member with the given name, either directly on the node or /// inherited from a prototype. /// /// @param {String} name /// The member name. /// /// @returns {Boolean} has: function (name) { return name in this.existing; }, /// Determine if a node has a member with the given name. The node's prototypes are not /// considered. /// /// @param {String} name /// The member name. /// /// @returns {Boolean} // Since prototypes of the collection objects mirror the node's prototype chain, // collection objects for the proto-prototype `node.vwf` intentionally don't inherit // from `Object.prototype`. Otherwise the Object members `hasOwnProperty`, // `isPrototypeOf`, etc. would be mistaken as members of a VWF node. // Instead of using the simpler `this.existing.hasOwnProperty( name )`, we must reach // `hasOwnProperty through `Object.prototype`. hasOwn: function (name) { return Object.prototype.hasOwnProperty.call(this.existing, name); }, /// Hoist a field from a prototype to the collection in preparation for making local /// changes. /// /// If the field in the prototype is an object, create a new object with that field as /// its prototype. If the field in the prototype is an array, clone the field since /// arrays can't readily serve as prototypes for other arrays. In other cases, copy the /// field from the prototype. Only objects, arrays and primitive values are supported. /// /// @param {String} fieldName /// The name of a field to hoist from the collection's prototype. makeOwn: function (fieldName) { if (!this.hasOwnProperty(fieldName)) { if (this[fieldName] instanceof Array) { this[fieldName] = this[fieldName].slice(); // clone arrays } else if (typeof this[fieldName] === "object" && this[fieldName] !== null) { this[fieldName] = Object.create(this[fieldName]); // inherit from objects } else { this[fieldName] = this[fieldName]; // copy primitives } } }, /// The property, method, or event members defined in this collection. /// /// `existing` is an unordered collection of elements and optional values. The keys are /// the primary data. Existence on the object is significant regardless of the value. /// Some collections store data in the element when the kernel owns additional details /// about the member. Values will be `undefined` in other collections. /// /// For each collection, `existing` is the authoritative list of the node's members. Use /// `collection.hasOwn( memberName )` to determine if the node defines a property, /// method or event by that name. /// /// The prototype of each `existing` object will be the `existing` object of the node's /// prototype (or a proxy to the top behavior for nodes with behaviors). Use /// `collection.has( memberName )` to determine if a property, method or event is /// defined on the node or its prototypes. existing: Object.create(null // name: undefined, // name: { ... } -- details // ... ), /// The change list for members in this collection. /// /// For patchable nodes, `changes` records the members that have been added, removed, or /// changed since the node was first initialized. `changes` is not created in the /// collection until the first change occurs. Only the change is recorded here. The /// state behind the change is retrieved from the drivers when needed. changes: { // name: "added" // name: "removed" // name: "changed" // name: { ... } -- changed, with details // ... }, /// The parent collection if this collection is a member of another. Changes applied to /// members of this collection will call `container.change( containerMember )` to also /// set the change flag for the containing member. /// /// For example, members of the `node.events` collection contain listener collections at /// `node.events.existing[name].listeners`. Each listener collection knows its event /// name and points back to `node.events`. Changing a listener will call /// `node.events.change( name )` to mark the event as changed. container: undefined, /// This collection's name in the parent if this collection is a member of another /// collection. Changes to members of this collection will mark that member changed in /// the containing collection. containerMember: undefined, } /// Prototype for index-based, ordered collections in the node registry, including /// `event.listeners`. this.indexedCollectionPrototype = { /// Record that a member has been created. /// /// @param {string|number|boolean|null} id /// The member's unique id. /// @param {Boolean} changes /// For patchable nodes, record changes so that `kernel.getNode` may create a patch /// when retrieving the node. /// /// @returns {Boolean} /// `true` if the member was successfully added. `false` if a member with that id /// already exists. create: function (id, changes) { if (!this.hasOwn(id)) { this.makeOwn("existing"); this.existing.push(id); if (changes) { this.makeOwn("changes"); var removedIndex = this.changes.removed ? this.changes.removed.indexOf(id) : -1; if (removedIndex < 0) { this.changes.added = this.changes.added || []; this.changes.added.push(id); } else { this.changes.removed.splice(removedIndex, 1); this.changes.changed = this.changes.changed || []; this.changes.changed.push(id); } if (this.container && this.containerMember) { this.container.change(this.containerMember); } } return true; } return false; }, /// Record that a member has been deleted. /// /// @param {string|number|boolean|null} id /// The member's unique id. /// /// @returns {Boolean} /// `true` if the member was successfully removed. `false` if a member with that id /// does not exist. delete: function (id, changes) { if (this.hasOwn(id)) { this.existing.splice(this.existing.indexOf(id), 1); if (changes) { this.makeOwn("changes"); var addedIndex = this.changes.added ? this.changes.added.indexOf(id) : -1; if (addedIndex < 0) { this.changes.removed = this.changes.removed || []; this.changes.removed.push(id); } else { this.changes.added.splice(addedIndex, 1); } if (this.container && this.containerMember) { this.container.change(this.containerMember); } } return true; } return false; }, /// Record that a member has changed. /// /// @param {string|number|boolean|null} id /// The member's unique id. /// /// @returns {Boolean} /// `true` if the change was successfully recorded. `false` if a member with that id /// does not exist. change: function (id) { if (this.hasOwn(id)) { this.makeOwn("changes"); var addedIndex = this.changes.added ? this.changes.added.indexOf(id) : -1; var changedIndex = this.changes.changed ? this.changes.changed.indexOf(id) : -1; if (addedIndex < 0 && changedIndex < 0) { this.changes.changed = this.changes.changed || []; this.changes.changed.push(id); } if (this.container && this.containerMember) { this.container.change(this.containerMember); } return true; } return false; }, /// Determine if a node has a member with the given id. /// /// `has` is the same as `hasOwn` for `indexedCollectionPrototype` since index-based /// collections don't automatically inherit from their prototypes. /// /// @param {string|number|boolean|null} id /// The member's unique id. /// /// @returns {Boolean} has: function (id) { return this.hasOwn(id); }, /// Determine if a node has a member with the given id. The node's prototypes are not /// considered. /// /// @param {string|number|boolean|null} id /// The member's unique id. /// /// @returns {Boolean} hasOwn: function (id) { return this.existing ? this.existing.indexOf(id) >= 0 : false; }, /// Hoist a field from a prototype to the collection in preparation for making local /// changes. /// /// If the field in the prototype is an object, create a new object with that field as /// its prototype. If the field in the prototype is an array, clone the field since /// arrays can't readily serve as prototypes for other arrays. In other cases, copy the /// field from the prototype. Only objects, arrays and primitive values are supported. /// /// @param {String} fieldName /// The name of a field to hoist from the collection's prototype. makeOwn: function (fieldName) { if (!this.hasOwnProperty(fieldName)) { if (this[fieldName] instanceof Array) { this[fieldName] = this[fieldName].slice(); // clone arrays } else if (typeof this[fieldName] === "object" && this[fieldName] !== null) { this[fieldName] = Object.create(this[fieldName]); // inherit from objects } else { this[fieldName] = this[fieldName]; // copy primitives } } }, /// IDs of the members defined in this collection. /// /// `existing` is an ordered list of IDs, which much be unique within the collection. /// The IDs retain the order in which they were originally added. /// /// For each collection, `existing` is the authoritative list of the node's members. Use /// `collection.hasOwn( memberID )` to determine if the collection contains a member /// with that id. Unlike `keyedCollectionPrototype` collections, /// `indexedCollectionPrototype` collections aren't connected in parallel with their /// containers' prototype chains. existing: [ // id, // id, // ... ], /// The change list for members in this collection. /// /// For patchable nodes, `changes` records the members that have been added, removed, or /// changed since the node was first initialized. Changes are recorded in separate /// `added`, `removed`, and `changed` arrays, respectively. The `added` array retains /// the order in which the members were added. Although `removed` and `changed` are also /// arrays, the order of removals and changes is not significant. /// /// `changes` is not created in the collection until the first change occurs. Only the /// change is recorded here. The state behind the change is retrieved from the drivers /// when needed. changes: { // added: [ id, ... ], // removed: [ id, ... ], // changed: [ id, ... ], }, /// The parent collection if this collection is a member of another. Changes applied to /// members of this collection will call `container.change( containerMember )` to also /// set the change flag for the containing member. /// /// For example, members of the `node.events` collection contain listener collections at /// `node.events.existing[name].listeners`. Each listener collection knows its event /// name and points back to `node.events`. Changing a listener will call /// `node.events.change( name )` to mark the event as changed. container: undefined, /// This collection's name in the parent if this collection is a member of another /// collection. Changes to members of this collection will mark that member changed in /// the containing collection. containerMember: undefined, } // Prototype for the `events` collection in the `nodes` objects. this.eventCollectionPrototype = Object.create(self.keyedCollectionPrototype, { create: { value: function (name, changes, parameters) { var value = parameters ? { parameters: parameters.slice(), // clone } : {}; value.listeners = Object.create(self.indexedCollectionPrototype, { container: self.enumerable(this), containerMember: self.enumerable(name), }); return self.keyedCollectionPrototype.create.call(this, name, changes, value); } }, }) /// The application's nodes, indexed by ID. /// /// The kernel defines an application as: /// /// * A tree of nodes, /// * Extending prototypes and implementing behaviors, /// * Publishing properties, and /// * Communicating using methods and events. /// /// This definition is as abstract as possible to avoid imposing unnecessary policy on the /// application. The concrete realization of these concepts lives in the hearts and minds of /// the drivers configured for the application. `nodes` contains the kernel's authoritative /// data about this arrangement. /// /// @name module:vwf~nodes // Note: this is a first step towards moving authoritative data out of the vwf/model/object // and vwf/model/javascript drivers and removing the kernel's dependency on them as special // cases. Only `nodes.existing[id].properties` is currently implemented this way. this.nodes = this.private.nodes = { /// Register a node as it is created. /// /// @param {ID} nodeID /// The ID assigned to the new node. The node will be indexed in `nodes` by this ID. /// @param {ID} prototypeID /// The ID of the node's prototype, or `undefined` if this is the proto-prototype, /// `node.vwf`. /// @param {ID[]} behaviorIDs /// An array of IDs of the node's behaviors. `behaviorIDs` should be an empty array if /// the node doesn't have any behaviors. /// @param {String} nodeURI /// The node's URI. `nodeURI` should be the component URI if this is the root node of /// a component loaded from a URI, and undefined in all other cases. /// @param {String} nodeName /// The node's name. /// @param {ID} parentID /// The ID of the node's parent, or `undefined` if this is the application root node /// or another global, top-level node. /// /// @returns {Object} /// The kernel `node` object if the node was successfully added. `undefined` if a node /// identified by `nodeID` already exists. create: function (nodeID, prototypeID, behaviorIDs, nodeURI, nodeName, parentID) { // if ( ! this.existing[nodeID] ) { let selfNode = this; var prototypeNode = behaviorIDs.reduce(function (prototypeNode, behaviorID) { return selfNode.proxy(prototypeNode, selfNode.existing[behaviorID]); }, this.existing[prototypeID]); // Look up the parent. var parentNode = this.existing[parentID]; // If this is the global root of a new tree, add it to the `globals` set. if (!parentNode) { this.globals[nodeID] = undefined; } // Add the node to the registry. return this.existing[nodeID] = { // id: ..., // Inheritance. -- not implemented here yet; still using vwf/model/object // prototype: ..., // behaviors: [], // Intrinsic state. -- not implemented here yet. // source: ..., // type: ..., uri: nodeURI, name: nodeName, // Internal state. The change flags are omitted until needed. -- not implemented here yet; still using vwf/model/object // sequence: ..., // sequenceChanged: true / false, // prng: ..., // prngChanged: true / false, // Tree. -- not implemented here yet; still using vwf/model/object // parent: ..., // children: [], // Property, Method and Event members defined on the node. properties: Object.create(self.keyedCollectionPrototype, { existing: self.enumerable(Object.create(prototypeNode ? prototypeNode.properties.existing : null)), }), methods: Object.create(self.keyedCollectionPrototype, { existing: self.enumerable(Object.create(prototypeNode ? prototypeNode.methods.existing : null)), }), events: Object.create(self.eventCollectionPrototype, { existing: self.enumerable(Object.create(prototypeNode ? prototypeNode.events.existing : null)), }), // Is this node patchable? Nodes are patchable if they were loaded from a // component. patchable: !!(nodeURI || parentNode && !parentNode.initialized && parentNode.patchable), // Has this node completed initialization? For applications, visibility to // ancestors from uninitialized nodes is blocked. Change tracking starts // after initialization. initialized: false, childsDeleted: {} }; // } else { // return undefined; // } }, /// Record that a node has initialized. initialize: function (nodeID) { if (this.existing[nodeID]) { this.existing[nodeID].initialized = true; return true; } return false; }, /// Unregister a node as it is deleted. delete: function (nodeID, parentID) { if (this.existing[nodeID] && this.existing[parentID]) { this.existing[parentID].childsDeleted[this.existing[nodeID].name] = nodeID; delete this.existing[nodeID]; delete this.globals[nodeID]; return true; } return false; }, /// Create a proxy node in the form of the nodes created by `nodes.create` to represent /// a behavior node in another node's prototype chain. The `existing` objects of the /// proxy's collections link to the prototype collection's `existing` objects, just as /// with a regular prototype. The proxy's members delegate to the corresponding members /// in the behavior. proxy: function (prototypeNode, behaviorNode) { return { properties: { existing: Object.create( prototypeNode ? prototypeNode.properties.existing : null, propertyDescriptorsFor(behaviorNode.properties.existing) ), }, methods: { existing: Object.create( prototypeNode ? prototypeNode.methods.existing : null, propertyDescriptorsFor(behaviorNode.methods.existing) ), }, events: { existing: Object.create( prototypeNode ? prototypeNode.events.existing : null, propertyDescriptorsFor(behaviorNode.events.existing) ), }, }; /// Return an `Object.create` properties object for a proxy object for the provided /// collection's `existing` object. function propertyDescriptorsFor(collectionExisting) { return Object.keys(collectionExisting).reduce( function (propertiesObject, memberName) { propertiesObject[memberName] = { get: function () { return collectionExisting[memberName] }, enumerable: true, }; return propertiesObject; }, {} ); } }, /// Registry of all nodes, indexed by ID. Each is an object created by `nodes.create`. existing: { // id: { // id: ..., // uri: ..., // name: ..., // ... // } }, /// Global root nodes. Each of these is the root of a tree. /// /// The `globals` object is a set: the keys are the data, and only existence on the /// object is significant. globals: { // id: undefined, } } } // == Public functions ===================================================================== // -- loadConfiguration --------------------------------------------------------------------------- // The main page only needs to call vwf.loadConfiguration() to launch the application. Use // require.ready() or jQuery(document).ready() to call loadConfiguration() once the page has // loaded. loadConfiguration() accepts three parameters. // // A component specification identifies the application to be loaded. modelInitializers and // viewInitializers identify the model and view libraries that were parsed out of the URL that // should be attached to the simulation. Each is specified as an object with each library // name as a property of the object with any arguments as the value of the property. // Arguments may be specified as an array [1], as a single value if there is only one [2], or as // undefined if there are none[3]. // // [1] vwf.loadConfiguration( ..., { "vwf/model/glge": [ "#scene, "second param" ] }, { ... } ) // [2] vwf.loadConfiguration( ..., { "vwf/model/glge": "#scene" }, { ... } ) // [3] vwf.loadConfiguration( ..., { "vwf/model/javascript": undefined }, { ... } ) loadConfiguration(app, userLib, cb /* [ componentURI|componentObject ] { modelInitializers } { viewInitializers } */ ) { let self = this; let applicationLoad = app; let userLibraries = userLib || {}; var applicationConfig = {}; let callback = cb; var initializers = { model: [ { library: "/core/vwf/model/ohm", active: true }, { library: "/core/vwf/model/javascript", active: true }, { library: "/core/vwf/model/object", active: true } ], view: [ { library: "/core/vwf/view/ohm", active: true }, { library: "/core/vwf/view/document", active: true }, ] }; //mapLibraryName(requireArray); mapLibraryName(initializers["model"]); mapLibraryName(initializers["view"]); function mapLibraryName(array) { for (var i = 0; i < array.length; i++) { array[array[i].library] = array[i]; } } function getActiveLibraries(libraryList, includeParameters) { var activeLibraryList = []; for (var i = 0; i < libraryList.length; i++) { if (libraryList[i].active) { if (includeParameters) { var activeLibrary = {}; activeLibrary[libraryList[i].library] = libraryList[i].parameters; activeLibraryList.push(activeLibrary); } else { activeLibraryList.push(libraryList[i].library); } } } return activeLibraryList; } // let confPromise = new Promise((resolve, reject) => { // resolve(this.driverConfiguration); // }); let configLibraries = this.driverConfiguration; if (configLibraries && typeof configLibraries == "object") { if (typeof configLibraries.configuration == "object") { applicationConfig = configLibraries.configuration; } Object.keys(configLibraries).forEach(function (libraryType) { if (libraryType == 'info' && configLibraries[libraryType]["title"]) { //jQuery('title').html(configLibraries[libraryType]["title"]); document.querySelector('title').innerHTML = configLibraries[libraryType]["title"] } if (!userLibraries[libraryType]) { userLibraries[libraryType] = {}; } // Merge libraries from config file and URL together. Check for incompatible // libraries, and disable them. Object.keys(configLibraries[libraryType]).forEach(function (libraryName) { var disabled = false; if (!disabled) { if (userLibraries[libraryType][libraryName] == undefined) { userLibraries[libraryType][libraryName] = configLibraries[libraryType][libraryName]; } else if (typeof userLibraries[libraryType][libraryName] == "object" && typeof configLibraries[libraryType][libraryName] == "object") { userLibraries[libraryType][libraryName] = Object.assign({}, configLibraries[libraryType][libraryName], userLibraries[libraryType][libraryName]); } } }); }); } Object.keys(userLibraries).forEach(function (libraryType) { if (initializers[libraryType]) { Object.keys(userLibraries[libraryType]).forEach(function (libraryName) { //if (requireArray[libraryName]) { // requireArray[libraryName].active = true; if(!initializers[libraryType][libraryName]){ initializers[libraryType].unshift({'library': libraryName}); initializers[libraryType][libraryName] = initializers[libraryType][0]; } initializers[libraryType][libraryName].active = true; if (userLibraries[libraryType][libraryName] && userLibraries[libraryType][libraryName] != "") { if (typeof initializers[libraryType][libraryName].parameters == "object") { initializers[libraryType][libraryName].parameters = Object.assign({}, initializers[libraryType][libraryName].parameters, userLibraries[libraryType][libraryName]); } else { initializers[libraryType][libraryName].parameters = userLibraries[libraryType][libraryName]; } } }); } }) // Load default renderer if no other librarys specified // if (Object.keys(userLibraries["model"]).length == 0 && Object.keys(userLibraries["view"]).length == 0) { // // requireArray["vwf/model/threejs"].active = true; // } // With the scripts loaded, we must initialize the framework. vwf.initialize() // accepts three parameters: a world specification, model configuration parameters, // and view configuration parameters. self.initialize(self.applicationLoad, getActiveLibraries(initializers["model"], true), getActiveLibraries(initializers["view"], true), callback); } // -- initialize --------------------------------------------------------------------------- /// The main page only needs to call vwf.initialize() to launch the application. Use /// require.ready() or jQuery(document).ready() to call initialize() once the page has /// loaded. initialize() accepts three parameters. /// /// A component specification identifies the application to be loaded. If a URI is provided, /// the specification is loaded from there [1]. Alternately, a JavaScript object literal /// containing the specfication may be provided [2]. Since a component can extend and /// specialize a prototype, using a simple object literal allows existing component to be /// configured for special uses [3]. /// /// [1] vwf.initialize( "http://vwf.example.com/applications/sample12345", ... ) /// /// [2] vwf.initialize( { source: "model.dae", type: "model/vnd.collada+xml", /// properties: { "p1": ... }, ... }, ... ) /// /// [3] vwf.initialize( { extends: "http://vwf.example.com/applications/sample12345", /// source: "alternate-model.dae", type: "model/vnd.collada+xml" }, ... ) /// /// modelInitializers and viewInitializers identify the model and view modules that should be /// attached to the simulation. Each is specified as an array of objects that map the name of /// a model or view to construct to the set of arguments to pass to its constructor. Modules /// without parameters may be specified as a string [4]. Arguments may be specified as an /// array [5], or as a single value if there is only one [6]. /// /// [4] vwf.initialize( ..., [ "vwf/model/javascript" ], [ ... ] ) /// [5] vwf.initialize( ..., [ { "vwf/model/glge": [ "#scene, "second param" ] } ], [ ... ] ) /// [6] vwf.initialize( ..., [ { "vwf/model/glge": "#scene" } ], [ ... ] ) /// /// @name module:vwf.initialize async initialize( /* [ componentURI|componentObject ] [ modelInitializers ] [ viewInitializers ] */ ) { let self = this; var args = Array.prototype.slice.call(arguments); //var application; // Load the runtime configuration. We start with the factory defaults. The reflector may // provide additional settings when we connect. //this.configuration = require("vwf/configuration").active; // "active" updates in place and changes don't invalidate the reference // Load the kernel utilities. //this.kutility = new KUtility //require("vwf/kernel/utility"); // Create the logger. //this.logger = require("logger").for("vwf", this); // TODO: for( "vwf", ... ), and update existing calls // Get the jQuery reference. This also happens in `loadConfiguration`, but the tests // initialize using `initialize` and don't call `loadConfiguration` first. // jQuery = require("jquery"); // Parse the function parameters. If the first parameter is not an array, then treat it // as the application specification. Otherwise, fall back to the "application" parameter // in the query string. if (typeof args[0] != "object" || !(args[0] instanceof Array)) { this.applicationLoad = args.shift(); } // Shift off the parameter containing the model list and initializer arguments. var modelInitializers = args.shift() || []; // Shift off the parameter containing the view list and initializer arguments. var viewInitializers = args.shift() || []; var callback = args.shift(); var compatibilityStatus = { compatible: true, errors: {} }; // Create the model interface to the kernel. Models can make direct calls that execute // immediately or future calls that are placed on the queue and executed when removed. let modelKernel = new ModelKernel({ id:"vwf/kernel/model" }).factory(); this.models.kernel = modelKernel.create(vwf); //this.models.kernel = require("vwf/kernel/model").create(vwf); // Create and attach each configured model. for (let modelInitializer of modelInitializers) { //modelInitializers.forEach(function (modelInitializer) { // Skip falsy values to allow initializers to be conditionally included by the // loader. if (modelInitializer) { // Accept either { "vwf/model/name": [ arguments] } or "vwf/model/name". if (typeof modelInitializer == "object" && modelInitializer != null) { var modelName = Object.keys(modelInitializer)[0]; var modelArguments = modelInitializer[modelName]; } else { var modelName = modelInitializer; var modelArguments = undefined; } let log = new Log({ id:"vwf/model/stage/log" }).factory(); // var modelMod = undefined; await import(modelName+'.js').then(m=>{ modelMod = (new m.default({ id: modelName }).factory()) }); var model = modelMod.create( this.models.kernel, // model's kernel access [log], //[require("vwf/model/stage/log")], // stages between the kernel and model {}, // state shared with a paired view [].concat(modelArguments || []) // arguments for initialize() ); if (model) { this.models.push(model); this.models[modelName] = model; // also index by id // TODO: this won't work if multiple model instances are allowed if (modelName == "/core/vwf/model/javascript") { // TODO: need a formal way to follow prototype chain from vwf.js; this is peeking inside of vwf-model-javascript this.models.javascript = model; while (this.models.javascript.model) this.models.javascript = this.models.javascript.model; } if (modelName == "/core/vwf/model/object") { // TODO: this is peeking inside of vwf-model-object this.models.object = model; while (this.models.object.model) this.models.object = this.models.object.model; } if (model.model.compatibilityStatus) { if (!model.model.compatibilityStatus.compatible) { compatibilityStatus.compatible = false; Object.assign(compatibilityStatus.errors, model.model.compatibilityStatus.errors); //jQuery.extend(compatibilityStatus.errors, model.model.compatibilityStatus.errors); } } } } } //, this); // Create the view interface to the kernel. Views can only make replicated calls which // bounce off the reflection server, are placed on the queue when received, and executed // when removed. let viewKernel = new ViewKernel({ id:"vwf/kernel/view" }).factory(); this.views.kernel = viewKernel.create(vwf); //this.views.kernel = require("vwf/kernel/view").create(vwf); // Create and attach each configured view. for( let viewInitializer of viewInitializers) { //viewInitializers.forEach(function (viewInitializer) { // Skip falsy values to allow initializers to be conditionally included by the // loader. if (viewInitializer) { // Accept either { "vwf/view/name": [ arguments] } or "vwf/view/name". if (typeof viewInitializer == "object" && viewInitializer != null) { var viewName = Object.keys(viewInitializer)[0]; var viewArguments = viewInitializer[viewName]; } else { var viewName = viewInitializer; var viewArguments = undefined; } // if (!viewName.match("^vwf/view/")) { // old way // var view = this.modules[viewName]; // if (view) { // var instance = new view(); // instance.state = this.models.actual["vwf/model/" + viewName] && this.models.actual["vwf/model/" + viewName].state || {}; // state shared with a paired model // view.apply(instance, [vwf].concat(viewArguments || [])); // this.views.push(instance); // this.views[viewName] = instance; // also index by id // TODO: this won't work if multiple view instances are allowed // if (view.compatibilityStatus) { // if (!view.compatibilityStatus.compatible) { // compatibilityStatus.compatible = false; // Object.assign(compatibilityStatus.errors, view.compatibilityStatus.errors); // // jQuery.extend(compatibilityStatus.errors, view.compatibilityStatus.errors); // } // } // } // } else { // new way var modelPeer = this.models.actual[viewName.replace("view/", "model/")]; // TODO: this.model.actual() is kind of heavy, but it's probably OK to use just a few times here at start-up var viewMod = undefined; await import(viewName +'.js').then(m=>{ viewMod = (new m.default({ id: viewName }).factory()) }) var view = viewMod.create( this.views.kernel, // view's kernel access [], // stages between the kernel and view modelPeer && modelPeer.state || {}, // state shared with a paired model [].concat(viewArguments || []) // arguments for initialize() ); if (view) { this.views.push(view); this.views[viewName] = view; // also index by id // TODO: this won't work if multiple view instances are allowed if (view.compatibilityStatus) { if (!view.compatibilityStatus.compatible) { compatibilityStatus.compatible = false; Object.assign(compatibilityStatus.errors, view.compatibilityStatus.errors); // jQuery.extend(compatibilityStatus.errors, view.compatibilityStatus.errors); } } } //} } } //, this); if (callback) { callback(compatibilityStatus); } //await _app.getApplicationState(); await _app.getApplicationState() .then(res => { return self.chooseConnection(res) }) .then(res => { self.ready(self.application, res) }) } // -- ready -------------------------------------------------------------------------------- /// @name module:vwf.ready ready(component_uri_or_json_or_object, path) { // Connect to the reflector. This implementation uses the socket.io library, which // communicates using a channel back to the server that provided the client documents. if (this.isLuminary) { //Use Luminary for connection this.namespace_ = this.luminary.namespace; //this.helpers.GetNamespace(path.path); //.split(".").join("_"); this.moniker_ = this.luminary.clientID; console.log('namespace: ' + this.namespace_, ' for client: ' + this.moniker_); //let heartbeat = _LCSDB.get('server').get('heartbeat'); var heartbeat = _lum.get(this.namespace_).get('heartbeat'); //_LCSDB.get(vwf.namespace_).get('heartbeat'); if (this.isLuminaryGlobalHB && this.luminaryGlobalHBPath) { let hbPath = this.luminaryGlobalHBPath.split('/'); var heartbeat = _LCSDB; hbPath.forEach(el => { heartbeat = heartbeat.get(el); }) } this.luminary.subscribeOnHeartbeat(heartbeat); this.luminary.subscribeOnMessages(); } else { //Use Reflector for connection this.reflectorClient.connect(component_uri_or_json_or_object, path); } } // -- log ---------------------------------------------------------------------------------- /// Send a log message to the reflector. /// /// @name module:vwf.log log() { this.respond(undefined, "log", undefined, undefined, arguments); } // -- setState ----------------------------------------------------------------------------- /// setState may complete asynchronously due to its dependence on createNode. To prevent /// actions from executing out of order, queue processing must be suspended while setState is /// in progress. createNode suspends the queue when necessary, but additional calls to /// suspend and resume the queue may be needed if other async operations are added. /// /// @name module:vwf.setState /// /// @see {@link module:vwf/api/kernel.setState} setState(appState, callback_async /* () */ ) { let self = this; this.logger.debuggx("setState"); // TODO: loggableState // Set the runtime configuration. var applicationState = appState; if (applicationState.init) { applicationState = JSON.parse(localStorage.getItem('lcs_app')).saveObject } if (applicationState.configuration) { this.configuration = applicationState.configuration; //require("vwf/configuration").instance = applicationState.configuration; } // Update the internal kernel state. if (applicationState.kernel) { if (applicationState.kernel.time !== undefined) self.virtualTime.now = applicationState.kernel.time; } // Create or update global nodes and their descendants. var nodes = applicationState.nodes || []; var annotations = applicationState.annotations || {}; var nodeIndex = 0; async.forEachSeries(nodes, function (nodeComponent, each_callback_async /* ( err ) */ ) { // Look up a possible annotation for this node. For backward compatibility, if the // state has exactly one node and doesn't contain an annotations object, assume the // node is the application. var nodeAnnotation = self.nodes.length > 1 || applicationState.annotations ? annotations[nodeIndex] : "application"; self.createNode(nodeComponent, nodeAnnotation, function (nodeID) /* async */ { each_callback_async(undefined); }); nodeIndex++; }, function (err) /* async */ { // Clear the message queue, except for reflector messages that arrived after the // current action. self.virtualTime.filterQueue(); // Set the queue time and add the incoming items to the queue. if (applicationState.queue) { self.virtualTime.time = applicationState.queue.time; self.virtualTime.insert(applicationState.queue.queue || []); } callback_async && callback_async(); }); this.logger.debugu(); }; // -- getState ----------------------------------------------------------------------------- /// @name module:vwf.getState /// /// @see {@link module:vwf/api/kernel.getState} getState(full, normalize) { let self = this; this.logger.debuggx("getState", full, normalize); // Get the application nodes and queue. var applicationState = { // Runtime configuration. configuration: self.configuration, //require("vwf/configuration").active, // Internal kernel state. kernel: { time: self.virtualTime.now, }, // Global node and descendant deltas. nodes: [ // TODO: all global objects this.getNode("proxy/clients.vwf", full), this.getNode(this.application(), full), ], // `createNode` annotations, keyed by `nodes` indexes. annotations: { 1: "application", }, // Message queue. queue: self.virtualTime.stateQueue }; // Normalize for consistency. if (normalize) { applicationState = self.utility.transform( applicationState, self.utility.transforms.hash); } this.logger.debugu(); return applicationState; }; // -- hashState ---------------------------------------------------------------------------- /// @name module:vwf.hashState /// /// @see {@link module:vwf/api/kernel.hashState} hashState() { this.logger.debuggx("hashState"); var applicationState = this.getState(true, true); // Hash the nodes. var hashn = this.hashNode(applicationState.nodes[0]); // TODO: all global objects // Hash the queue. var hashq = "q" + Crypto.MD5(JSON.stringify(applicationState.queue)).toString().substring(0, 16); // Hash the other kernel properties. var hashk = "k" + Crypto.MD5(JSON.stringify(applicationState.kernel)).toString().substring(0, 16); this.logger.debugu(); // Generate the combined hash. return hashn + ":" + hashq + ":" + hashk; } // -- createNode --------------------------------------------------------------------------- /// Create a node from a component specification. Construction may require loading data from /// multiple remote documents. This function returns before construction is complete. A /// callback is invoked once the node has fully loaded. /// /// A simple node consists of a set of properties, methods and events, but a node may /// specialize a prototype component and may also contain multiple child nodes, any of which /// may specialize a prototype component and contain child nodes, etc. So components cover a /// vast range of complexity. The application definition for the overall simulation is a /// single component instance. /// /// A node is a component instance--a single, anonymous specialization of its component. /// Nodes specialize components in the same way that any component may specialize a prototype /// component. The prototype component is made available as a base, then new or modified /// properties, methods, events, child nodes and scripts are attached to modify the base /// implemenation. /// /// To create a node, we first make the prototoype available by loading it (if it has not /// already been loaded). This is a recursive call to createNode() with the prototype /// specification. Then we add new, and modify existing, properties, methods, and events /// according to the component specification. Then we load and add any children, again /// recursively calling createNode() for each. Finally, we attach any new scripts and invoke /// an initialization function. /// /// createNode may complete asynchronously due to its dependence on setNode, createChild and /// loadComponent. To prevent actions from executing out of order, queue processing must be /// suspended while createNode is in progress. setNode, createChild and loadComponent suspend /// the queue when necessary, but additional calls to suspend and resume the queue may be /// needed if other async operations are added. /// /// @name module:vwf.createNode /// /// @see {@link module:vwf/api/kernel.createNode} createNode (nodeComponent, nodeAnnotation, baseURI, callback_async /* ( nodeID ) */ ) { let self = this; // Interpret `createNode( nodeComponent, callback )` as // `createNode( nodeComponent, undefined, undefined, callback )` and // `createNode( nodeComponent, nodeAnnotation, callback )` as // `createNode( nodeComponent, nodeAnnotation, undefined, callback )`. `nodeAnnotation` // was added in 0.6.12, and `baseURI` was added in 0.6.25. if (typeof nodeAnnotation == "function" || nodeAnnotation instanceof Function) { callback_async = nodeAnnotation; baseURI = undefined; nodeAnnotation = undefined; } else if (typeof baseURI == "function" || baseURI instanceof Function) { callback_async = baseURI; baseURI = undefined; } this.logger.debuggx("createNode", function () { return [JSON.stringify(self.loggableComponent(nodeComponent)), nodeAnnotation]; }); var nodePatch; if (self.componentIsDescriptor(nodeComponent) && nodeComponent.patches) { nodePatch = nodeComponent; nodeComponent = nodeComponent.patches; // TODO: possible sync errors if the patched node is a URI component and the kernel state (time, random) is different from when the node was created on the originating client } // nodeComponent may be a URI, a descriptor, or an ID. While being created, it will // transform from a URI to a descriptor to an ID (depending on its starting state). // nodeURI, nodeDescriptor, and nodeID capture the intermediate states. var nodeURI, nodeDescriptor, nodeID; async.series([ // If `nodeComponent` is a URI, load the descriptor. `nodeComponent` may be a URI, a // descriptor or an ID here. function (series_callback_async /* ( err, results ) */ ) { if (self.componentIsURI(nodeComponent)) { // URI // TODO: allow non-vwf URIs (models, images, etc.) to pass through to stage 2 and pass directly to createChild() // Resolve relative URIs, but leave host-relative, locally-absolute // references intact. nodeURI = nodeComponent //nodeComponent[0] == "/" ? nodeComponent : require( "vwf/utility" ).resolveURI( nodeComponent, baseURI ); // Load the document if we haven't seen this URI yet. Mark the components // list to indicate that this component is loading. if (!self.components[nodeURI]) { // uri is not loading (Array) or is loaded (id) self.components[nodeURI] = []; // [] => array of callbacks while loading => true self.loadComponent(nodeURI, undefined, function (nodeDescriptor) /* async */ { nodeComponent = nodeDescriptor; series_callback_async(undefined, undefined); }, function (errorMessage) { nodeComponent = undefined; series_callback_async(errorMessage, undefined); }); // If we've seen this URI, but it is still loading, just add our callback to // the list. The original load's completion will call our callback too. } else if (self.components[nodeURI] instanceof Array) { // uri is loading callback_async && self.components[nodeURI].push(callback_async); // TODO: is this leaving a series callback hanging if we don't call series_callback_async? // If this URI has already loaded, skip to the end and call the callback // with the ID. } else { // uri is loaded if (nodePatch) { self.setNode(self.components[nodeURI], nodePatch, function (nodeID) /* async */ { callback_async && callback_async(self.components[nodeURI]); // TODO: is this leaving a series callback hanging if we don't call series_callback_async? }); } else { callback_async && callback_async(self.components[nodeURI]); // TODO: is this leaving a series callback hanging if we don't call series_callback_async? } } } else { // descriptor, ID or error series_callback_async(undefined, undefined); } }, // Rudimentary support for `{ includes: prototype }`, which absorbs a prototype // descriptor into the node descriptor before creating the node. // Notes: // // - Only supports one level, so `{ includes: prototype }` won't work if the // prototype also contains a `includes` directive). // - Only works with prototype URIs, so `{ includes: { ... descriptor ... } }` // won't work. // - Loads the prototype on each reference, so unlike real prototypes, multiple // references to the same prototype cause multiple network loads. // // Also see the `mergeDescriptors` limitations. function (series_callback_async /* ( err, results ) */ ) { if (self.componentIsDescriptor(nodeComponent) && nodeComponent.includes && self.componentIsURI(nodeComponent.includes)) { // TODO: for "includes:", accept an already-loaded component (which componentIsURI exludes) since the descriptor will be loaded again var prototypeURI = nodeComponent.includes //require( "vwf/utility" ).resolveURI( nodeComponent.includes, nodeURI || baseURI ); self.loadComponent(prototypeURI, undefined, function (prototypeDescriptor) /* async */ { prototypeDescriptor = self.resolvedDescriptor(prototypeDescriptor, prototypeURI); nodeComponent = self.mergeDescriptors(nodeComponent, prototypeDescriptor); // modifies prototypeDescriptor series_callback_async(undefined, undefined); }, function (errorMessage) { nodeComponent = undefined; series_callback_async(errorMessage, undefined); }); } else { series_callback_async(undefined, undefined); } }, // If `nodeComponent` is a descriptor, construct and get the ID. `nodeComponent` may // be a descriptor or an ID here. function (series_callback_async /* ( err, results ) */ ) { if (self.componentIsDescriptor(nodeComponent)) { // descriptor // TODO: allow non-vwf URIs (models, images, etc.) to pass through to stage 2 and pass directly to createChild() nodeDescriptor = nodeComponent; // Create the node as an unnamed child global object. self.createChild(0, nodeAnnotation, nodeDescriptor, nodeURI, function (nodeID) /* async */ { nodeComponent = nodeID; series_callback_async(undefined, undefined); }); } else { // ID or error series_callback_async(undefined, undefined); } }, // nodeComponent is an ID here. function (series_callback_async /* ( err, results ) */ ) { if (self.componentIsID(nodeComponent) || self.components[nodeComponent] instanceof Array) { // ID nodeID = nodeComponent; if (nodePatch) { self.setNode(nodeID, nodePatch, function (nodeID) /* async */ { series_callback_async(undefined, undefined); }); } else { series_callback_async(undefined, undefined); } } else { // error series_callback_async(undefined, undefined); // TODO: error } }, ], function (err, results) /* async */ { // If this node derived from a URI, save the list of callbacks waiting for // completion and update the component list with the ID. if (nodeURI) { var callbacks_async = self.components[nodeURI]; self.components[nodeURI] = nodeID; } // Pass the ID to our callback. callback_async && callback_async(nodeID); // TODO: handle error if invalid id // Call the other callbacks. if (nodeURI) { callbacks_async.forEach(function (callback_async) { callback_async && callback_async(nodeID); }); } }); this.logger.debugu(); } // -- deleteNode --------------------------------------------------------------------------- /// @name module:vwf.deleteNode /// /// @see {@link module:vwf/api/kernel.deleteNode} deleteNode (nodeID) { let self = this; this.logger.debuggx("deleteNode", nodeID); // Send the meta event into the application. We send it before deleting the child so // that the child will still be available for review. var parentID = this.parent(nodeID); if (parentID !== 0) { var nodeIndex = this.children(parentID).indexOf(nodeID); if (nodeIndex < 0) { nodeIndex = undefined; } if (this.models.kernel.enabled()) { this.fireEvent(parentID, ["children", "removed"], [nodeIndex, this.kutility.nodeReference(nodeID)]); } } // Remove the entry in the components list if this was the root of a component loaded // from a URI. Object.keys(self.components).some(function (nodeURI) { // components: nodeURI => nodeID if (self.components[nodeURI] == nodeID) { delete self.components[nodeURI]; return true; } }); //Delete childs nodes self.children(nodeID).forEach(function(child) { self.deleteNode(child); }); // Call deletingNode() on each model. The node is considered deleted after all models // have run. this.models.forEach(function (model) { model.deletingNode && model.deletingNode(nodeID); }); // Unregister the node. this.nodes.delete(nodeID, parentID); // Call deletedNode() on each view. The view is being notified that a node has been // deleted. this.views.forEach(function (view) { view.deletedNode && view.deletedNode(nodeID); }); this.logger.debugu(); } // -- setNode ------------------------------------------------------------------------------ /// setNode may complete asynchronously due to its dependence on createChild. To prevent /// actions from executing out of order, queue processing must be suspended while setNode is /// in progress. createChild suspends the queue when necessary, but additional calls to /// suspend and resume the queue may be needed if other async operations are added. /// /// @name module:vwf.setNode /// /// @see {@link module:vwf/api/kernel.setNode} setNode (nodeID, nodeComponent, callback_async /* ( nodeID ) */ ) { // TODO: merge with createChild? let self = this; self.logger.debuggx("setNode", function () { return [nodeID, JSON.stringify(self.loggableComponent(nodeComponent))]; }); var node = self.nodes.existing[nodeID]; // Set the internal state. self.models.object.internals(nodeID, nodeComponent); // Suppress kernel reentry so that we can write the state without coloring from // any scripts. self.models.kernel.disable(); // Create the properties, methods, and events. For each item in each set, invoke // createProperty(), createMethod(), or createEvent() to create the field. Each // delegates to the models and views as above. // Properties. nodeComponent.properties && Object.keys(nodeComponent.properties).forEach(function (propertyName) { // TODO: setProperties should be adapted like this to be used here var propertyValue = nodeComponent.properties[propertyName]; // Is the property specification directing us to create a new property, or // initialize a property already defined on a prototype? // Create a new property if the property is not defined on a prototype. // Otherwise, initialize the property. var creating = !node.properties.has(propertyName); // not defined on node or prototype // Translate node references in the descriptor's form `{ node: nodeID }` into kernel // node references. if (self.valueHasAccessors(propertyValue) && propertyValue.node) { propertyValue = self.kutility.nodeReference(propertyValue.node); } // Create or initialize the property. if (creating) { self.createProperty(nodeID, propertyName, propertyValue); } else { self.setProperty(nodeID, propertyName, propertyValue); } // TODO: delete when propertyValue === null in patch }); // Methods. nodeComponent.methods && Object.keys(nodeComponent.methods).forEach(function (methodName) { var methodHandler = nodeComponent.methods[methodName]; var creating = !node.methods.has(methodName); // not defined on node or prototype // Create or initialize the method. if (creating) { self.createMethod(nodeID, methodName, methodHandler.parameters, methodHandler.body); } else { self.setMethod(nodeID, methodName, methodHandler); } // TODO: delete when methodHandler === null in patch }); // Events. nodeComponent.events && Object.keys(nodeComponent.events).forEach(function (eventName) { var eventDescriptor = nodeComponent.events[eventName]; var creating = !node.events.has(eventName); // not defined on node or prototype // Create or initialize the event. if (creating) { self.createEvent(nodeID, eventName, eventDescriptor.parameters); self.setEvent(nodeID, eventName, eventDescriptor); // set the listeners since `createEvent` can't do it yet } else { self.setEvent(nodeID, eventName, eventDescriptor); } // TODO: delete when eventDescriptor === null in patch }); // Restore kernel reentry. self.models.kernel.enable(); async.series([ function (series_callback_async /* ( err, results ) */ ) { // Create and attach the children. For each child, call createChild() with the // child's component specification. createChild() delegates to the models and // views as before. async.forEach(Object.keys(nodeComponent.children || {}), function (childName, each_callback_async /* ( err ) */ ) { var creating = !self.nodeHasOwnChild.call(self, nodeID, childName); if (creating) { self.createChild(nodeID, childName, nodeComponent.children[childName], undefined, function (childID) /* async */ { // TODO: add in original order from nodeComponent.children // TODO: ensure id matches nodeComponent.children[childName].id // TODO: propagate childURI + fragment identifier to children of a URI component? each_callback_async(undefined); }); } else { self.setNode(nodeComponent.children[childName].id || nodeComponent.children[childName].patches, nodeComponent.children[childName], function (childID) /* async */ { each_callback_async(undefined); }); } // TODO: delete when nodeComponent.children[childName] === null in patch }, function (err) /* async */ { let deletedProtoNodes = nodeComponent.childrenDeleted; if (deletedProtoNodes) { let childNames = Object.keys(nodeComponent.childrenDeleted) if (childNames) { childNames.forEach(el => { console.log("DELETE CHILD HERE!: ", el); self.deleteChild(nodeID, el); }); } } series_callback_async(err, undefined); }); }, function (series_callback_async /* ( err, results ) */ ) { // Attach the scripts. For each script, load the network resource if the script // is specified as a URI, then once loaded, call execute() to direct any model // that manages scripts of this script's type to evaluate the script where it // will perform any immediate actions and retain any callbacks as appropriate // for the script type. var scripts = nodeComponent.scripts ? [].concat(nodeComponent.scripts) : []; // accept either an array or a single item var baseURI = self.uri(nodeID, true); async.map(scripts, function (script, map_callback_async /* ( err, result ) */ ) { if (self.valueHasType(script)) { if (script.source) { self.loadScript(script.source, baseURI, function (scriptText) /* async */ { // TODO: this load would be better left to the driver, which may want to ignore it in certain cases, but that would require a completion callback from kernel.execute() map_callback_async(undefined, { text: scriptText, type: script.type }); }, function (errorMessage) { map_callback_async(errorMessage, undefined); }); } else { map_callback_async(undefined, { text: script.text, type: script.type }); } } else { map_callback_async(undefined, { text: script, type: undefined }); } }, function (err, scripts) /* async */ { // Suppress kernel reentry so that initialization functions don't make any // changes during replication. self.models.kernel.disable(); // Create each script. scripts.forEach(function (script) { self.execute(nodeID, script.text, script.type); // TODO: callback }); // Restore kernel reentry. self.models.kernel.enable(); series_callback_async(err, undefined); }); }, ], function (err, results) /* async */ { callback_async && callback_async(nodeID); }); this.logger.debugu(); return nodeComponent; } // -- getNode ------------------------------------------------------------------------------ /// @name module:vwf.getNode /// /// @see {@link module:vwf/api/kernel.getNode} getNode(nodeID, full, normalize, proto) { // TODO: options to include/exclude children, prototypes this.logger.debuggx("getNode", nodeID, full); var node = this.nodes.existing[nodeID]; // Start the descriptor. var nodeComponent = {}; // Arrange the component as a patch if the node originated in a URI component. We want // to refer to the original URI but apply any changes that have been made to the node // since it was loaded. var patches = this.models.object.patches(nodeID), patched = false; if (node.patchable) { nodeComponent.patches = node.uri || nodeID; } else { nodeComponent.id = nodeID; } // Intrinsic state. These don't change once created, so they can be omitted if we're // patching. if (full || !node.patchable) { var intrinsics = this.intrinsics(nodeID); // source, type var prototypeID = this.prototype(nodeID); if (prototypeID === undefined) { nodeComponent.extends = null; } else if (prototypeID !== this.kutility.protoNodeURI) { nodeComponent.extends = this.getNode(prototypeID); // TODO: move to vwf/model/object and get from intrinsics } nodeComponent.implements = this.behaviors(nodeID).map(function (behaviorID) { return this.getNode(behaviorID); // TODO: move to vwf/model/object and get from intrinsics }, this); nodeComponent.implements.length || delete nodeComponent.implements; if (intrinsics.source !== undefined) nodeComponent.source = intrinsics.source; if (intrinsics.type !== undefined) nodeComponent.type = intrinsics.type; } // Internal state. //patches = patches ? patches : {} if (full || !node.patchable || patches.internals) { var internals = this.models.object.internals(nodeID); // sequence and random nodeComponent.sequence = internals.sequence; nodeComponent.random = internals.random; } // Suppress kernel reentry so that we can read the state without coloring from any // scripts. this.models.kernel.disable(); // Properties. if (full || !node.patchable) { // Want everything, or only want patches but the node is not patchable. nodeComponent.properties = this.getProperties(nodeID); // Description of Getters & Setters let jsProps = this.models.javascript.nodes[nodeID]; let getters = jsProps.private.getters; let setters = jsProps.private.setters; Object.keys(nodeComponent.properties).forEach(el => { if (Object.keys(getters).includes(el) || Object.keys(setters).includes(el)) { let prop = { value: nodeComponent.properties[el] } if (typeof getters[el] == 'function') { let fn = getters[el].toString() prop.get = fn.slice(fn.indexOf("{") + 1, fn.lastIndexOf("}")); } else { prop.get = "" } if (typeof setters[el] == 'function') { let fn = setters[el].toString() prop.set = fn.slice(fn.indexOf("{") + 1, fn.lastIndexOf("}")); } else { prop.set = "" } nodeComponent.properties[el] = prop } }) ///end of Getters & Setters for (var propertyName in nodeComponent.properties) { var propertyValue = nodeComponent.properties[propertyName]; if (propertyValue === undefined) { delete nodeComponent.properties[propertyName]; } else if (this.kutility.valueIsNodeReference(propertyValue)) { // Translate kernel node references into descriptor node references. nodeComponent.properties[propertyName] = { node: propertyValue.id }; } } } else if (node.properties.changes) { // The node is patchable and properties have changed. nodeComponent.properties = {}; Object.keys(node.properties.changes).forEach(function (propertyName) { if (node.properties.changes[propertyName] !== "removed") { // TODO: handle delete var propertyValue = this.getProperty(nodeID, propertyName); if (this.kutility.valueIsNodeReference(propertyValue)) { // Translate kernel node references into descriptor node references. nodeComponent.properties[propertyName] = { node: propertyValue.id }; } else { nodeComponent.properties[propertyName] = propertyValue; } } }, this); } if (Object.keys(nodeComponent.properties).length == 0) { delete nodeComponent.properties; } else { patched = true; } // Methods. if (full || !node.patchable) { Object.keys(node.methods.existing).forEach(function (methodName) { nodeComponent.methods = nodeComponent.methods || {}; nodeComponent.methods[methodName] = this.getMethod(nodeID, methodName); patched = true; }, this); } else if (node.methods.changes) { Object.keys(node.methods.changes).forEach(function (methodName) { if (node.methods.changes[methodName] !== "removed") { // TODO: handle delete nodeComponent.methods = nodeComponent.methods || {}; nodeComponent.methods[methodName] = this.getMethod(nodeID, methodName); patched = true; } }, this); } // Events. var events = full || !node.patchable ? node.events.existing : node.events.changes; if (events) { Object.keys(events).forEach(function (eventName) { nodeComponent.events = nodeComponent.events || {}; nodeComponent.events[eventName] = this.getEvent(nodeID, eventName); patched = true; }, this); } // Restore kernel reentry. this.models.kernel.enable(); // Children. nodeComponent.children = {}; this.children(nodeID).forEach(function (childID) { nodeComponent.children[this.name(childID)] = this.getNode(childID, full); }, this); for (var childName in nodeComponent.children) { // TODO: distinguish add, change, remove if (nodeComponent.children[childName] === undefined) { delete nodeComponent.children[childName]; } } ////CHECK FOR DELETED PROTO NODES nodeComponent.childrenDeleted = {}; Object.keys(node.childsDeleted).forEach((childName) => { nodeComponent.childrenDeleted[childName] = null; patched = true; }); //// if (Object.keys(nodeComponent.children).length == 0) { delete nodeComponent.children; } else { patched = true; } // Scripts. // TODO: scripts if (node.scripts) { console.log("SCRIPTS: ", node.scripts) } // Normalize for consistency. if (normalize) { nodeComponent = self.utility.transform( nodeComponent, self.utility.transforms.hash); } this.logger.debugu(); // Return the descriptor created, unless it was arranged as a patch and there were no // changes. Otherwise, return the URI if this is the root of a URI component. if (full || !node.patchable || patched) { return nodeComponent; } else if (node.uri) { return node.uri; } else { return undefined; } }; // -- hashNode ----------------------------------------------------------------------------- /// @name module:vwf.hashNode /// /// @see {@link module:vwf/api/kernel.hashNode} hashNode(nodeID) { // TODO: works with patches? // TODO: only for nodes from getNode( , , true ) this.logger.debuggx("hashNode", typeof nodeID == "object" ? nodeID.id : nodeID); var nodeComponent = typeof nodeID == "object" ? nodeID : this.getNode(nodeID, true, true); // Hash the intrinsic state. var internal = { id: nodeComponent.id, source: nodeComponent.source, type: nodeComponent.type }; // TODO: get subset same way as getNode() puts them in without calling out specific field names internal.source === undefined && delete internal.source; internal.type === undefined && delete internal.type; var hashi = "i" + Crypto.MD5(JSON.stringify(internal)).toString().substring(0, 16); // Hash the properties. var properties = nodeComponent.properties || {}; var hashp = Object.keys(properties).length ? "p" + Crypto.MD5(JSON.stringify(properties)).toString().substring(0, 16) : undefined; // Hash the children. var children = nodeComponent.children || {}; var hashc = Object.keys(children).length ? "c" + Crypto.MD5(JSON.stringify(children)).toString().substring(0, 16) : undefined; this.logger.debugu(); // Generate the combined hash. return hashi + (hashp ? "." + hashp : "") + (hashc ? "/" + hashc : ""); } // -- createChild -------------------------------------------------------------------------- /// When we arrive here, we have a prototype node in hand (by way of its ID) and an object /// containing a component specification. We now need to create and assemble the new node. /// /// The VWF manager doesn't directly manipulate any node. The various models act in /// federation to create the greater model. The manager simply routes messages within the /// system to allow the models to maintain the necessary data. Additionally, the views /// receive similar messages that allow them to keep their interfaces current. /// /// To create a node, we simply assign a new ID, then invoke a notification on each model and /// a notification on each view. /// /// createChild may complete asynchronously due to its dependence on createNode and the /// creatingNode and createdNode driver calls. To prevent actions from executing out of /// order, queue processing must be suspended while createChild is in progress. createNode /// and the driver callbacks suspend the queue when necessary, but additional calls to /// suspend and resume the queue may be needed if other async operations are added. /// /// @name module:vwf.createChild /// /// @see {@link module:vwf/api/kernel.createChild} createChild(nodeID, childName, childComponent, childURI, callback_async /* ( childID ) */ ) { let self = this; this.logger.debuggx("createChild", function () { return [nodeID, childName, JSON.stringify(self.loggableComponent(childComponent)), childURI]; }); childComponent = self.normalizedComponent(childComponent); var child, childID, childIndex, childPrototypeID, childBehaviorIDs = [], deferredInitializations = {}; var resolvedSource; // resolved `childComponent.source` for the drivers. // Determine if we're replicating previously-saved state, or creating a fresh object. var replicating = !!childComponent.id; // Allocate an ID for the node. IDs must be unique and consistent across all clients // sharing the same instance regardless of the component load order. Each node maintains // a sequence counter, and we allocate the ID based on the parent's sequence counter and // ID. Top-level nodes take the ID from their origin URI when available or from a hash // of the descriptor. An existing ID is used when synchronizing to state drawn from // another client or to a previously-saved state. if (childComponent.id) { // incoming replication: pre-calculated id childID = childComponent.id; childIndex = this.children(nodeID).length; } else if (nodeID === 0) { // global: component's URI or hash of its descriptor childID = childURI || Crypto.MD5(JSON.stringify(childComponent)).toString(); // TODO: MD5 may be too slow here childIndex = childURI; } else { // descendant: parent id + next from parent's sequence childID = nodeID + ":" + this.sequence(nodeID) + (this.configuration["randomize-ids"] ? "-" + ("0" + Math.floor(this.random(nodeID) * 100)).slice(-2) : "") + (this.configuration["humanize-ids"] ? "-" + childName.replace(/[^0-9A-Za-z_-]+/g, "-") : ""); childIndex = this.children(nodeID).length; } // Register the node. child = self.nodes.create(childID, childPrototypeID, childBehaviorIDs, childURI, childName, nodeID); // Register the node in vwf/model/object. Since the kernel delegates many node // information functions to vwf/model/object, this serves to register it with the // kernel. The node must be registered before any async operations occur to ensure that // the parent's child list is correct when following siblings calculate their index // numbers. this.models.object.creatingNode(nodeID, childID, childPrototypeID, childBehaviorIDs, childComponent.source, childComponent.type, childIndex, childName); // TODO: move node metadata back to the kernel and only use vwf/model/object just as a property store? // The base URI for relative references is the URI of this node or the closest ancestor. var baseURI = this.uri(childID, true); // Construct the node. async.series([ function (series_callback_async /* ( err, results ) */ ) { // Rudimentary support for `{ includes: prototype }`, which absorbs a prototype // descriptor into the child descriptor before creating the child. See the notes // in `createNode` and the `mergeDescriptors` limitations. // This first task always completes asynchronously (even if it doesn't perform // an async operation) so that the stack doesn't grow from node to node while // createChild() recursively traverses a component. If this task is moved, // replace it with an async stub, or make the next task exclusively async. if (self.componentIsDescriptor(childComponent) && childComponent.includes && self.componentIsURI(childComponent.includes)) { // TODO: for "includes:", accept an already-loaded component (which componentIsURI exludes) since the descriptor will be loaded again var prototypeURI = childComponent.includes //require( "vwf/utility" ).resolveURI( childComponent.includes, baseURI ); var sync = true; // will loadComponent() complete synchronously? self.loadComponent(prototypeURI, undefined, function (prototypeDescriptor) /* async */ { // Resolve relative references with respect to the included component. prototypeDescriptor = self.resolvedDescriptor(prototypeDescriptor, prototypeURI); // Merge the child descriptor onto the `includes` descriptor. childComponent = self.mergeDescriptors(childComponent, prototypeDescriptor); // modifies prototypeDescriptor if (sync) { self.virtualTime.suspend("before beginning " + childID); // suspend the queue async.nextTick(function () { series_callback_async(undefined, undefined); self.virtualTime.resume("after beginning " + childID); // resume the queue; may invoke dispatch(), so call last before returning to the host }); } else { series_callback_async(undefined, undefined); } }, function (errorMessage) { childComponent = undefined; series_callback_async(errorMessage, undefined); }); sync = false; // not if we got here first } else { self.virtualTime.suspend("before beginning " + childID); // suspend the queue async.nextTick(function () { series_callback_async(undefined, undefined); self.virtualTime.resume("after beginning " + childID); // resume the queue; may invoke dispatch(), so call last before returning to the host }); } }, function (series_callback_async /* ( err, results ) */ ) { // Create the prototype and behavior nodes (or locate previously created // instances). async.parallel([ function (parallel_callback_async /* ( err, results ) */ ) { // Create or find the prototype and save the ID in childPrototypeID. if (childComponent.extends !== null) { // TODO: any way to prevent node loading node as a prototype without having an explicit null prototype attribute in node? var prototypeComponent = childComponent.extends || self.kutility.protoNodeURI; self.createNode(prototypeComponent, undefined, baseURI, function (prototypeID) /* async */ { childPrototypeID = prototypeID; // TODO: the GLGE driver doesn't handle source/type or properties in prototypes properly; as a work-around pull those up into the component when not already defined if (!childComponent.source) { var prototype_intrinsics = self.intrinsics(prototypeID); if (prototype_intrinsics.source) { var prototype_uri = self.uri(prototypeID); var prototype_properties = self.getProperties(prototypeID); childComponent.source = prototype_intrinsics.source //require( "vwf/utility" ).resolveURI( prototype_intrinsics.source, prototype_uri ); childComponent.type = prototype_intrinsics.type; childComponent.properties = childComponent.properties || {}; Object.keys(prototype_properties).forEach(function (prototype_property_name) { if (childComponent.properties[prototype_property_name] === undefined && prototype_property_name != "transform") { childComponent.properties[prototype_property_name] = prototype_properties[prototype_property_name]; } }); } } parallel_callback_async(undefined, undefined); }); } else { childPrototypeID = undefined; parallel_callback_async(undefined, undefined); } }, function (parallel_callback_async /* ( err, results ) */ ) { // Create or find the behaviors and save the IDs in childBehaviorIDs. var behaviorComponents = childComponent.implements ? [].concat(childComponent.implements) : []; // accept either an array or a single item async.map(behaviorComponents, function (behaviorComponent, map_callback_async /* ( err, result ) */ ) { self.createNode(behaviorComponent, undefined, baseURI, function (behaviorID) /* async */ { map_callback_async(undefined, behaviorID); }); }, function (err, behaviorIDs) /* async */ { childBehaviorIDs = behaviorIDs; parallel_callback_async(err, undefined); }); }, ], function (err, results) /* async */ { series_callback_async(err, undefined); }); }, function (series_callback_async /* ( err, results ) */ ) { // Re-register the node now that we have the prototypes and behaviors. child = self.nodes.create(childID, childPrototypeID, childBehaviorIDs, childURI, childName, nodeID); // For the proto-prototype node `node.vwf`, register the meta events. if (childID === self.kutility.protoNodeURI) { child.events.create(self.namespaceEncodedName(["properties", "created"])); child.events.create(self.namespaceEncodedName(["properties", "initialized"])); child.events.create(self.namespaceEncodedName(["properties", "deleted"])); child.events.create(self.namespaceEncodedName(["methods", "created"])); child.events.create(self.namespaceEncodedName(["methods", "deleted"])); child.events.create(self.namespaceEncodedName(["events", "created"])); child.events.create(self.namespaceEncodedName(["events", "deleted"])); child.events.create(self.namespaceEncodedName(["children", "added"])); child.events.create(self.namespaceEncodedName(["children", "removed"])); } // Re-register the node in vwf/model/object now that we have the prototypes and // behaviors. vwf/model/object knows that we call it more than once and only // updates the new information. self.models.object.creatingNode(nodeID, childID, childPrototypeID, childBehaviorIDs, childComponent.source, childComponent.type, childIndex, childName); // TODO: move node metadata back to the kernel and only use vwf/model/object just as a property store? // Resolve the asset source URL for the drivers. resolvedSource = childComponent.source && childComponent.source //require( "vwf/utility" ).resolveURI( childComponent.source, baseURI ); // Call creatingNode() on each model. The node is considered to be constructed // after all models have run. async.forEachSeries(self.models, function (model, each_callback_async /* ( err ) */ ) { var driver_ready = true; var timeoutID; // TODO: suppress kernel reentry here (just for childID?) with kernel/model showing a warning when breached; no actions are allowed until all drivers have seen creatingNode() model.creatingNode && model.creatingNode(nodeID, childID, childPrototypeID, childBehaviorIDs, resolvedSource, childComponent.type, childIndex, childName, function (ready) /* async */ { if (driver_ready && !ready) { suspend(); } else if (!driver_ready && ready) { resume(); } function suspend() { self.virtualTime.suspend("while loading " + childComponent.source + " for " + childID + " in creatingNode"); // suspend the queue timeoutID = window.setTimeout(function () { resume("timeout loading " + childComponent.source) }, self.configuration["load-timeout"] * 1000); driver_ready = false; } function resume(err) { window.clearTimeout(timeoutID); driver_ready = true; err && self.logger.warnx("createChild", nodeID, childName + ":", err); each_callback_async(err); // resume createChild() self.virtualTime.resume("after loading " + childComponent.source + " for " + childID + " in creatingNode"); // resume the queue; may invoke dispatch(), so call last before returning to the host } }); // TODO: restore kernel reentry here driver_ready && each_callback_async(undefined); }, function (err) /* async */ { series_callback_async(err, undefined); }); }, function (series_callback_async /* ( err, results ) */ ) { // Call createdNode() on each view. The view is being notified of a node that has // been constructed. async.forEach(self.views, function (view, each_callback_async /* ( err ) */ ) { var driver_ready = true; var timeoutID; view.createdNode && view.createdNode(nodeID, childID, childPrototypeID, childBehaviorIDs, resolvedSource, childComponent.type, childIndex, childName, function (ready) /* async */ { if (driver_ready && !ready) { suspend(); } else if (!driver_ready && ready) { resume(); } function suspend() { self.virtualTime.suspend("while loading " + childComponent.source + " for " + childID + " in createdNode"); // suspend the queue timeoutID = window.setTimeout(function () { resume("timeout loading " + childComponent.source) }, self.configuration["load-timeout"] * 1000); driver_ready = false; } function resume(err) { window.clearTimeout(timeoutID); driver_ready = true; err && self.logger.warnx("createChild", nodeID, childName + ":", err); each_callback_async(err); // resume createChild() self.virtualTime.resume("after loading " + childComponent.source + " for " + childID + " in createdNode"); // resume the queue; may invoke dispatch(), so call last before returning to the host } }); driver_ready && each_callback_async(undefined); }, function (err) /* async */ { series_callback_async(err, undefined); }); }, function (series_callback_async /* ( err, results ) */ ) { // Set the internal state. self.models.object.internals(childID, childComponent); // Suppress kernel reentry so that we can read the state without coloring from // any scripts. replicating && self.models.kernel.disable(); // Create the properties, methods, and events. For each item in each set, invoke // createProperty(), createMethod(), or createEvent() to create the field. Each // delegates to the models and views as above. childComponent.properties && Object.keys(childComponent.properties).forEach(function (propertyName) { var propertyValue = childComponent.properties[propertyName]; var value = propertyValue, get, set, create; if (self.valueHasAccessors(propertyValue)) { value = propertyValue.node ? self.kutility.nodeReference(propertyValue.node) : propertyValue.value; get = propertyValue.get; set = propertyValue.set; create = propertyValue.create; } // Is the property specification directing us to create a new property, or // initialize a property already defined on a prototype? // Create a new property if an explicit getter or setter are provided or if // the property is not defined on a prototype. Initialize the property when // the property is already defined on a prototype and no explicit getter or // setter are provided. var creating = create || // explicit create directive, or get !== undefined || set !== undefined || // explicit accessor, or !child.properties.has(propertyName); // not defined on prototype // Are we assigning the value here, or deferring assignment until the node // is constructed because setters will run? var assigning = value === undefined || // no value, or set === undefined && (creating || !self.nodePropertyHasSetter.call(self, childID, propertyName)) || // no setter, or replicating; // replicating previously-saved state (setters never run during replication) if (!assigning) { deferredInitializations[propertyName] = value; value = undefined; } // Create or initialize the property. if (creating) { self.createProperty(childID, propertyName, value, get, set); } else { self.setProperty(childID, propertyName, value); } }); childComponent.methods && Object.keys(childComponent.methods).forEach(function (methodName) { var methodValue = childComponent.methods[methodName]; if (self.valueHasBody(methodValue)) { self.createMethod(childID, methodName, methodValue.parameters, methodValue.body); } else { self.createMethod(childID, methodName, undefined, methodValue); } }); childComponent.events && Object.keys(childComponent.events).forEach(function (eventName) { var eventValue = childComponent.events[eventName]; if (self.valueHasBody(eventValue)) { self.createEvent(childID, eventName, eventValue.parameters); self.setEvent(childID, eventName, eventValue); // set the listeners since `createEvent` can't do it yet } else { self.createEvent(childID, eventName, undefined); } }); // Restore kernel reentry. replicating && self.models.kernel.enable(); // Create and attach the children. For each child, call createChild() with the // child's component specification. createChild() delegates to the models and // views as before. async.forEach(Object.keys(childComponent.children || {}), function (childName, each_callback_async /* ( err ) */ ) { var childValue = childComponent.children[childName]; self.createChild(childID, childName, childValue, undefined, function (childID) /* async */ { // TODO: add in original order from childComponent.children // TODO: propagate childURI + fragment identifier to children of a URI component? each_callback_async(undefined); }); }, function (err) /* async */ { series_callback_async(err, undefined); }); }, function (series_callback_async /* ( err, results ) */ ) { // Attach the scripts. For each script, load the network resource if the script is // specified as a URI, then once loaded, call execute() to direct any model that // manages scripts of this script's type to evaluate the script where it will // perform any immediate actions and retain any callbacks as appropriate for the // script type. var scripts = childComponent.scripts ? [].concat(childComponent.scripts) : []; // accept either an array or a single item async.map(scripts, function (script, map_callback_async /* ( err, result ) */ ) { if (self.valueHasType(script)) { if (script.source) { self.loadScript(script.source, baseURI, function (scriptText) /* async */ { // TODO: this load would be better left to the driver, which may want to ignore it in certain cases, but that would require a completion callback from kernel.execute() map_callback_async(undefined, { text: scriptText, type: script.type }); }, function (errorMessage) { map_callback_async(errorMessage, undefined); }); } else { map_callback_async(undefined, { text: script.text, type: script.type }); } } else { map_callback_async(undefined, { text: script, type: undefined }); } }, function (err, scripts) /* async */ { // Watch for any async kernel calls generated as we run the scripts and wait // for them complete before completing the node. self.models.kernel.capturingAsyncs(function () { // Suppress kernel reentry so that initialization functions don't make // any changes during replication. replicating && self.models.kernel.disable(); // Create each script. scripts.forEach(function (script) { self.execute(childID, script.text, script.type); // TODO: callback }); // Perform initializations for properties with setter functions. These // are assigned here so that the setters run on a fully-constructed node. Object.keys(deferredInitializations).forEach(function (propertyName) { self.setProperty(childID, propertyName, deferredInitializations[propertyName]); }); // TODO: Adding the node to the tickable list here if it contains a tick() function in JavaScript at initialization time. Replace with better control of ticks on/off and the interval by the node. if (self.execute(childID, "Boolean( this.tick )")) { self.tickable.nodeIDs.push(childID); } // Restore kernel reentry. replicating && self.models.kernel.enable(); }, function () { // This function is called when all asynchronous calls from the previous // function have returned. // Call initializingNode() on each model and initializedNode() on each // view to indicate that the node is fully constructed. // Since nodes don't (currently) inherit their prototypes' children, // for each prototype also call initializingNodeFromPrototype() to allow // model drivers to apply the prototypes' initializers to the node. async.forEachSeries(self.prototypes(childID, true).reverse().concat(childID), function (childInitializingNodeID, each_callback_async /* err */ ) { // Call initializingNode() on each model. self.models.kernel.capturingAsyncs(function () { self.models.forEach(function (model) { // Suppress kernel reentry so that initialization functions // don't make any changes during replication. replicating && self.models.kernel.disable(); // For a prototype, call `initializingNodeFromPrototype` to // run the prototype's initializer on the node. For the // node, call `initializingNode` to run its own initializer. // // `initializingNodeFromPrototype` is separate from // `initializingNode` so that `initializingNode` remains a // single call that indicates that the node is fully // constructed. Existing drivers, and any drivers that don't // care about prototype initializers will by default ignore // the intermediate initializations. // (`initializingNodeFromPrototype` was added in 0.6.23.) if (childInitializingNodeID !== childID) { model.initializingNodeFromPrototype && model.initializingNodeFromPrototype(nodeID, childID, childInitializingNodeID); } else { model.initializingNode && model.initializingNode(nodeID, childID, childPrototypeID, childBehaviorIDs, resolvedSource, childComponent.type, childIndex, childName); } // Restore kernel reentry. replicating && self.models.kernel.enable(); }); }, function () { each_callback_async(undefined); }); }, function (err) /* async */ { // Call initializedNode() on each view. self.views.forEach(function (view) { view.initializedNode && view.initializedNode(nodeID, childID, childPrototypeID, childBehaviorIDs, resolvedSource, childComponent.type, childIndex, childName); }); // Mark the node as initialized. self.nodes.initialize(childID); // Send the meta event into the application. if (!replicating && nodeID !== 0) { self.fireEvent(nodeID, ["children", "added"], [childIndex, self.kutility.nodeReference(childID)]); } // Dismiss the loading spinner if (childID === self.application()) { _app.hideProgressBar(); } series_callback_async(err, undefined); }); }); }); }, ], function (err, results) /* async */ { // The node is complete. Invoke the callback method and pass the new node ID and the // ID of its prototype. If this was the root node for the application, the // application is now fully initialized. // Always complete asynchronously so that the stack doesn't grow from node to node // while createChild() recursively traverses a component. if (callback_async) { self.virtualTime.suspend("before completing " + childID); // suspend the queue async.nextTick(function () { callback_async(childID); self.virtualTime.resume("after completing " + childID); // resume the queue; may invoke dispatch(), so call last before returning to the host }); } }); this.logger.debugu(); } // -- deleteChild -------------------------------------------------------------------------- /// @name module:vwf.deleteChild /// /// @see {@link module:vwf/api/kernel.deleteChild} deleteChild(nodeID, childName) { var childID = this.children(nodeID).filter(function (childID) { return this.name(childID) === childName; }, this)[0]; if (childID !== undefined) { return this.deleteNode(childID); } } // -- addChild ----------------------------------------------------------------------------- /// @name module:vwf.addChild /// /// @see {@link module:vwf/api/kernel.addChild} addChild(nodeID, childID, childName) { this.logger.debuggx("addChild", nodeID, childID, childName); // Call addingChild() on each model. The child is considered added after all models have // run. this.models.forEach(function (model) { model.addingChild && model.addingChild(nodeID, childID, childName); }); // Call addedChild() on each view. The view is being notified that a child has been // added. this.views.forEach(function (view) { view.addedChild && view.addedChild(nodeID, childID, childName); }); this.logger.debugu(); } // -- removeChild -------------------------------------------------------------------------- /// @name module:vwf.removeChild /// /// @see {@link module:vwf/api/kernel.removeChild} removeChild(nodeID, childID) { this.logger.debuggx("removeChild", nodeID, childID); // Call removingChild() on each model. The child is considered removed after all models // have run. this.models.forEach(function (model) { model.removingChild && model.removingChild(nodeID, childID); }); // Call removedChild() on each view. The view is being notified that a child has been // removed. this.views.forEach(function (view) { view.removedChild && view.removedChild(nodeID, childID); }); this.logger.debugu(); } // -- setProperties ------------------------------------------------------------------------ /// Set all of the properties for a node. /// /// @name module:vwf.setProperties /// /// @see {@link module:vwf/api/kernel.setProperties} setProperties(nodeID, properties) { // TODO: rework as a cover for setProperty(), or remove; passing all properties to each driver is impractical since initializing and setting are different, and reentry can't be controlled when multiple sets are in progress. let self = this; this.logger.debuggx("setProperties", nodeID, properties); var node = self.nodes.existing[nodeID]; var entrants = this.setProperty.entrants; // Call settingProperties() on each model. properties = this.models.reduceRight(function (intermediate_properties, model, index) { // TODO: note that we can't go left to right and stop after the first that accepts the set since we are setting all of the properties as a batch; verify that this creates the same result as calling setProperty individually on each property and that there are no side effects from setting through a driver after the one that handles the set. var model_properties = {}; if (model.settingProperties) { model_properties = model.settingProperties(nodeID, properties); } else if (model.settingProperty) { Object.keys(node.properties.existing).forEach(function (propertyName) { if (properties[propertyName] !== undefined) { var reentry = entrants[nodeID + '-' + propertyName] = { index: index }; // the active model number from this call // TODO: need unique nodeID+propertyName hash model_properties[propertyName] = model.settingProperty(nodeID, propertyName, properties[propertyName]); if (self.models.kernel.blocked()) { model_properties[propertyName] = undefined; // ignore result from a blocked setter } delete entrants[nodeID + '-' + propertyName]; } }); } Object.keys(node.properties.existing).forEach(function (propertyName) { if (model_properties[propertyName] !== undefined) { // copy values from this model intermediate_properties[propertyName] = model_properties[propertyName]; } else if (intermediate_properties[propertyName] === undefined) { // as well as recording any new keys intermediate_properties[propertyName] = undefined; } }); return intermediate_properties; }, {}); // Record the change. if (node.initialized && node.patchable) { Object.keys(properties).forEach(function (propertyName) { node.properties.change(propertyName); }); } // Call satProperties() on each view. this.views.forEach(function (view) { if (view.satProperties) { view.satProperties(nodeID, properties); } else if (view.satProperty) { for (var propertyName in properties) { view.satProperty(nodeID, propertyName, properties[propertyName]); // TODO: be sure this is the value actually set, not the incoming value } } }); this.logger.debugu(); return properties; } // -- getProperties ------------------------------------------------------------------------ /// Get all of the properties for a node. /// /// @name module:vwf.getProperties /// /// @see {@link module:vwf/api/kernel.getProperties} getProperties(nodeID) { // TODO: rework as a cover for getProperty(), or remove; passing all properties to each driver is impractical since reentry can't be controlled when multiple gets are in progress. let self = this; this.logger.debuggx("getProperties", nodeID); var node = self.nodes.existing[nodeID]; var entrants = this.getProperty.entrants; // Call gettingProperties() on each model. var properties = this.models.reduceRight(function (intermediate_properties, model, index) { // TODO: note that we can't go left to right and take the first result since we are getting all of the properties as a batch; verify that this creates the same result as calling getProperty individually on each property and that there are no side effects from getting through a driver after the one that handles the get. var model_properties = {}; if (model.gettingProperties) { model_properties = model.gettingProperties(nodeID, properties); } else if (model.gettingProperty) { Object.keys(node.properties.existing).forEach(function (propertyName) { var reentry = entrants[nodeID + '-' + propertyName] = { index: index }; // the active model number from this call // TODO: need unique nodeID+propertyName hash model_properties[propertyName] = model.gettingProperty(nodeID, propertyName, intermediate_properties[propertyName]); if (self.models.kernel.blocked()) { model_properties[propertyName] = undefined; // ignore result from a blocked getter } delete entrants[nodeID + '-' + propertyName]; }); } Object.keys(node.properties.existing).forEach(function (propertyName) { if (model_properties[propertyName] !== undefined) { // copy values from this model intermediate_properties[propertyName] = model_properties[propertyName]; } else if (intermediate_properties[propertyName] === undefined) { // as well as recording any new keys intermediate_properties[propertyName] = undefined; } }); return intermediate_properties; }, {}); // Call gotProperties() on each view. this.views.forEach(function (view) { if (view.gotProperties) { view.gotProperties(nodeID, properties); } else if (view.gotProperty) { for (var propertyName in properties) { view.gotProperty(nodeID, propertyName, properties[propertyName]); // TODO: be sure this is the value actually gotten and not an intermediate value from above } } }); this.logger.debugu(); return properties; } // -- createProperty ----------------------------------------------------------------------- /// Create a property on a node and assign an initial value. /// /// @name module:vwf.createProperty /// /// @see {@link module:vwf/api/kernel.createProperty} createProperty(nodeID, propertyName, propertyValue, propertyGet, propertySet) { let self = this; this.logger.debuggx("createProperty", function () { return [nodeID, propertyName, JSON.stringify(self.loggableValue(propertyValue)), self.loggableScript(propertyGet), self.loggableScript(propertySet) ]; }); var node = self.nodes.existing[nodeID]; // Register the property. node.properties.create(propertyName, node.initialized && node.patchable); // Call creatingProperty() on each model. The property is considered created after all // models have run. this.models.forEach(function (model) { model.creatingProperty && model.creatingProperty(nodeID, propertyName, propertyValue, propertyGet, propertySet); }); // Call createdProperty() on each view. The view is being notified that a property has // been created. this.views.forEach(function (view) { view.createdProperty && view.createdProperty(nodeID, propertyName, propertyValue, propertyGet, propertySet); }); // Send the meta event into the application. if (this.models.kernel.enabled()) { this.fireEvent(nodeID, ["properties", "created"], [propertyName]); } this.logger.debugu(); return propertyValue; } // -- setProperty -------------------------------------------------------------------------- /// Set a property value on a node. /// /// @name module:vwf.setProperty /// /// @see {@link module:vwf/api/kernel.setProperty} setProperty(nodeID, propertyName, propertyValue) { let self = this; this.logger.debuggx("setProperty", function () { return [nodeID, propertyName, JSON.stringify(self.loggableValue(propertyValue))]; }); var node = self.nodes.existing[nodeID]; // Record calls into this function by nodeID and propertyName so that models may call // back here (directly or indirectly) to delegate responses further down the chain // without causing infinite recursion. var entrants = this.setProperty.entrants; var entry = entrants[nodeID + '-' + propertyName] || {}; // the most recent call, if any // TODO: need unique nodeID+propertyName hash var reentry = entrants[nodeID + '-' + propertyName] = {}; // this call // Select the actual driver calls. Create the property if it doesn't exist on this node // or its prototypes. Initialize it if it exists on a prototype but not on this node. // Set it if it already exists on this node. if (!node.properties.has(propertyName) || entry.creating) { reentry.creating = true; var settingPropertyEtc = "creatingProperty"; var satPropertyEtc = "createdProperty"; node.properties.create(propertyName, node.initialized && node.patchable); } else if (!node.properties.hasOwn(propertyName) || entry.initializing) { reentry.initializing = true; var settingPropertyEtc = "initializingProperty"; var satPropertyEtc = "initializedProperty"; node.properties.create(propertyName, node.initialized && node.patchable); } else { var settingPropertyEtc = "settingProperty"; var satPropertyEtc = "satProperty"; } // Keep track of the number of assignments made by this `setProperty` call and others // invoked indirectly by it, starting with the outermost call. var outermost = entrants.assignments === undefined; if (outermost) { entrants.assignments = 0; } // Have we been called for the same property on the same node for a property still being // assigned (such as when a setter function assigns the property to itself)? If so, then // the inner call should skip drivers that the outer call has already invoked, and the // outer call should complete without invoking drivers that the inner call will have // already called. var reentered = (entry.index !== undefined); // We'll need to know if the set was delegated to other properties or actually assigned // here. var delegated = false, assigned = false; // Call settingProperty() on each model. The first model to return a non-undefined value // has performed the set and dictates the return value. The property is considered set // after all models have run. this.models.some(function (model, index) { // Skip initial models that an outer call has already invoked for this node and // property (if any). If an inner call completed for this node and property, skip // the remaining models. if ((!reentered || index > entry.index) && !reentry.completed) { // Record the active model number. reentry.index = index; // Record the number of assignments made since the outermost call. When // `entrants.assignments` increases, a driver has called `setProperty` to make // an assignment elsewhere. var assignments = entrants.assignments; // Make the call. if (!delegated && !assigned) { var value = model[settingPropertyEtc] && model[settingPropertyEtc](nodeID, propertyName, propertyValue); } else { model[settingPropertyEtc] && model[settingPropertyEtc](nodeID, propertyName, undefined); } // Ignore the result if reentry is disabled and the driver attempted to call // back into the kernel. Kernel reentry is disabled during replication to // prevent coloring from accessor scripts. if (this.models.kernel.blocked()) { // TODO: this might be better handled wholly in vwf/kernel/model by converting to a stage and clearing blocked results on the return value = undefined; } // The property was delegated if the call made any assignments. if (entrants.assignments !== assignments) { delegated = true; } // Otherwise if the call returned a value, the property was assigned here. else if (value !== undefined) { entrants.assignments++; assigned = true; } // Record the value actually assigned. This may differ from the incoming value // if it was range limited, quantized, etc. by the model. This is the value // passed to the views. if (value !== undefined) { propertyValue = value; } // If we are setting, exit from the this.models.some() iterator once the value // has been set. Don't exit early if we are creating or initializing since every // model needs the opportunity to register the property. return settingPropertyEtc == "settingProperty" && (delegated || assigned); } }, this); // Record the change if the property was assigned here. if (assigned && node.initialized && node.patchable) { node.properties.change(propertyName); } // Call satProperty() on each view. The view is being notified that a property has // been set. Only call for value properties as they are actually assigned. Don't call // for accessor properties that have delegated to other properties. Notifying when // setting an accessor property would be useful, but since that information is // ephemeral, and views on late-joining clients would never see it, it's best to never // send those notifications. if (assigned) { this.views.forEach(function (view) { view[satPropertyEtc] && view[satPropertyEtc](nodeID, propertyName, propertyValue); }); } if (reentered) { // For a reentrant call, restore the previous state and move the index forward to // cover the models we called. entrants[nodeID + '-' + propertyName] = entry; entry.completed = true; } else { // Delete the call record if this is the first, non-reentrant call here (the normal // case). delete entrants[nodeID + '-' + propertyName]; // If the property was created or initialized, send the corresponding meta event // into the application. if (this.models.kernel.enabled()) { if (settingPropertyEtc === "creatingProperty") { this.fireEvent(nodeID, ["properties", "created"], [propertyName]); } else if (settingPropertyEtc === "initializingProperty") { this.fireEvent(nodeID, ["properties", "initialized"], [propertyName]); } } } // Clear the assignment counter when the outermost `setProperty` completes. if (outermost) { delete entrants.assignments; } this.logger.debugu(); return propertyValue; } // -- getProperty -------------------------------------------------------------------------- /// Get a property value for a node. /// /// @name module:vwf.getProperty /// /// @see {@link module:vwf/api/kernel.getProperty} getProperty(nodeID, propertyName, ignorePrototype) { this.logger.debuggx("getProperty", nodeID, propertyName); var propertyValue = undefined; // Record calls into this function by nodeID and propertyName so that models may call // back here (directly or indirectly) to delegate responses further down the chain // without causing infinite recursion. var entrants = this.getProperty.entrants; var entry = entrants[nodeID + '-' + propertyName] || {}; // the most recent call, if any // TODO: need unique nodeID+propertyName hash var reentry = entrants[nodeID + '-' + propertyName] = {}; // this call // Keep track of the number of retrievals made by this `getProperty` call and others // invoked indirectly by it, starting with the outermost call. var outermost = entrants.retrievals === undefined; if (outermost) { entrants.retrievals = 0; } // Have we been called for the same property on the same node for a property still being // retrieved (such as when a getter function retrieves the property from itself)? If so, // then the inner call should skip drivers that the outer call has already invoked, and // the outer call should complete without invoking drivers that the inner call will have // already called. var reentered = (entry.index !== undefined); // We'll need to know if the get was delegated to other properties or actually retrieved // here. var delegated = false, retrieved = false; // Call gettingProperty() on each model. The first model to return a non-undefined value // dictates the return value. this.models.some(function (model, index) { // Skip initial models that an outer call has already invoked for this node and // property (if any). If an inner call completed for this node and property, skip // the remaining models. if ((!reentered || index > entry.index) && !reentry.completed) { // Record the active model number. reentry.index = index; // Record the number of retrievals made since the outermost call. When // `entrants.retrievals` increases, a driver has called `getProperty` to make // a retrieval elsewhere. var retrievals = entrants.retrievals; // Make the call. var value = model.gettingProperty && model.gettingProperty(nodeID, propertyName, propertyValue); // TODO: probably don't need propertyValue here // Ignore the result if reentry is disabled and the driver attempted to call // back into the kernel. Kernel reentry is disabled during replication to // prevent coloring from accessor scripts. if (this.models.kernel.blocked()) { // TODO: this might be better handled wholly in vwf/kernel/model by converting to a stage and clearing blocked results on the return value = undefined; } // The property was delegated if the call made any retrievals. if (entrants.retrievals !== retrievals) { delegated = true; } // Otherwise if the call returned a value, the property was retrieved here. else if (value !== undefined) { entrants.retrievals++; retrieved = true; } // Record the value retrieved. if (value !== undefined) { propertyValue = value; } // Exit from the this.models.some() iterator once we have a return value. return delegated || retrieved; } }, this); if (reentered) { // For a reentrant call, restore the previous state, move the index forward to cover // the models we called. entrants[nodeID + '-' + propertyName] = entry; entry.completed = true; } else { // Delete the call record if this is the first, non-reentrant call here (the normal // case). delete entrants[nodeID + '-' + propertyName]; // Delegate to the behaviors and prototype if we didn't get a result from the // current node. if (propertyValue === undefined && !ignorePrototype) { this.behaviors(nodeID).reverse().concat(this.prototype(nodeID)). some(function (prototypeID, prototypeIndex, prototypeArray) { if (prototypeIndex < prototypeArray.length - 1) { propertyValue = this.getProperty(prototypeID, propertyName, true); // behavior node only, not its prototypes } else if (prototypeID !== this.kutility.protoNodeURI) { propertyValue = this.getProperty(prototypeID, propertyName); // prototype node, recursively } return propertyValue !== undefined; }, this); } // Call gotProperty() on each view. this.views.forEach(function (view) { view.gotProperty && view.gotProperty(nodeID, propertyName, propertyValue); // TODO: be sure this is the value actually gotten and not an intermediate value from above }); } // Clear the retrieval counter when the outermost `getProperty` completes. if (outermost) { delete entrants.retrievals; } this.logger.debugu(); return propertyValue; } // -- createMethod ------------------------------------------------------------------------- /// @name module:vwf.createMethod /// /// @see {@link module:vwf/api/kernel.createMethod} createMethod(nodeID, methodName, methodParameters, methodBody) { let self = this; this.logger.debuggx("createMethod", function () { return [nodeID, methodName, methodParameters, self.loggableScript(methodBody)]; }); var node = self.nodes.existing[nodeID]; // Register the method. node.methods.create(methodName, node.initialized && node.patchable); // Call `creatingMethod` on each model. The method is considered created after all // models have run. this.models.forEach(function (model) { model.creatingMethod && model.creatingMethod(nodeID, methodName, methodParameters, methodBody); }); // Call `createdMethod` on each view. The view is being notified that a method has been // created. this.views.forEach(function (view) { view.createdMethod && view.createdMethod(nodeID, methodName, methodParameters, methodBody); }); // Send the meta event into the application. if (this.models.kernel.enabled()) { this.fireEvent(nodeID, ["methods", "created"], [methodName]); } this.logger.debugu(); } // -- setMethod ---------------------------------------------------------------------------- /// @name module:vwf.setMethod /// /// @see {@link module:vwf/api/kernel.setMethod} setMethod(nodeID, methodName, methodHandler) { let self = this; this.logger.debuggx("setMethod", function () { return [nodeID, methodName]; // TODO loggable methodHandler }); var node = this.nodes.existing[nodeID]; methodHandler = this.normalizedHandler(methodHandler); if (!node.methods.hasOwn(methodName)) { // If the method doesn't exist on this node, delegate to `kernel.createMethod` to // create and assign the method. this.createMethod(nodeID, methodName, methodHandler.parameters, methodHandler.body); // TODO: result } else { // Call `settingMethod` on each model. The first model to return a non-undefined // value dictates the return value. this.models.some(function (model) { // Call the driver. var handler = model.settingMethod && model.settingMethod(nodeID, methodName, methodHandler); // Update the value to the value assigned if the driver handled it. if (handler !== undefined) { methodHandler = self.utility.merge({}, handler); // omit `undefined` values } // Exit the iterator once a driver has handled the assignment. return handler !== undefined; }); // Record the change. if (node.initialized && node.patchable) { node.methods.change(methodName); } // Call `satMethod` on each view. this.views.forEach(function (view) { view.satMethod && view.satMethod(nodeID, methodName, methodHandler); }); } this.logger.debugu(); return methodHandler; } // -- getMethod ---------------------------------------------------------------------------- /// @name module:vwf.getMethod /// /// @see {@link module:vwf/api/kernel.getMethod} getMethod(nodeID, methodName) { let self = this; this.logger.debuggx("getMethod", function () { return [nodeID, methodName]; }); var node = this.nodes.existing[nodeID]; // Call `gettingMethod` on each model. The first model to return a non-undefined value // dictates the return value. var methodHandler = {}; this.models.some(function (model) { // Call the driver. var handler = model.gettingMethod && model.gettingMethod(nodeID, methodName); // Update the value to the value assigned if the driver handled it. if (handler !== undefined) { methodHandler = self.utility.merge({}, handler); // omit `undefined` values } // Exit the iterator once a driver has handled the retrieval. return handler !== undefined; }); // Call `gotMethod` on each view. this.views.forEach(function (view) { view.gotMethod && view.gotMethod(nodeID, methodName, methodHandler); }); this.logger.debugu(); return methodHandler; } // -- callMethod --------------------------------------------------------------------------- /// @name module:vwf.callMethod /// /// @see {@link module:vwf/api/kernel.callMethod} callMethod(nodeID, methodName, methodParameters) { let self = this; this.logger.debuggx("callMethod", function () { return [nodeID, methodName, JSON.stringify(self.loggableValues(methodParameters))]; }); // Call callingMethod() on each model. The first model to return a non-undefined value // dictates the return value. var methodValue = undefined; this.models.some(function (model) { methodValue = model.callingMethod && model.callingMethod(nodeID, methodName, methodParameters); return methodValue !== undefined; }); // Call calledMethod() on each view. this.views.forEach(function (view) { view.calledMethod && view.calledMethod(nodeID, methodName, methodParameters, methodValue); }); this.logger.debugu(); return methodValue; } // -- createEvent -------------------------------------------------------------------------- /// @name module:vwf.createEvent /// /// @see {@link module:vwf/api/kernel.createEvent} createEvent(nodeID, eventName, eventParameters) { // TODO: parameters (used? or just for annotation?) // TODO: allow a handler body here and treat as this.*event* = function() {} (a self-targeted handler); will help with ui event handlers this.logger.debuggx("createEvent", nodeID, eventName, eventParameters); var node = this.nodes.existing[nodeID]; // Encode any namespacing into the name. (Namespaced names were added in 0.6.21.) var encodedEventName = this.namespaceEncodedName(eventName); // Register the event. node.events.create(encodedEventName, node.initialized && node.patchable, eventParameters); // Call `creatingEvent` on each model. The event is considered created after all models // have run. this.models.forEach(function (model) { model.creatingEvent && model.creatingEvent(nodeID, encodedEventName, eventParameters); }); // Call `createdEvent` on each view. The view is being notified that a event has been // created. this.views.forEach(function (view) { view.createdEvent && view.createdEvent(nodeID, encodedEventName, eventParameters); }); // Send the meta event into the application. if (this.models.kernel.enabled()) { this.fireEvent(nodeID, ["events", "created"], [eventName]); } this.logger.debugu(); } // -- setEvent ----------------------------------------------------------------------------- /// @name module:vwf.setEvent /// /// @see {@link module:vwf/api/kernel.setEvent} setEvent(nodeID, eventName, eventDescriptor) { this.logger.debuggx("setEvent", function () { return [nodeID, eventName]; // TODO: loggable eventDescriptor }); var node = this.nodes.existing[nodeID]; // eventDescriptor = normalizedHandler( eventDescriptor ); // TODO // Encode any namespacing into the name. var encodedEventName = this.namespaceEncodedName(eventName); if (!node.events.hasOwn(encodedEventName)) { // If the event doesn't exist on this node, delegate to `kernel.createEvent` to // create and assign the event. this.createEvent(nodeID, eventName, eventDescriptor.parameters); // TODO: result (eventDescriptor.listeners || []).forEach(function (listener) { return this.addEventListener(nodeID, eventName, listener, listener.context, listener.phases); }, this); } else { // Locate the event in the registry. var event = node.events.existing[encodedEventName]; // xxx eventDescriptor = { parameters: event.parameters ? event.parameters.slice() : [], // TODO: note: we're ignoring eventDescriptor.parameters in the set listeners: (eventDescriptor.listeners || []).map(function (listener) { if (event.listeners.hasOwn(listener.id)) { return this.setEventListener(nodeID, eventName, listener.id, listener); } else { return this.addEventListener(nodeID, eventName, listener, listener.context, listener.phases); } }, this), }; } this.logger.debugu(); return eventDescriptor; } // -- getEvent ----------------------------------------------------------------------------- /// @name module:vwf.getEvent /// /// @see {@link module:vwf/api/kernel.getEvent} getEvent(nodeID, eventName) { this.logger.debuggx("getEvent", function () { return [nodeID, eventName]; }); var node = this.nodes.existing[nodeID]; // Encode any namespacing into the name. var encodedEventName = this.namespaceEncodedName(eventName); // Locate the event in the registry. var event = node.events.existing[encodedEventName]; // Build the result descriptor. Omit the `parameters` and `listeners` fields when the // parameters or listeners are missing or empty, respectively. var eventDescriptor = {}; if (event.parameters) { eventDescriptor.parameters = event.parameters.slice(); } if (event.listeners.existing.length) { eventDescriptor.listeners = event.listeners.existing.map(function (eventListenerID) { var listener = this.getEventListener(nodeID, eventName, eventListenerID); listener.id = eventListenerID; return listener; }, this); } this.logger.debugu(); return eventDescriptor; } // -- addEventListener --------------------------------------------------------------------- /// @name module:vwf.addEventListener /// /// @see {@link module:vwf/api/kernel.addEventListener} addEventListener(nodeID, eventName, eventHandler, eventContextID, eventPhases) { let self = this; this.logger.debuggx("addEventListener", function () { return [nodeID, eventName, self.loggableScript(eventHandler), eventContextID, eventPhases ]; }); var node = self.nodes.existing[nodeID]; // Encode any namespacing into the name. var encodedEventName = this.namespaceEncodedName(eventName); // Register the event if this is the first listener added to an event on a prototype. if (!node.events.hasOwn(encodedEventName)) { node.events.create(encodedEventName, node.initialized && node.patchable); } // Locate the event in the registry. var event = node.events.existing[encodedEventName]; // Normalize the descriptor. eventHandler = this.normalizedHandler(eventHandler, event.parameters); // Register the listener. var eventListenerID = eventHandler.id || this.sequence(nodeID); event.listeners.create(eventListenerID, node.initialized && node.patchable); // Call `addingEventListener` on each model. this.models.forEach(function (model) { model.addingEventListener && model.addingEventListener(nodeID, encodedEventName, eventListenerID, eventHandler, eventContextID, eventPhases); }); // Call `addedEventListener` on each view. this.views.forEach(function (view) { view.addedEventListener && view.addedEventListener(nodeID, encodedEventName, eventListenerID, eventHandler, eventContextID, eventPhases); }); this.logger.debugu(); return eventListenerID; } // -- removeEventListener ------------------------------------------------------------------ /// @name module:vwf.removeEventListener /// /// @see {@link module:vwf/api/kernel.removeEventListener} removeEventListener(nodeID, eventName, eventListenerID) { let self = this; this.logger.debuggx("removeEventListener", function () { return [nodeID, eventName, self.loggableScript(eventListenerID)]; }); var node = this.nodes.existing[nodeID]; // Encode any namespacing into the name. var encodedEventName = this.namespaceEncodedName(eventName); // Locate the event in the registry. var event = node.events.existing[encodedEventName]; // Unregister the listener. event.listeners.delete(eventListenerID, node.initialized && node.patchable); // Call `removingEventListener` on each model. this.models.forEach(function (model) { model.removingEventListener && model.removingEventListener(nodeID, encodedEventName, eventListenerID); }); // Call `removedEventListener` on each view. this.views.forEach(function (view) { view.removedEventListener && view.removedEventListener(nodeID, encodedEventName, eventListenerID); }); this.logger.debugu(); return eventListenerID; } // -- setEventListener --------------------------------------------------------------------- /// @name module:vwf.setEventListener /// /// @see {@link module:vwf/api/kernel.setEventListener} setEventListener(nodeID, eventName, eventListenerID, eventListener) { let self = this; this.logger.debuggx("setEventListener", function () { return [nodeID, eventName, eventListenerID]; // TODO: loggable eventListener }); var node = this.nodes.existing[nodeID]; // Encode any namespacing into the name. var encodedEventName = this.namespaceEncodedName(eventName); // Locate the event in the registry. var event = node.events.existing[encodedEventName]; // Normalize the descriptor. eventListener = this.normalizedHandler(eventListener, event.parameters); // Record the change. if (node.initialized && node.patchable) { event.listeners.change(eventListenerID); } // Call `settingEventListener` on each model. The first model to return a non-undefined // value dictates the return value. this.models.some(function (model) { // Call the driver. var listener = model.settingEventListener && model.settingEventListener(nodeID, encodedEventName, eventListenerID, eventListener); // Update the value to the value assigned if the driver handled it. if (listener !== undefined) { eventListener = self.utility.merge({}, listener); // omit `undefined` values } // Exit the iterator once a driver has handled the assignment. return listener !== undefined; }); // Call `satEventListener` on each view. this.views.forEach(function (view) { view.satEventListener && view.satEventListener(nodeID, encodedEventName, eventListenerID, eventListener); }); this.logger.debugu(); return eventListener; } // -- getEventListener --------------------------------------------------------------------- /// @name module:vwf.getEventListener /// /// @see {@link module:vwf/api/kernel.getEventListener} getEventListener(nodeID, eventName, eventListenerID) { let self = this; this.logger.debuggx("getEventListener", function () { return [nodeID, eventName, eventListenerID]; }); // Encode any namespacing into the name. var encodedEventName = this.namespaceEncodedName(eventName); // Call `gettingEventListener` on each model. The first model to return a non-undefined // value dictates the return value. var eventListener = {}; this.models.some(function (model) { // Call the driver. var listener = model.gettingEventListener && model.gettingEventListener(nodeID, encodedEventName, eventListenerID); // Update the value to the value assigned if the driver handled it. if (listener !== undefined) { eventListener = self.utility.merge({}, listener); // omit `undefined` values } // Exit the iterator once a driver has handled the assignment. return listener !== undefined; }); // Call `gotEventListener` on each view. this.views.forEach(function (view) { view.gotEventListener && view.gotEventListener(nodeID, encodedEventName, eventListenerID, eventListener); }); this.logger.debugu(); return eventListener; } // -- flushEventListeners ------------------------------------------------------------------ /// @name module:vwf.flushEventListeners /// /// @see {@link module:vwf/api/kernel.flushEventListeners} flushEventListeners(nodeID, eventName, eventContextID) { this.logger.debuggx("flushEventListeners", nodeID, eventName, eventContextID); // Encode any namespacing into the name. var encodedEventName = this.namespaceEncodedName(eventName); // Retrieve the event in case we need to remove listeners from it var node = this.nodes.existing[nodeID]; var event = node.events.existing[encodedEventName]; // Call `flushingEventListeners` on each model. this.models.forEach(function (model) { var removedIds = model.flushingEventListeners && model.flushingEventListeners(nodeID, encodedEventName, eventContextID); // If the model driver returned an array of the ids of event listeners that it // removed, remove their references in the kernel if (removedIds && removedIds.length) { // Unregister the listeners that were flushed removedIds.forEach(function (removedId) { event.listeners.delete(removedId, node.initialized && node.patchable); }); } }); // Call `flushedEventListeners` on each view. this.views.forEach(function (view) { view.flushedEventListeners && view.flushedEventListeners(nodeID, encodedEventName, eventContextID); }); // TODO: `flushEventListeners` can be interpreted by the kernel now and handled with a // `removeEventListener` call instead of separate `flushingEventListeners` and // `flushedEventListeners` calls to the drivers. this.logger.debugu(); } // -- fireEvent ---------------------------------------------------------------------------- /// @name module:vwf.fireEvent /// /// @see {@link module:vwf/api/kernel.fireEvent} fireEvent(nodeID, eventName, eventParameters) { let self = this; this.logger.debuggx("fireEvent", function () { return [nodeID, eventName, JSON.stringify(self.loggableValues(eventParameters))]; }); // Encode any namespacing into the name. (Namespaced names were added in 0.6.21.) var encodedEventName = this.namespaceEncodedName(eventName); // Call `firingEvent` on each model. var handled = this.models.reduce(function (handled, model) { return model.firingEvent && model.firingEvent(nodeID, encodedEventName, eventParameters) || handled; }, false); // Call `firedEvent` on each view. this.views.forEach(function (view) { view.firedEvent && view.firedEvent(nodeID, encodedEventName, eventParameters); }); this.logger.debugu(); // TODO: `fireEvent` needs to tell the drivers to invoke each listener individually // now that listeners can be spread across multiple drivers. return handled; } // -- dispatchEvent ------------------------------------------------------------------------ /// Dispatch an event toward a node. Using fireEvent(), capture (down) and bubble (up) along /// the path from the global root to the node. Cancel when one of the handlers returns a /// truthy value to indicate that it has handled the event. /// /// @name module:vwf.dispatchEvent /// /// @see {@link module:vwf/api/kernel.dispatchEvent} dispatchEvent(nodeID, eventName, eventParameters, eventNodeParameters) { let self = this; this.logger.debuggx("dispatchEvent", function () { return [nodeID, eventName, JSON.stringify(self.loggableValues(eventParameters)), JSON.stringify(self.loggableIndexedValues(eventNodeParameters)) ]; }); // Defaults for the parameters to send with the events. Values from `eventParameters` // are sent to each node. `eventNodeParameters` contains additional values to send to // specific nodes. eventParameters = eventParameters || []; eventNodeParameters = eventNodeParameters || {}; // Find the inheritance path from the node to the root. var ancestorIDs = this.ancestors(nodeID); var lastAncestorID = ""; // Make space to record the parameters sent to each node. Parameters provided for upper // nodes cascade down until another definition is found for a lower node. We'll remember // these on the way down and replay them on the way back up. var cascadedEventNodeParameters = { "": eventNodeParameters[""] || [] // defaults come from the "" key in eventNodeParameters }; // Parameters passed to the handlers are the concatention of the eventParameters array, // the eventNodeParameters for the node (cascaded), and the phase. var targetEventParameters = undefined; var phase = undefined; var handled = false; // Capturing phase. phase = "capture"; // only handlers tagged "capture" will be invoked handled = handled || ancestorIDs.reverse().some(function (ancestorID) { // TODO: reverse updates the array in place every time and we'd rather not cascadedEventNodeParameters[ancestorID] = eventNodeParameters[ancestorID] || cascadedEventNodeParameters[lastAncestorID]; lastAncestorID = ancestorID; targetEventParameters = eventParameters.concat(cascadedEventNodeParameters[ancestorID], phase); targetEventParameters.phase = phase; // smuggle the phase across on the parameters array // TODO: add "phase" as a fireEvent() parameter? it isn't currently needed in the kernel public API (not queueable, not called by the drivers), so avoid if possible return this.fireEvent(ancestorID, eventName, targetEventParameters); }, this); // At target. phase = undefined; // invoke all handlers cascadedEventNodeParameters[nodeID] = eventNodeParameters[nodeID] || cascadedEventNodeParameters[lastAncestorID]; targetEventParameters = eventParameters.concat(cascadedEventNodeParameters[nodeID], phase); handled = handled || this.fireEvent(nodeID, eventName, targetEventParameters); // Bubbling phase. phase = undefined; // invoke all handlers handled = handled || ancestorIDs.reverse().some(function (ancestorID) { // TODO: reverse updates the array in place every time and we'd rather not targetEventParameters = eventParameters.concat(cascadedEventNodeParameters[ancestorID], phase); return this.fireEvent(ancestorID, eventName, targetEventParameters); }, this); this.logger.debugu(); } // -- execute ------------------------------------------------------------------------------ /// @name module:vwf.execute /// /// @see {@link module:vwf/api/kernel.execute} execute(nodeID, scriptText, scriptType, callback_async /* result */ ) { let self = this; this.logger.debuggx("execute", function () { return [nodeID, self.loggableScript(scriptText), scriptType]; }); // Assume JavaScript if the type is not specified and the text is a string. if (!scriptType && (typeof scriptText == "string" || scriptText instanceof String)) { scriptType = "application/javascript"; } // Call executing() on each model. The script is considered executed after all models // have run. var scriptValue = undefined; // Watch for any async kernel calls generated as we execute the scriptText and wait for // them to complete before calling the callback. self.models.kernel.capturingAsyncs(function () { self.models.some(function (model) { scriptValue = model.executing && model.executing(nodeID, scriptText, scriptType); return scriptValue !== undefined; }); // Call executed() on each view to notify view that a script has been executed. self.views.forEach(function (view) { view.executed && view.executed(nodeID, scriptText, scriptType); }); }, function () { callback_async && callback_async(scriptValue); }); this.logger.debugu(); return scriptValue; } // -- random ------------------------------------------------------------------------------- /// @name module:vwf.random /// /// @see {@link module:vwf/api/kernel.random} random(nodeID) { return this.models.object.random(nodeID); } // -- seed --------------------------------------------------------------------------------- /// @name module:vwf.seed /// /// @see {@link module:vwf/api/kernel.seed} seed(nodeID, seed) { return this.models.object.seed(nodeID, seed); } // -- time --------------------------------------------------------------------------------- /// The current simulation time. /// /// @name module:vwf.time /// /// @see {@link module:vwf/api/kernel.time} now() { return this.virtualTime.now; } time() { return this.virtualTime.now; } // -- client ------------------------------------------------------------------------------- /// The moniker of the client responsible for the current action. Will be falsy for actions /// originating in the server, such as time ticks. /// /// @name module:vwf.client /// /// @see {@link module:vwf/api/kernel.client} client() { return this.virtualTime.client_; } // -- moniker ------------------------------------------------------------------------------ /// The identifer the server assigned to this client. /// /// @name module:vwf.moniker /// /// @see {@link module:vwf/api/kernel.moniker} moniker() { return this.moniker_; } // -- application -------------------------------------------------------------------------- /// @name module:vwf.application /// /// @see {@link module:vwf/api/kernel.application} application(initializedOnly) { let self = this; var applicationID; Object.keys(self.nodes.globals).forEach(function (globalID) { var global = self.nodes.existing[globalID]; if ((!initializedOnly || global.initialized) && global.name === "application") { applicationID = globalID; } }, this); return applicationID; } // -- intrinsics --------------------------------------------------------------------------- /// @name module:vwf.intrinsics /// /// @see {@link module:vwf/api/kernel.intrinsics} intrinsics(nodeID, result) { return this.models.object.intrinsics(nodeID, result); } // -- uri ---------------------------------------------------------------------------------- /// @name module:vwf.uri /// /// @see {@link module:vwf/api/kernel.uri} uri(nodeID, searchAncestors, initializedOnly) { var uri = this.models.object.uri(nodeID); if (searchAncestors) { while (!uri && (nodeID = this.parent(nodeID, initializedOnly))) { uri = this.models.object.uri(nodeID); } } return uri; } // -- name --------------------------------------------------------------------------------- /// @name module:vwf.name /// /// @see {@link module:vwf/api/kernel.name} name(nodeID) { return this.models.object.name(nodeID); } // -- prototype ---------------------------------------------------------------------------- /// @name module:vwf.prototype /// /// @see {@link module:vwf/api/kernel.prototype} prototype(nodeID) { return this.models.object.prototype(nodeID); } // -- prototypes --------------------------------------------------------------------------- /// @name module:vwf.prototypes /// /// @see {@link module:vwf/api/kernel.prototypes} prototypes(nodeID, includeBehaviors) { var prototypes = []; do { // Add the current node's behaviors. if (includeBehaviors) { var b = [].concat(this.behaviors(nodeID)); Array.prototype.push.apply(prototypes, b.reverse()); } // Get the next prototype. nodeID = this.prototype(nodeID); // Add the prototype. if (nodeID) { prototypes.push(nodeID); } } while (nodeID); return prototypes; } // -- behaviors ---------------------------------------------------------------------------- /// @name module:vwf.behaviors /// /// @see {@link module:vwf/api/kernel.behaviors} behaviors(nodeID) { return this.models.object.behaviors(nodeID); } // -- globals ------------------------------------------------------------------------------ /// @name module:vwf.globals /// /// @see {@link module:vwf/api/kernel.globals} globals(initializedOnly) { let self = this; var globals = {}; Object.keys(self.nodes.globals).forEach(function (globalID) { if (!initializedOnly || self.nodes.existing[globalID].initialized) { globals[globalID] = undefined; } }, this); return globals; } // -- global ------------------------------------------------------------------------------- /// @name module:vwf.global /// /// @see {@link module:vwf/api/kernel.global} global(globalReference, initializedOnly) { let self = this; var globals = this.globals(initializedOnly); // Look for a global node whose URI matches `globalReference`. If there is no match by // URI, then search again by name. return matches("uri") || matches("name"); // Look for a global node where the field named by `field` matches `globalReference`. function matches(field) { var matchingID; Object.keys(globals).some(function (globalID) { if (self.nodes.existing[globalID][field] === globalReference) { matchingID = globalID; return true; } }); return matchingID; } } // -- root --------------------------------------------------------------------------------- /// @name module:vwf.root /// /// @see {@link module:vwf/api/kernel.root} root(nodeID, initializedOnly) { var rootID; // Walk the ancestors to the top of the tree. Stop when we reach the pseudo-node at the // global root, which unlike all other nodes has a falsy ID, or `undefined` if we could // not reach the top because `initializedOnly` is set and we attempted to cross between // nodes that have and have not completed initialization. do { rootID = nodeID; nodeID = this.parent(nodeID, initializedOnly); } while (nodeID); // Return the root ID, or `undefined` when `initializedOnly` is set and the node can't // see the root. return nodeID === undefined ? undefined : rootID; } // -- ancestors ---------------------------------------------------------------------------- /// @name module:vwf.ancestors /// /// @see {@link module:vwf/api/kernel.ancestors} ancestors(nodeID, initializedOnly) { var ancestors = []; nodeID = this.parent(nodeID, initializedOnly); while (nodeID) { ancestors.push(nodeID); nodeID = this.parent(nodeID, initializedOnly); } return ancestors; } // -- parent ------------------------------------------------------------------------------- /// @name module:vwf.parent /// /// @see {@link module:vwf/api/kernel.parent} parent(nodeID, initializedOnly) { return this.models.object.parent(nodeID, initializedOnly); } // -- children ----------------------------------------------------------------------------- /// @name module:vwf.children /// /// @see {@link module:vwf/api/kernel.children} children(nodeID, initializedOnly) { if (nodeID === undefined) { this.logger.errorx("children", "cannot retrieve children of nonexistent node"); return; } return this.models.object.children(nodeID, initializedOnly); } // -- child -------------------------------------------------------------------------------- /// @name module:vwf.child /// /// @see {@link module:vwf/api/kernel.child} child(nodeID, childReference, initializedOnly) { var children = this.children(nodeID, initializedOnly); if (typeof childReference === "number" || childReference instanceof Number) { return children[childReference]; } else { return children.filter(function (childID) { return childID && this.name(childID) === childReference; }, this)[0]; } } // -- descendants -------------------------------------------------------------------------- /// @name module:vwf.descendants /// /// @see {@link module:vwf/api/kernel.descendants} descendants(nodeID, initializedOnly) { if (nodeID === undefined) { this.logger.errorx("descendants", "cannot retrieve children of nonexistent node"); return; } var descendants = []; this.children(nodeID, initializedOnly).forEach(function (childID) { descendants.push(childID); childID && Array.prototype.push.apply(descendants, this.descendants(childID, initializedOnly)); }, this); return descendants; } // -- sequence ----------------------------------------------------------------------------- /// @name module:vwf.sequence /// /// @see {@link module:vwf/api/kernel.sequence} sequence(nodeID) { return this.models.object.sequence(nodeID); } /// Locate nodes matching a search pattern. See vwf.api.kernel#find for details. /// /// @name module:vwf.find /// /// @param {ID} nodeID /// The reference node. Relative patterns are resolved with respect to this node. `nodeID` /// is ignored for absolute patterns. /// @param {String} matchPattern /// The search pattern. /// @param {Boolean} [initializedOnly] /// Interpret nodes that haven't completed initialization as though they don't have /// ancestors. Drivers that manage application code should set `initializedOnly` since /// applications should never have access to uninitialized parts of the application graph. /// @param {Function} [callback] /// A callback to receive the search results. If callback is provided, find invokes /// callback( matchID ) for each match. Otherwise the result is returned as an array. /// /// @returns {ID[]|undefined} /// If callback is provided, undefined; otherwise an array of the node ids of the result. /// /// @see {@link module:vwf/api/kernel.find} find(nodeID, matchPattern, initializedOnly, callback /* ( matchID ) */ ) { // Interpret `find( nodeID, matchPattern, callback )` as // `find( nodeID, matchPattern, undefined, callback )`. (`initializedOnly` was added in // 0.6.8.) if (typeof initializedOnly == "function" || initializedOnly instanceof Function) { callback = initializedOnly; initializedOnly = undefined; } // Run the query. var matchIDs = find.call(this, nodeID, matchPattern, initializedOnly); // Return the result. Invoke the callback if one was provided. Otherwise, return the // array directly. if (callback) { matchIDs.forEach(function (matchID) { callback(matchID); }); } else { // TODO: future iterator proxy return matchIDs; } } // -- findClients ------------------------------------------------------------------------------ /// Locate client nodes matching a search pattern. /// /// @name module:vwf.findClients /// /// @param {ID} nodeID /// The reference node. Relative patterns are resolved with respect to this node. `nodeID` /// is ignored for absolute patterns. /// @param {String} matchPattern /// The search pattern. /// @param {Function} [callback] /// A callback to receive the search results. If callback is provided, find invokes /// callback( matchID ) for each match. Otherwise the result is returned as an array. /// /// @returns {ID[]|undefined} /// If callback is provided, undefined; otherwise an array of the node ids of the result. /// /// @deprecated in version 0.6.21. Instead of `kernel.findClients( reference, "/pattern" )`, /// use `kernel.find( reference, "doc('http://vwf.example.com/clients.vwf')/pattern" )`. /// /// @see {@link module:vwf/api/kernel.findClients} findClients(nodeID, matchPattern, callback /* ( matchID ) */ ) { this.logger.warn("`kernel.findClients` is deprecated. Use " + "`kernel.find( nodeID, \"doc('proxy/clients.vwf')/pattern\" )`" + " instead."); var clientsMatchPattern = "doc('proxy/clients.vwf')" + (matchPattern[0] === "/" ? "" : "/") + matchPattern; return this.find(nodeID || this.application(), clientsMatchPattern, callback); } /// Test a node against a search pattern. See vwf.api.kernel#test for details. /// /// @name module:vwf.test /// /// @param {ID} nodeID /// The reference node. Relative patterns are resolved with respect to this node. `nodeID` /// is ignored for absolute patterns. /// @param {String} matchPattern /// The search pattern. /// @param {ID} testID /// A node to test against the pattern. /// @param {Boolean} [initializedOnly] /// Interpret nodes that haven't completed initialization as though they don't have /// ancestors. Drivers that manage application code should set `initializedOnly` since /// applications should never have access to uninitialized parts of the application graph. /// /// @returns {Boolean} /// true when testID matches the pattern. /// /// @see {@link module:vwf/api/kernel.test} test(nodeID, matchPattern, testID, initializedOnly) { // Run the query. var matchIDs = this.find.call(this, nodeID, matchPattern, initializedOnly); // Search for the test node in the result. return matchIDs.some(function (matchID) { return matchID == testID; }); } // == Private functions ==================================================================== // -- loadComponent ------------------------------------------------------------------------ /// @name module:vwf~loadComponent loadComponent(nodeURI, baseURI, callback_async /* nodeDescriptor */ , errback_async /* errorMessage */ ) { // TODO: turn this into a generic xhr loader exposed as a kernel function? let self = this; if (nodeURI == self.kutility.protoNodeURI) { callback_async(self.kutility.protoNodeDescriptor); } else if (nodeURI.match(RegExp("^data:application/json;base64,"))) { // Primarly for testing, parse one specific form of data URIs. We need to parse // these ourselves since Chrome can't load data URIs due to cross origin // restrictions. callback_async(JSON.parse(atob(nodeURI.substring(29)))); // TODO: support all data URIs } else { self.virtualTime.suspend("while loading " + nodeURI); // suspend the queue let fetchUrl = self.remappedURI(nodeURI, baseURI); let dbName = fetchUrl.split(".").join("_"); const parseComp = function (f) { let result = JSON.parse(f); let nativeObject = result; // console.log(nativeObject); if (nativeObject) { callback_async(nativeObject); self.virtualTime.resume("after loading " + nodeURI); // resume the queue; may invoke dispatch(), so call last before returning to the host } else { self.logger.warnx("loadComponent", "error loading", nodeURI + ":", error); errback_async(error); self.virtualTime.resume("after loading " + nodeURI); // resume the queue; may invoke dispatch(), so call last before returning to the host } } //var fileName = ""; // let userDB = _LCSDB.user(_LCS_WORLD_USER.pub); if (dbName.includes("proxy")) { //userDB = await window._LCS_SYS_USER.get('proxy').then(); let proxyDB = self.proxy ? _LCSDB.user(self.proxy) : _LCSDB.user(_LCS_WORLD_USER.pub); let fileName = dbName + '_json'; let dbNode = proxyDB.get('proxy').get(fileName); let nodeProm = new Promise(res => dbNode.once(res)); nodeProm.then(comp => { parseComp(comp); }) // (function(r){ // //console.log(r); // parseComp(r); // }); } else { let worldName = dbName.split('/')[1]; let fileName = dbName.split('/')[2] + '_json'; let dbNode = _LCSDB.user(_LCS_WORLD_USER.pub).get('worlds').path(worldName).get(fileName); let nodeProm = new Promise(res => dbNode.once(res)) nodeProm.then(comp => { parseComp(comp); }) // (function(r){ // //console.log(r); // parseComp(r); // }); } //console.log(source); //userDB.get(fileName).once(async function(res) { // }, {wait: 1000}) } } // -- loadScript --------------------------------------------------------------------------- /// @name module:vwf~loadScript loadScript(scriptURI, baseURI, callback_async /* scriptText */ , errback_async /* errorMessage */ ) { let self = this; if (scriptURI.match(RegExp("^data:application/javascript;base64,"))) { // Primarly for testing, parse one specific form of data URIs. We need to parse // these ourselves since Chrome can't load data URIs due to cross origin // restrictions. callback_async(atob(scriptURI.substring(35))); // TODO: support all data URIs } else { self.virtualTime.suspend("while loading " + scriptURI); // suspend the queue let fetchUrl = self.remappedURI(scriptURI, baseURI); let dbName = fetchUrl.split(".").join("_") const parseComp = function (res) { let scriptText = res; try { callback_async(scriptText); self.virtualTime.resume("after loading " + scriptURI); // resume the queue; may invoke dispatch(), so call last before returning to the host } catch (e) { self.logger.warnx("loadScript", "error loading", scriptURI + ":", error); errback_async(error); self.virtualTime.resume("after loading " + scriptURI); // resume the queue; may invoke dispatch(), so call last before returning to the host } } let worldName = dbName.split('/')[1]; //let userDB = _LCSDB.user(_LCS_WORLD_USER.pub); if (dbName.includes("proxy")) { let proxyDB = self.proxy ? _LCSDB.user(self.proxy) : _LCSDB.user(_LCS_WORLD_USER.pub); //userDB = window._LCS_SYS_USER.get('proxy'); let fileName = dbName; let dbNode = proxyDB.get('proxy').get(fileName); let nodeProm = new Promise(res => dbNode.once(res)) nodeProm.then(comp => { parseComp(comp); }) // window._LCS_SYS_USER.get('proxy').get(fileName).get('file').once(function(r){ // //console.log(r); // parseComp(r); // }); } else { let fileName = dbName.split('/')[2]; //dbName.replace(worldName + '/', ""); let dbNode = _LCSDB.user(_LCS_WORLD_USER.pub).get('worlds').path(worldName).get(fileName); let nodeProm = new Promise(res => dbNode.once(res)) nodeProm.then(comp => { parseComp(comp); }) // userDB.get('worlds').path(worldName).get(fileName).get('file').once(function(r){ // //console.log(r); // parseComp(r); // }); } // userDB.get(fileName).once(, {wait: 1000}) } } /// Determine if a given property of a node has a setter function, either directly on the /// node or inherited from a prototype. /// /// This function must run as a method of the kernel. Invoke as: nodePropertyHasSetter.call( /// kernel, nodeID, propertyName ). /// /// @name module:vwf~nodePropertyHasSetter /// /// @param {ID} nodeID /// @param {String} propertyName /// /// @returns {Boolean} nodePropertyHasSetter(nodeID, propertyName) { // invoke with the kernel as "this" // TODO: this is peeking inside of vwf-model-javascript; need to delegate to all script drivers var node = this.models.javascript.nodes[nodeID]; var setter = node.private.setters && node.private.setters[propertyName]; return typeof setter == "function" || setter instanceof Function; } /// Determine if a given property of a node has a setter function. The node's prototypes are /// not considered. /// /// This function must run as a method of the kernel. Invoke as: /// nodePropertyHasOwnSetter.call( kernel, nodeID, propertyName ). /// /// @name module:vwf~nodePropertyHasOwnSetter /// /// @param {ID} nodeID /// @param {String} propertyName /// /// @returns {Boolean} nodePropertyHasOwnSetter(nodeID, propertyName) { // invoke with the kernel as "this" // TODO: this is peeking inside of vwf-model-javascript; need to delegate to all script drivers var node = this.models.javascript.nodes[nodeID]; var setter = node.private.setters && node.private.setters.hasOwnProperty(propertyName) && node.private.setters[propertyName]; return typeof setter == "function" || setter instanceof Function; } /// Determine if a node has a child with the given name, either directly on the node or /// inherited from a prototype. /// /// This function must run as a method of the kernel. Invoke as: nodeHasChild.call( /// kernel, nodeID, childName ). /// /// @name module:vwf~nodeHasChild /// /// @param {ID} nodeID /// @param {String} childName /// /// @returns {Boolean} nodeHasChild(nodeID, childName) { // invoke with the kernel as "this" // TODO: this is peeking inside of vwf-model-javascript var node = this.models.javascript.nodes[nodeID]; return childName in node.children; } /// Determine if a node has a child with the given name. The node's prototypes are not /// considered. /// /// This function must run as a method of the kernel. Invoke as: nodeHasOwnChild.call( /// kernel, nodeID, childName ). /// /// @name module:vwf~nodeHasOwnChild /// /// @param {ID} nodeID /// @param {String} childName /// /// @returns {Boolean} nodeHasOwnChild(nodeID, childName) { // invoke with the kernel as "this" // TODO: this is peeking inside of vwf-model-javascript var node = this.models.javascript.nodes[nodeID]; var hasChild = false; if (parseInt(childName).toString() !== childName) { hasChild = node.children.hasOwnProperty(childName); // TODO: this is peeking inside of vwf-model-javascript } else { // Children with numeric names do not get added as properties of the children array, so loop over the children // to check manually for (var i = 0, il = node.children.length; i < il; i++) { if (childName === node.children[i].name) { hasChild = true; } } } return hasChild; } /// Determine if a component specifier is a URI. /// /// A component may be specified as the URI of a resource containing a descriptor (string), /// a descriptor (object), or the ID of a previously-created node (primitive). /// /// @name module:vwf~componentIsURI /// /// @param {String|Object} candidate /// /// @returns {Boolean} componentIsURI(candidate) { return (typeof candidate == "string" || candidate instanceof String) && !this.componentIsID(candidate); } /// Determine if a component specifier is a descriptor. /// /// A component may be specified as the URI of a resource containing a descriptor (string), /// a descriptor (object), or the ID of a previously-created node (primitive). /// /// @name module:vwf~componentIsDescriptor /// /// @param {String|Object} candidate /// /// @returns {Boolean} componentIsDescriptor(candidate) { return typeof candidate == "object" && candidate != null && !this.isPrimitive(candidate); } /// Determine if a component specifier is an ID. /// /// A component may be specified as the URI of a resource containing a descriptor (string), /// a descriptor (object), or the ID of a previously-created node (primitive). /// /// @name module:vwf~componentIsID /// /// @param {String|Object} candidate /// /// @returns {Boolean} componentIsID(candidate) { return this.isPrimitive(candidate) && this.models.object.exists(candidate) && !(this.components[candidate] instanceof Array); } /// Determine if a value is a JavaScript primitive, or the boxed version of a JavaScript /// primitive. /// /// Node IDs are JavaScript primitives. This function may be used to determine if a value /// has the correct type to be a node ID. /// /// @name module:vwf~isPrimitive /// /// @param candidate /// /// @returns {Boolean} isPrimitive(candidate) { switch (typeof candidate) { case "string": case "number": case "boolean": return true; case "object": return candidate instanceof String || candidate instanceof Number || candidate instanceof Boolean; default: return false; } } /// Determine if an object is a component descriptor. Detect the type by searching for /// descriptor keys in the candidate object. /// /// @name module:vwf~objectIsComponent /// /// @param {Object} candidate /// /// @returns {Boolean} objectIsComponent(candidate) { var componentAttributes = [ "extends", "implements", "source", "type", "properties", "methods", "events", "children", "scripts", ]; var isComponent = false; if (typeof candidate == "object" && candidate != null) { isComponent = componentAttributes.some(function (attributeName) { return candidate.hasOwnProperty(attributeName); }); } return isComponent; } /// Determine if a property initializer is a detailed initializer containing explicit /// accessor and value parameters (rather than a simple value specification). Detect the /// type by searching for property initializer keys in the candidate object. /// /// @name module:vwf~valueHasAccessors /// /// @param {Object} candidate /// /// @returns {Boolean} valueHasAccessors(candidate) { var accessorAttributes = [ "get", "set", "value", "node", "create", "undefined", ]; var hasAccessors = false; if (typeof candidate == "object" && candidate != null) { hasAccessors = accessorAttributes.some(function (attributeName) { return candidate.hasOwnProperty(attributeName); }); } return hasAccessors; } /// Determine if a method or event initializer is a detailed initializer containing a /// parameter list along with the body text (method initializers only). Detect the type by /// searching for method and event initializer keys in the candidate object. /// /// @name module:vwf~valueHasBody /// /// @param {Object} candidate /// /// @returns {Boolean} valueHasBody(candidate) { // TODO: refactor and share with valueHasAccessors and possibly objectIsComponent // TODO: unlike a property initializer, we really only care if it's an object vs. text; text == use as body; object == presume o.parameters and o.body // TODO: except that a script in the unnamed-list format would appear as an object but should be used as the body var bodyAttributes = [ "parameters", "body", "listeners", ]; var hasBody = false; // TODO: "body" term is confusing, but that's the current terminology used in vwf/model/javascript if (typeof candidate == "object" && candidate != null) { hasBody = bodyAttributes.some(function (attributeName) { return candidate.hasOwnProperty(attributeName); }); } return hasBody; } /// Determine if a script initializer is a detailed initializer containing explicit text and /// type parameters (rather than being a simple text specification). Detect the type by /// searching for the script initializer keys in the candidate object. /// /// @name module:vwf~valueHasType /// /// @param {Object} candidate /// /// @returns {Boolean} valueHasType(candidate) { // TODO: refactor and share with valueHasBody, valueHasAccessors and possibly objectIsComponent var typeAttributes = [ "source", "text", "type", ]; var hasType = false; if (typeof candidate == "object" && candidate != null) { hasType = typeAttributes.some(function (attributeName) { return candidate.hasOwnProperty(attributeName); }); } return hasType; } /// Convert a potentially-namespaced member name into a string such that a namespaced name /// will be distinct from an encoded name in any other namespace, or from any simple name /// not having a namespace. /// /// Simple names are strings such as `"name"`. Namespaced names are arrays of strings, such /// as `[ "ns", "name" ]` or `[ "outer", "inner", "name" ]`. An array containing a single /// string, such as `[ "name" ]`, is not namespaced and is the same name as `"name"`. /// /// Each of the following encodes into a distinct value: /// /// `"name"` or `[ "name" ]` /// `[ "a", "name" ]` /// `[ "b", "name" ]` /// `[ "a", "a", "name" ]` /// `[ "a", "b", "name" ]` /// `[ "b", "b", "name" ]` /// *etc.* /// /// @name module:vwf~namespaceEncodedName /// /// @param {String|String[]} memberName /// A string, or an array of strings containing a name preceded by any number of namespace /// names. In an array, each element defines a unique space for the member name and for /// any intermediate namespaces. /// /// @returns {String} namespaceEncodedName(memberName) { if (typeof memberName === "object" && memberName instanceof Array) { return (memberName.length !== 1) ? "vwf$" + memberName.join("$") : memberName[0]; } else { return memberName; } } /// Convert a (potentially-abbreviated) component specification to a descriptor parsable by /// vwf.createChild. The following forms are accepted: /// /// - Descriptor: { extends: component, source: ..., type: ..., ... } /// - Component URI: http://host/path/to/component.vwf /// - Asset URI: http://host/ath/to/asset.type /// - Node ID /// /// They are converted as follows: /// /// - Descriptor: unchanged [1] /// - Component URI: a component that extends the component identified by the URI /// - Asset URI: a component having the asset identified by the URI as its source /// - Node ID: a component that extends the previously-created node identified by the ID /// /// [1] As a special case, missing MIME types are filled in for assets matcching the /// patterns *.unity3d and *.dae, and components having assets of those types but no /// prototype declared will be upgraded to extend scene.vwf and navscene.vwf, respectively. /// /// @name module:vwf~normalizedComponent /// /// @param {String|Object} component /// /// @returns {Object} normalizedComponent(component) { // Convert a component URI to an instance of that type or an asset reference to an // untyped reference to that asset. Convert a component ID to an instance of that // prototype. if (this.componentIsURI(component)) { if (component.match(/\.vwf$/)) { // TODO: detect component from mime-type instead of extension? component = { extends: component }; } else { component = { source: component }; } } else if (this.componentIsID(component)) { component = { extends: component }; } // Fill in the mime type from the source specification if not provided. if (component.source && !component.type) { // TODO: validate component var match = component.source.match(/\.([^.]*)$/); // TODO: get type from mime-type (from server if remote, from os if local, or (?) from this internal table otherwise) if (match) { switch (match[1]) { case "unity3d": component.type = "application/vnd.unity"; break; case "dae": component.type = "model/vnd.collada+xml"; break; } } } // Fill in the component type from the mime type if not provided. if (component.type && !component.extends) { // TODO: load from a server configuration file switch (component.type) { case "application/vnd.unity": component.extends = "http://vwf.example.com/scene.vwf"; break; case "model/vnd.collada+xml": component.extends = "http://vwf.example.com/navscene.vwf"; break; } } return component; } /// Convert a `Handler` specification into the standard form of an object containing /// `parameters`, `body` and `type` fields. /// /// @name module:vwf~normalizedHandler /// /// @param {Handler|string} /// @param {string[]} [defaultParameters] /// /// @returns {Handler} normalizedHandler(handler, defaultParameters) { // Convert abbreviated forms to the explict `Handler` form. if (typeof handler !== "object" || handler instanceof Array) { handler = { body: handler }; //if (require("vwf/configuration").active["preserve-script-closures"] } else if (this.configuration["preserve-script-closures"] && (typeof handler == "function" || handler instanceof Function)) { handler = { body: handler }; } // Use a default parameter list if the handler doesn't provide its own and if defaults // were provided. if (!handler.parameters && defaultParameters) { handler.parameters = defaultParameters; } // Fill in a default media type if `type` is not provided. A `body` of type `string` is // taken to be `application/javascript`. if (handler.type === undefined) { if (typeof handler.body === "string" || handler.body instanceof String) { handler.type = "application/javascript"; } } return handler; } /// Convert a `fields` object as passed between the client and reflector and stored in the /// message queue into a form suitable for writing to a log. /// /// @name module:vwf~loggableFields /// /// @param {Object} fields /// /// @returns {Object} loggableFields(fields) { return this.utility.transform(fields, this.utility.transforms.transit); } /// Convert a component URI, descriptor or ID into a form suitable for writing to a log. /// /// @name module:vwf~loggableComponent /// /// @param {String|Object} component /// /// @returns {String|Object} loggableComponent(component) { return this.utility.transform(component, this.loggableComponentTransformation); } /// Convert an arbitrary JavaScript value into a form suitable for writing to a log. /// /// @name module:vwf~loggableValue /// /// @param {Object} value /// /// @returns {Object} loggableValue(value) { let self = this; return this.utility.transform(value, function (object, names, depth) { object = self.utility.transforms.transit(object, names, depth); return typeof object == "number" ? Number(object.toPrecision(5)) : object; // reduce numeric precision to remove visual noise }); } /// Convert an array of arbitrary JavaScript values into a form suitable for writing to a /// log. /// /// @name module:vwf~loggableValues /// /// @param {Object[]|undefined} values /// /// @returns {Object[]|undefined} loggableValues(values) { return this.loggableValue(values); } /// Convert an object indexing arrays of arbitrary JavaScript values into a form suitable /// for writing to a log. /// /// @name module:vwf~loggableIndexedValues /// /// @param {Object|undefined} values /// /// @returns {Object|undefined} loggableIndexedValues(values) { return this.loggableValue(values); } /// Convert script text into a form suitable for writing to a log. /// /// @name module:vwf~loggableScript /// /// @param {String|undefined} script /// /// @returns {String} loggableScript(script) { return (script || "").replace(/\s+/g, " ").substring(0, 100); } // -- remappedURI -------------------------------------------------------------------------- /// Remap a component URI to its location in a local cache. /// /// http://vwf.example.com/component.vwf => http://localhost/proxy/vwf.example.com/component.vwf /// /// @name module:vwf~remappedURI remappedURI(uri, baseURI) { // var match = uri.match( RegExp( "http://(vwf.example.com)/(.*)" ) ); // if ( match ) { // uri = window.location.protocol + "//" + window.location.host + // "/proxy/" + match[1] + "/" + match[2]; // } if (baseURI && uri[0] !== "/") { let newURI = ""; let parts = baseURI.split('/'); parts[parts.length - 1] = uri; parts.map(el => { newURI = newURI + el + '/'; }) return newURI.slice(0, -1) } return uri } // -- resolvedDescriptor ------------------------------------------------------------------- /// Resolve relative URIs in a component descriptor. /// /// @name module:vwf~resolvedDescriptor resolvedDescriptor(component, baseURI) { return this.utility.transform(component, resolvedDescriptorTransformationWithBaseURI); function resolvedDescriptorTransformationWithBaseURI(object, names, depth) { return resolvedDescriptorTransformation.call(this, object, names, depth, baseURI); } } // -- loggableComponentTransformation ------------------------------------------------------ /// vwf/utility/transform() transformation function to truncate the verbose bits of a /// component so that it may be written to a log. /// /// @name module:vwf~loggableComponentTransformation loggableComponentTransformation(object, names, depth) { // Get our bearings at the current recusion level. var markers = this.descriptorMarkers(object, names, depth); // Transform the object here. switch (markers.containerName) { case "extends": // Omit a component descriptor for the prototype. if (markers.memberIndex == 0 && this.componentIsDescriptor(object)) { return {}; } break; case "implements": // Omit component descriptors for the behaviors. if (markers.memberIndex == 0 && this.componentIsDescriptor(object)) { return {}; } break; case "properties": // Convert property values to a loggable version, and omit getter and setter // text. if (markers.memberIndex == 0 && !this.valueHasAccessors(object) || markers.memberIndex == 1 && names[0] == "value") { return this.loggableValue(object); } else if (markers.memberIndex == 1 && (names[0] == "get" || names[0] == "set")) { return "..."; } break; case "methods": // Omit method body text. if (markers.memberIndex == 0 && !this.valueHasBody(object) || markers.memberIndex == 1 && names[0] == "body") { return "..."; } break; case "events": // Nothing for events. break; case "children": // Omit child component descriptors. if (markers.memberIndex == 0 && this.componentIsDescriptor(object)) { return {}; } break; case "scripts": // Shorten script text. if (markers.memberIndex == 0 && !this.valueHasType(object) || markers.memberIndex == 1 && names[0] == "text") { return "..."; } break; } return object; } // -- resolvedDescriptorTransformation ----------------------------------------------------- /// vwf/utility/transform() transformation function to resolve relative URIs in a component /// descriptor. /// /// @name module:vwf~resolvedDescriptorTransformation resolvedDescriptorTransformation(object, names, depth, baseURI) { // Get our bearings at the current recusion level. var markers = this.descriptorMarkers(object, names, depth); // Resolve all the URIs. switch (markers.containerName) { case "extends": case "implements": case "source": case "children": if (markers.memberIndex == 0 && this.componentIsURI(object)) { return object //require( "vwf/utility" ).resolveURI( object, baseURI ); } break; case "scripts": if (markers.memberIndex == 1 && names[0] == "source") { return object //require( "vwf/utility" ).resolveURI( object, baseURI ); } break; } return object; } // -- descriptorMarkers -------------------------------------------------------------------- /// Locate the closest container (`properties`, `methods`, `events`, `children`) and /// contained member in a `vwf/utility/transform` iterator call on a component descriptor. /// /// @name module:vwf~descriptorMarkers descriptorMarkers(object, names, depth) { // Find the index of the lowest nested component in the names list. var componentIndex = names.length; while (componentIndex > 2 && names[componentIndex - 1] == "children") { componentIndex -= 2; } // depth names notes // ----- ----- ----- // 0: [] the component // 1: [ "properties" ] its properties object // 2: [ "propertyName", "properties" ] one property // 1: [ "children" ] the children object // 2: [ "childName", "children" ] one child // 3: [ "properties", "childName", "children" ] the child's properties // 4: [ "propertyName", "properties", "childName", "children" ] one child property if (componentIndex > 0) { // Locate the container ("properties", "methods", "events", etc.) below the // component in the names list. var containerIndex = componentIndex - 1; var containerName = names[containerIndex]; // Locate the member as appropriate for the container. if (containerName == "extends") { var memberIndex = containerIndex; var memberName = names[memberIndex]; } else if (containerName == "implements") { if (containerIndex > 0) { if (typeof names[containerIndex - 1] == "number") { var memberIndex = containerIndex - 1; var memberName = names[memberIndex]; } else { var memberIndex = containerIndex; var memberName = undefined; } } else if (typeof object != "object" || !(object instanceof Array)) { var memberIndex = containerIndex; var memberName = undefined; } } else if (containerName == "source" || containerName == "type") { var memberIndex = containerIndex; var memberName = names[memberIndex]; } else if (containerName == "properties" || containerName == "methods" || containerName == "events" || containerName == "children") { if (containerIndex > 0) { var memberIndex = containerIndex - 1; var memberName = names[memberIndex]; } } else if (containerName == "scripts") { if (containerIndex > 0) { if (typeof names[containerIndex - 1] == "number") { var memberIndex = containerIndex - 1; var memberName = names[memberIndex]; } else { var memberIndex = containerIndex; var memberName = undefined; } } else if (typeof object != "object" || !(object instanceof Array)) { var memberIndex = containerIndex; var memberName = undefined; } } else { containerIndex = undefined; containerName = undefined; } } return { containerIndex: containerIndex, containerName: containerName, memberIndex: memberIndex, memberName: memberName, }; } /// Locate nodes matching a search pattern. {@link module:vwf/api/kernel.find} describes the /// supported patterns. /// /// This is the internal implementation used by {@link module:vwf.find} and /// {@link module:vwf.test}. /// /// This function must run as a method of the kernel. Invoke it as: /// `find.call( kernel, nodeID, matchPattern, initializedOnly )`. /// /// @name module:vwf~find /// /// @param {ID} nodeID /// The reference node. Relative patterns are resolved with respect to this node. `nodeID` /// is ignored for absolute patterns. /// @param {String} matchPattern /// The search pattern. /// @param {Boolean} [initializedOnly] /// Interpret nodes that haven't completed initialization as though they don't have /// ancestors. Drivers that manage application code should set `initializedOnly` since /// applications should never have access to uninitialized parts of the application graph. /// /// @returns {ID[]|undefined} /// An array of the node ids of the result. find(nodeID, matchPattern, initializedOnly) { // Evaluate the expression using the provided node as the reference. Take the root node // to be the root of the reference node's tree. If a reference node is not provided, use // the application as the root. var rootID = nodeID ? this.root(nodeID, initializedOnly) : this.application(initializedOnly); return this.xpath.resolve(matchPattern, rootID, nodeID, resolverWithInitializedOnly, this); // Wrap `xpathResolver` to pass `initializedOnly` through. function resolverWithInitializedOnly(step, contextID, resolveAttributes) { return this.xpathResolver.call(this, step, contextID, resolveAttributes, initializedOnly); } } // -- xpathResolver ------------------------------------------------------------------------ /// Interpret the steps of an XPath expression being resolved. Use with /// vwf.utility.xpath#resolve. /// /// @name module:vwf~xpathResolver /// /// @param {Object} step /// @param {ID} contextID /// @param {Boolean} [resolveAttributes] /// @param {Boolean} [initializedOnly] /// Interpret nodes that haven't completed initialization as though they don't have /// ancestors. Drivers that manage application code should set `initializedOnly` since /// applications should never have access to uninitialized parts of the application graph. /// /// @returns {ID[]} xpathResolver(step, contextID, resolveAttributes, initializedOnly) { let self = this; var resultIDs = []; switch (step.axis) { // case "preceding": // TODO // case "preceding-sibling": // TODO case "ancestor-or-self": resultIDs.push(contextID); Array.prototype.push.apply(resultIDs, this.ancestors(contextID, initializedOnly)); break; case "ancestor": Array.prototype.push.apply(resultIDs, this.ancestors(contextID, initializedOnly)); break; case "parent": var parentID = this.parent(contextID, initializedOnly); parentID && resultIDs.push(parentID); break; case "self": resultIDs.push(contextID); break; case "child": Array.prototype.push.apply(resultIDs, this.children(contextID, initializedOnly).filter(function (childID) { return childID; }, this) ); break; case "descendant": Array.prototype.push.apply(resultIDs, this.descendants(contextID, initializedOnly).filter(function (descendantID) { return descendantID; }, this) ); break; case "descendant-or-self": resultIDs.push(contextID); Array.prototype.push.apply(resultIDs, this.descendants(contextID, initializedOnly).filter(function (descendantID) { return descendantID; }, this) ); break; // case "following-sibling": // TODO // case "following": // TODO case "attribute": if (resolveAttributes) { resultIDs.push("@" + contextID); // TODO: @? } break; // n/a: case "namespace": // n/a: break; } switch (step.kind) { // Name test. case undefined: resultIDs = resultIDs.filter(function (resultID) { if (resultID[0] != "@") { // TODO: @? return self.xpathNodeMatchesStep.call(this, resultID, step.name); } else { return self.xpathPropertyMatchesStep.call(this, resultID.slice(1), step.name); // TODO: @? } }, this); break; // Element test. case "element": // Cases: kind(node,type) // element() // element(name) // element(,type) // element(name,type) resultIDs = resultIDs.filter(function (resultID) { return resultID[0] != "@" && self.xpathNodeMatchesStep.call(this, resultID, step.name, step.type); // TODO: @? }, this); break; // Attribute test. case "attribute": resultIDs = resultIDs.filter(function (resultID) { return resultID[0] == "@" && self.xpathPropertyMatchesStep.call(this, resultID.slice(1), step.name); // TODO: @? }, this); break; // The `doc()` function for referencing globals outside the current tree. // http://www.w3.org/TR/xpath-functions/#func-doc. case "doc": if (this.root(contextID, initializedOnly)) { var globalID = this.global(step.name, initializedOnly); resultIDs = globalID ? [globalID] : []; } else { resultIDs = []; } break; // Any-kind test. case "node": break; // Unimplemented test. default: resultIDs = []; break; } return resultIDs; } // -- xpathNodeMatchesStep ----------------------------------------------------------------- /// Determine if a node matches a step of an XPath expression being resolved. /// /// @name module:vwf~xpathNodeMatchesStep /// /// @param {ID} nodeID /// @param {String} [name] /// @param {String} [type] /// /// @returns {Boolean} xpathNodeMatchesStep(nodeID, name, type) { if (name && this.name(nodeID) != name) { return false; } var matches_type = !type || this.uri(nodeID) == type || this.prototypes(nodeID, true).some(function (prototypeID) { return this.uri(prototypeID) == type; }, this); return matches_type; } // -- xpathPropertyMatchesStep ------------------------------------------------------------- /// Determine if a property matches a step of an XPath expression being resolved. /// /// @name module:vwf~xpathPropertyMatchesStep /// /// @param {ID} nodeID /// @param {String} [name] /// /// @returns {Boolean} xpathPropertyMatchesStep(nodeID, name) { var properties = this.models.object.properties(nodeID); if (name) { return properties[name]; } else { return Object.keys(properties).some(function (propertyName) { return properties[propertyName]; }, this); } } /// Merge two component descriptors into a single descriptor for a combined component. A /// component created from the combined descriptor will behave in the same way as a /// component created from `nodeDescriptor` that extends a component created from /// `prototypeDescriptor`. /// /// Warning: this implementation modifies `prototypeDescriptor`. /// /// @name module:vwf~mergeDescriptors /// /// @param {Object} nodeDescriptor /// A descriptor representing a node extending `prototypeDescriptor`. /// @param {Object} prototypeDescriptor /// A descriptor representing a prototype for `nodeDescriptor`. // Limitations: // // - Doesn't merge children from the prototype with like-named children in the node. // - Doesn't merge property setters and getters from the prototype when the node provides // an initializing value. // - Methods from the prototype descriptor are lost with no way to invoke them if the node // overrides them. // - Scripts from both the prototype and the node are retained, but if both define an // `initialize` function, the node's `initialize` will overwrite `initialize` in // the prototype. // - The prototype doesn't carry its location with it, so relative paths will load with // respect to the location of the node. mergeDescriptors(nodeDescriptor, prototypeDescriptor) { if (nodeDescriptor.implements) { prototypeDescriptor.implements = (prototypeDescriptor.implements || []). concat(nodeDescriptor.implements); } if (nodeDescriptor.source) { prototypeDescriptor.source = nodeDescriptor.source; prototypeDescriptor.type = nodeDescriptor.type; } if (nodeDescriptor.properties) { prototypeDescriptor.properties = prototypeDescriptor.properties || {}; for (var propertyName in nodeDescriptor.properties) { prototypeDescriptor.properties[propertyName] = nodeDescriptor.properties[propertyName]; } } if (nodeDescriptor.methods) { prototypeDescriptor.methods = prototypeDescriptor.methods || {}; for (var methodName in nodeDescriptor.methods) { prototypeDescriptor.methods[methodName] = nodeDescriptor.methods[methodName]; } } if (nodeDescriptor.events) { prototypeDescriptor.events = prototypeDescriptor.events || {}; for (var eventName in nodeDescriptor.events) { prototypeDescriptor.events[eventName] = nodeDescriptor.events[eventName]; } } if (nodeDescriptor.children) { prototypeDescriptor.children = prototypeDescriptor.children || {}; for (var childName in nodeDescriptor.children) { prototypeDescriptor.children[childName] = nodeDescriptor.children[childName]; } } if (nodeDescriptor.scripts) { prototypeDescriptor.scripts = (prototypeDescriptor.scripts || []). concat(nodeDescriptor.scripts); } return prototypeDescriptor; } /// Return an {@link external:Object.defineProperty} descriptor for a property that is to be /// enumerable, but not writable or configurable. /// /// @param value /// A value to wrap in a descriptor. /// /// @returns /// An {@link external:Object.defineProperty} descriptor. enumerable(value) { return { value: value, enumerable: true, writable: false, configurable: false, }; } /// Return an {@link external:Object.defineProperty} descriptor for a property that is to be /// enumerable and writable, but not configurable. /// /// @param value /// A value to wrap in a descriptor. /// /// @returns /// An {@link external:Object.defineProperty} descriptor. writable(value) { return { value: value, enumerable: true, writable: true, configurable: false, }; } /// Return an {@link external:Object.defineProperty} descriptor for a property that is to be /// enumerable, writable, and configurable. /// /// @param value /// A value to wrap in a descriptor. /// /// @returns /// An {@link external:Object.defineProperty} descriptor. configurable(value) { return { value: value, enumerable: true, writable: true, configurable: true, }; } async chooseConnection(data) { if (this.isLuminary) { return await this.luminary.connect(data) //use Luminary } else { return data //use Reflector } } } export { VWF }