"use strict"; // Copyright 2012 United States Government, as represented by the Secretary of Defense, Under // Secretary of Defense (Personnel & Readiness). // // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except // in compliance with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software distributed under the License // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express // or implied. See the License for the specific language governing permissions and limitations under // the License. /// @module vwf /// vwf.js is the main Virtual World Framework manager. It is constructed as a JavaScript module /// (http://www.yuiblog.com/blog/2007/06/12/module-pattern) 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. ( function( window ) { window.console && console.debug && console.debug( "loading vwf" ); window.vwf = new function() { window.console && console.debug && console.debug( "creating vwf" ); // == Public variables ===================================================================== /// The runtime environment (production, development, testing) and other configuration /// settings appear here. /// /// @name module:vwf.configuration /// /// @private this.configuration = 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 = undefined; // require( "vwf/kernel/utility" ); // TODO: assign here after converting vwf.js to a RequireJS module and listing "vwf/kernel/utility" as a dependency /// The kernel logger. /// /// @name module:vwf.logger /// /// @private this.logger = 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 simulation clock, which contains the current time in seconds. Time is controlled by /// the reflector and updates here as we receive control messages. /// /// @name module:vwf.now /// /// @private this.now = 0; /// The queue's sequence number for the currently executing action. /// /// The queue enumerates actions in order of arrival, which is distinct from execution order /// since actions may be scheduled to run in the future. `sequence_` can be used to /// distinguish between actions that were previously placed on the queue for execution at a /// later time, and those that arrived after the current action, regardless of their /// scheduled time. /// /// @name module:vwf.sequence_ /// /// @private this.sequence_ = undefined; /// The moniker of the client responsible for the currently executing action. `client_` will /// be falsy for actions originating in the server, such as time ticks. /// /// @name module:vwf.client_ /// /// @private this.client_ = undefined; /// 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 var components = this.private.components = {}; // maps component node ID => component specification /// This is the connection to the reflector. In this sample implementation, "socket" is a /// socket.io client that communicates over a channel provided by the server hosting the /// client documents. /// /// @name module:vwf~socket var socket = this.private.socket = undefined; // 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 var vwf = this; // Store the jQuery module for reuse //var jQuery; // == 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 }, { ... } ) this.loadConfiguration = function(/* [ componentURI|componentObject ] { modelInitializers } { viewInitializers } */) { var args = Array.prototype.slice.call( arguments ); if ( typeof args[0] != "object" || ! ( args[0] instanceof Array ) ) { application = args.shift(); } var userLibraries = args.shift() || {}; var applicationConfig = {}; var callback = args.shift(); var requireConfig = { shim: { "vwf/model/aframe/addon/aframe-interpolation": { deps: [ "vwf/model/aframe/aframe-master" ] }, "vwf/model/aframe/extras/aframe-extras.loaders": { deps: [ "vwf/model/aframe/aframe-master" ] }, "vwf/model/aframe/addon/aframe-sun-sky": { deps: [ "vwf/model/aframe/aframe-master" ] }, "vwf/model/aframe/extras/aframe-extras.controls.min": { deps: [ "vwf/model/aframe/aframe-master" ] }, "vwf/model/aframe/addon/SkyShader": { deps: [ "vwf/model/aframe/aframe-master" ] }, "vwf/model/aframe/addon/BVHLoader": { deps: [ "vwf/model/aframe/aframe-master" ] }, "vwf/model/aframe/addon/TransformControls": { deps: [ "vwf/model/aframe/aframe-master" ] }, "vwf/model/aframe/addon/THREE.MeshLine": { deps: [ "vwf/model/aframe/aframe-master" ] }, "vwf/model/aframe/addon/aframe-components": { deps: [ "vwf/model/aframe/aframe-master", "vwf/model/aframe/extras/aframe-extras.loaders", "vwf/model/aframe/addon/aframe-sun-sky", "vwf/model/aframe/addon/SkyShader", "vwf/model/aframe/addon/BVHLoader", "vwf/model/aframe/addon/TransformControls", "vwf/model/aframe/addon/THREE.MeshLine" ] } } }; //jQuery = require("jquery"); var requireArray = [ { library: "domReady", active: true }, { library: "vwf/configuration", active: true }, { library: "vwf/kernel/model", active: true }, { library: "vwf/model/javascript", active: true }, { library: "vwf/model/object", active: true }, { library: "vwf/model/stage/log", active: true }, { library: "vwf/model/ohm", active: true }, { library: "vwf/model/osc", active: true }, { library: "vwf/model/aframe/addon/aframe-components", linkedLibraries: [ "vwf/model/aframe/addon/SkyShader" ], active: false }, { library: "vwf/model/aframe", linkedLibraries: [ "vwf/model/aframe/aframe-master", "vwf/model/aframe/extras/aframe-extras.loaders", "vwf/model/aframe/addon/aframe-interpolation", "vwf/model/aframe/addon/aframe-sun-sky", "vwf/model/aframe/addon/aframe-components", "vwf/model/aframe/addon/SkyShader", "vwf/model/aframe/extras/aframe-extras.controls.min", "vwf/model/aframe/addon/BVHLoader", "vwf/model/aframe/addon/TransformControls", "vwf/model/aframe/addon/THREE.MeshLine" ], active: false }, { library: "vwf/model/aframeComponent", active: true }, { library: "vwf/kernel/view", active: true }, { library: "vwf/view/document", active: true }, { library: "vwf/view/editor-new", active: false }, { library: "vwf/view/webrtc", // linkedLibraries: ["vwf/view/webrtc/adapter"], active: true }, { library: "vwf/view/ohm", active: true }, { library: "vwf/view/osc", active: true }, { library: "vwf/view/aframe", active: true }, { library: "vwf/model/aframe/aframe-master", active: false }, { library: "vwf/model/aframe/extras/aframe-extras.loaders", active: false }, { library: "vwf/model/aframe/addon/aframe-interpolation", active: false }, { library: "vwf/model/aframe/addon/aframe-sun-sky", active: false }, { library: "vwf/model/aframe/addon/aframe-components", active: false }, { library: "vwf/model/aframe/addon/BVHLoader", active: false }, { library: "vwf/model/aframe/addon/TransformControls", active: false }, { library: "vwf/model/aframe/addon/THREE.MeshLine", active: false }, { library: "vwf/model/aframe/addon/SkyShader", active: false }, { library: "vwf/model/aframe/extras/aframe-extras.controls.min", active: false }, { library: "vwf/view/aframeComponent", active: true }, { library: "vwf/kernel/utility", active: true }, { library: "vwf/utility", active: true }, // { library: "vwf/view/webrtc/adapter", active: false }, //{ library: "vwf/view/webrtc/adapterWebRTC", active: false }, // { library: "vwf/admin", active: false } ]; var initializers = { model: [ { library: "vwf/model/javascript", active: true }, { library: "vwf/model/ohm", active: true }, { library: "vwf/model/osc", active: true }, { library: "vwf/model/aframe", active: true }, { library: "vwf/model/aframeComponent", active: true }, { library: "vwf/model/object", active: true } ], view: [ { library: "vwf/view/document", active: true }, { library: "vwf/view/editor-new", active: false }, { library: "vwf/view/ohm", active: true }, { library: "vwf/view/osc", active: true }, { library: "vwf/view/aframe", active: true }, { library: "vwf/view/aframeComponent", active: true }, { library: "vwf/view/webrtc", active: true} ] }; mapLibraryName(requireArray); mapLibraryName(initializers["model"]); mapLibraryName(initializers["view"]); function mapLibraryName(array) { for(var i=0;i res.json()) .catch(error => console.error('Error:', error)) .then(response => { console.log('Success:', response); this.ready( application, response); }); }; // -- ready -------------------------------------------------------------------------------- /// @name module:vwf.ready this.ready = function( 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. try { var options = { // The socket is relative to the application path. // resource: window.location.pathname.slice( 1, // window.location.pathname.lastIndexOf("/") ), query: { pathname: window.location.pathname.slice( 1, window.location.pathname.lastIndexOf("/") ), appRoot: "./public", path: JSON.stringify(path) }, // query: 'pathname=' + window.location.pathname.slice( 1, // window.location.pathname.lastIndexOf("/") ), // Use a secure connection when the application comes from https. secure: window.location.protocol === "https:", // Don't attempt to reestablish lost connections. The client reloads after a // disconnection to recreate the application from scratch. //reconnect: false, reconnection: false, transports: ['websocket'] }; if ( isSocketIO07() ) { //window.location.host var host = localStorage.getItem('lcs_reflector'); if(!host) host = window.location.origin; socket = io.connect( host, options ); } else { // Ruby Server -- only supports socket.io 0.6 io.util.merge( options, { // For socket.io 0.6, specify the port since the default isn't correct when // using https. port: window.location.port || ( window.location.protocol === "https:" ? 443 : 80 ), // The ruby socket.io server only supports WebSockets. Don't try the others. transports: [ 'websocket', ], // Increase the timeout because of starvation while loading the scene. The // server timeout must also be increased. (For socket.io 0.7+, the client // timeout is controlled by the server.) transportOptions: { "websocket": { timeout: 90000 }, }, } ); socket = io.connect( undefined, options ); } } catch ( e ) { // If a connection to the reflector is not available, then run in single-user mode. // Messages intended for the reflector will loop directly back to us in this case. // Start a timer to monitor the incoming queue and dispatch the messages as though // they were received from the server. this.dispatch(); setInterval( function() { var fields = { time: vwf.now + 0.010, // TODO: there will be a slight skew here since the callback intervals won't be exactly 10 ms; increment using the actual delta time; also, support play/pause/stop and different playback rates as with connected mode. origin: "reflector", }; queue.insert( fields, true ); // may invoke dispatch(), so call last before returning to the host }, 10 ); } if ( socket ) { socket.on('connect_error', function(err) { console.log(err); var errDiv = document.createElement("div"); errDiv.innerHTML = "
Connection error!
"; document.querySelector('body').appendChild(errDiv); }); socket.on( "connect", function() { vwf.logger.infox( "-socket", "connected" ); if ( isSocketIO07() ) { vwf.moniker_ = this.id; } else { //Ruby Server vwf.moniker_ = this.transport.sessionid; } } ); // Configure a handler to receive messages from the server. // Note that this example code doesn't implement a robust parser capable of handling // arbitrary text and that the messages should be placed in a dedicated priority // queue for best performance rather than resorting the queue as each message // arrives. Additionally, overlapping messages may cause actions to be performed out // of order in some cases if messages are not processed on a single thread. socket.on( "message", function( message ) { // vwf.logger.debugx( "-socket", "message", message ); try { if ( isSocketIO07() ) { var fields = message; } else { // Ruby Server - Unpack the arguements var fields = JSON.parse( message ); } fields.time = Number( fields.time ); // TODO: other message validation (check node id, others?) fields.origin = "reflector"; // Update the queue. Messages in the queue are ordered by time, then by order of arrival. // Time is only advanced if the message has no action, meaning it is a tick. queue.insert( fields, !fields.action ); // may invoke dispatch(), so call last before returning to the host // Each message from the server allows us to move time forward. Parse the // timestamp from the message and call dispatch() to execute all queued // actions through that time, including the message just received. // The simulation may perform immediate actions at the current time or it // may post actions to the queue to be performed in the future. But we only // move time forward for items arriving in the queue from the reflector. } catch ( e ) { vwf.logger.warn( fields.action, fields.node, fields.member, fields.parameters, "exception performing action:", require( "vwf/utility" ).exceptionMessage( e ) ); } } ); socket.on( "disconnect", function() { vwf.logger.infox( "-socket", "disconnected" ); // Reload to rejoin the application. window.location = window.location.href; } ); socket.on( "error", function() { //Overcome by compatibility.js websockets check document.querySelector('body').innerHTML = "
WebSockets connections are currently being blocked. Please check your proxy server settings.
"; // jQuery('body').html("
WebSockets connections are currently being blocked. Please check your proxy server settings.
"); } ); if ( !isSocketIO07() ) { // Start communication with the reflector. socket.connect(); // TODO: errors can occur here too, particularly if a local client contains the socket.io files but there is no server; do the loopback here instead of earlier in response to new io.Socket. } } else if ( component_uri_or_json_or_object ) { // Load the application. The application is rooted in a single node constructed here // as an instance of the component passed to initialize(). That component, its // prototype(s), and its children, and their prototypes and children, flesh out the // entire application. // TODO: add note that this is only for a self-determined application; with socket, wait for reflection server to tell us. // TODO: maybe depends on component_uri_or_json_or_object too; when to override and not connect to reflection server? this.createNode( component_uri_or_json_or_object, "application" ); } else { // TODO: also do this if component_uri_or_json_or_object was invalid and createNode() failed // TODO: show a selection dialog } }; // -- plan --------------------------------------------------------------------------------- /// @name module:vwf.plan this.plan = function( nodeID, actionName, memberName, parameters, when, callback_async /* ( result ) */ ) { this.logger.debuggx( "plan", nodeID, actionName, memberName, parameters && parameters.length, when, callback_async && "callback" ); var time = when > 0 ? // absolute (+) or relative (-) Math.max( this.now, when ) : this.now + ( -when ); var fields = { time: time, node: nodeID, action: actionName, member: memberName, parameters: parameters, client: this.client_, // propagate originating client origin: "future", // callback: callback_async, // TODO }; queue.insert( fields ); this.logger.debugu(); }; // -- send --------------------------------------------------------------------------------- /// Send a message to the reflector. The message will be reflected back to all participants /// in the instance. /// /// @name module:vwf.send this.send = function( nodeID, actionName, memberName, parameters, when, callback_async /* ( result ) */ ) { this.logger.debuggx( "send", nodeID, actionName, memberName, parameters && parameters.length, when, callback_async && "callback" ); // TODO: loggableParameters() var time = when > 0 ? // absolute (+) or relative (-) Math.max( this.now, when ) : this.now + ( -when ); // Attach the current simulation time and pack the message as an array of the arguments. var fields = { time: time, node: nodeID, action: actionName, member: memberName, parameters: require( "vwf/utility" ).transform( parameters, require( "vwf/utility" ).transforms.transit ), // callback: callback_async, // TODO: provisionally add fields to queue (or a holding queue) then execute callback when received back from reflector }; if ( socket ) { // Send the message. var message = JSON.stringify( fields ); socket.send( message ); } else { // In single-user mode, loop the message back to the incoming queue. fields.client = this.moniker_; // stamp with the originating client like the reflector does fields.origin = "reflector"; queue.insert( fields ); } this.logger.debugu(); }; // -- respond ------------------------------------------------------------------------------ /// Return a result for a function invoked by the server. /// /// @name module:vwf.respond this.respond = function( nodeID, actionName, memberName, parameters, result ) { this.logger.debuggx( "respond", nodeID, actionName, memberName, parameters && parameters.length, "..." ); // TODO: loggableParameters(), loggableResult() // Attach the current simulation time and pack the message as an array of the arguments. var fields = { // sequence: undefined, // TODO: use to identify on return from reflector? time: this.now, node: nodeID, action: actionName, member: memberName, parameters: require( "vwf/utility" ).transform( parameters, require( "vwf/utility" ).transforms.transit ), result: require( "vwf/utility" ).transform( result, require( "vwf/utility" ).transforms.transit ), }; if ( socket ) { // Send the message. var message = JSON.stringify( fields ); socket.send( message ); } else { // Nothing to do in single-user mode. } this.logger.debugu(); }; // -- receive ------------------------------------------------------------------------------ /// Handle receipt of a message. Unpack the arguments and call the appropriate handler. /// /// @name module:vwf.receive this.receive = function( nodeID, actionName, memberName, parameters, respond, origin ) { // origin == "reflector" ? // this.logger.infogx( "receive", nodeID, actionName, memberName, // parameters && parameters.length, respond, origin ) : // this.logger.debuggx( "receive", nodeID, actionName, memberName, // parameters && parameters.length, respond, origin ); // TODO: delegate parsing and validation to each action. // Look up the action handler and invoke it with the remaining parameters. // Note that the message should be validated before looking up and invoking an arbitrary // handler. var args = [], result; if ( nodeID || nodeID === 0 ) args.push( nodeID ); if ( memberName ) args.push( memberName ); if ( parameters ) args = args.concat( parameters ); // flatten if(actionName == 'createChild') { console.log("create child!"); // args.push(function(childID) // { // //when creating over the reflector, call ready on heirarchy after create. // //nodes from setState are readied in createNode // // vwf.decendants(childID).forEach(function(i){ // // vwf.callMethod(i,'ready',[]); // // }); // // vwf.callMethod(childID,'ready',[]); // console.log("create child!"); // }); } // Invoke the action. if ( environment( actionName, parameters ) ) { require( "vwf/configuration" ).environment = environment( actionName, parameters ); } else if ( origin !== "reflector" || ! nodeID || nodes.existing[ nodeID ] ) { result = this[ actionName ] && this[ actionName ].apply( this, args ); } else { this.logger.debugx( "receive", "ignoring reflector action on non-existent node", nodeID ); result = undefined; } // Return the result. respond && this.respond( nodeID, actionName, memberName, parameters, result ); // origin == "reflector" ? // this.logger.infou() : this.logger.debugu(); /// The reflector sends a `setState` action as part of the application launch to pass /// the server's execution environment to the client. A `setState` action isn't really /// appropriate though since `setState` should be the last part of the launch, whereas /// the environment ought to be set much earlier--ideally before the kernel loads. /// /// Executing the `setState` as received would overwrite any configuration settings /// already applied by the application. So instead, we detect this particular message /// and only use it to update the environment in the configuration object. /// /// `environment` determines if a message is the reflector's special pre-launch /// `setState` action, and if so, and if the application hasn't been created yet, /// returns the execution environment property. function environment( actionName, parameters ) { if ( actionName === "setState" && ! vwf.application() ) { var applicationState = parameters && parameters[0]; if ( applicationState && Object.keys( applicationState ).length === 1 && applicationState.configuration && Object.keys( applicationState.configuration ).length === 1 ) { return applicationState.configuration.environment; } } return undefined; } }; // -- dispatch ----------------------------------------------------------------------------- /// Dispatch incoming messages waiting in the queue. "currentTime" specifies the current /// simulation time that we should advance to and was taken from the time stamp of the last /// message received from the reflector. /// /// @name module:vwf.dispatch this.dispatch = function() { var fields; // Actions may use receive's ready function to suspend the queue for asynchronous // operations, and to resume it when the operation is complete. while ( fields = /* assignment! */ queue.pull() ) { // Advance time to the message time. if ( this.now != fields.time ) { this.sequence_ = undefined; // clear after the previous action this.client_ = undefined; // clear after the previous action this.now = fields.time; this.tock(); } // Perform the action. if ( fields.action ) { // TODO: don't put ticks on the queue but just use them to fast-forward to the current time (requires removing support for passing ticks to the drivers and nodes) this.sequence_ = fields.sequence; // note the message's queue sequence number for the duration of the action this.client_ = fields.client; // ... and note the originating client this.receive( fields.node, fields.action, fields.member, fields.parameters, fields.respond, fields.origin ); } else { this.tick(); } } // Advance time to the most recent time received from the server. Tick if the time // changed. if ( queue.ready() && this.now != queue.time ) { this.sequence_ = undefined; // clear after the previous action this.client_ = undefined; // clear after the previous action this.now = queue.time; this.tock(); } }; // -- log ---------------------------------------------------------------------------------- /// Send a log message to the reflector. /// /// @name module:vwf.log this.log = function() { this.respond( undefined, "log", undefined, undefined, arguments ); } // -- tick --------------------------------------------------------------------------------- /// Tick each tickable model, view, and node. Ticks are sent on each reflector idle message. /// /// @name module:vwf.tick // TODO: remove, in favor of drivers and nodes exclusively using future scheduling; // TODO: otherwise, all clients must receive exactly the same ticks at the same times. this.tick = function() { // Call ticking() on each model. this.models.forEach( function( model ) { model.ticking && model.ticking( this.now ); // TODO: maintain a list of tickable models and only call those }, this ); // Call ticked() on each view. this.views.forEach( function( view ) { view.ticked && view.ticked( this.now ); // TODO: maintain a list of tickable views and only call those }, this ); // Call tick() on each tickable node. this.tickable.nodeIDs.forEach( function( nodeID ) { this.callMethod( nodeID, "tick", [ this.now ] ); }, this ); }; // -- tock --------------------------------------------------------------------------------- /// Notify views of a kernel time change. Unlike `tick`, `tock` messages are sent each time /// that time moves forward. Only view drivers are notified since the model state should be /// independent of any particular sequence of idle messages. /// /// @name module:vwf.tock this.tock = function() { // Call tocked() on each view. this.views.forEach( function( view ) { view.tocked && view.tocked( this.now ); }, this ); }; // -- 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} this.setState = function( applicationState, callback_async /* () */ ) { this.logger.debuggx( "setState" ); // TODO: loggableState // Set the runtime configuration. if ( applicationState.configuration ) { require( "vwf/configuration" ).instance = applicationState.configuration; } // Update the internal kernel state. if ( applicationState.kernel ) { if ( applicationState.kernel.time !== undefined ) vwf.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 = nodes.length > 1 || applicationState.annotations ? annotations[nodeIndex] : "application"; vwf.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. queue.filter( function( fields ) { if ( fields.origin === "reflector" && fields.sequence > vwf.sequence_ ) { return true; } else { vwf.logger.debugx( "setState", function() { return [ "removing", JSON.stringify( loggableFields( fields ) ), "from queue" ]; } ); } } ); // Set the queue time and add the incoming items to the queue. if ( applicationState.queue ) { queue.time = applicationState.queue.time; queue.insert( applicationState.queue.queue || [] ); } callback_async && callback_async(); } ); this.logger.debugu(); }; // -- getState ----------------------------------------------------------------------------- /// @name module:vwf.getState /// /// @see {@link module:vwf/api/kernel.getState} this.getState = function( full, normalize ) { this.logger.debuggx( "getState", full, normalize ); // Get the application nodes and queue. var applicationState = { // Runtime configuration. configuration: require( "vwf/configuration" ).active, // Internal kernel state. kernel: { time: vwf.now, }, // Global node and descendant deltas. nodes: [ // TODO: all global objects this.getNode( "http://vwf.example.com/clients.vwf", full ), this.getNode( this.application(), full ), ], // `createNode` annotations, keyed by `nodes` indexes. annotations: { 1: "application", }, // Message queue. queue: { // TODO: move to the queue object time: queue.time, queue: require( "vwf/utility" ).transform( queue.queue, queueTransitTransformation ), }, }; // Normalize for consistency. if ( normalize ) { applicationState = require( "vwf/utility" ).transform( applicationState, require( "vwf/utility" ).transforms.hash ); } this.logger.debugu(); return applicationState; }; // -- hashState ---------------------------------------------------------------------------- /// @name module:vwf.hashState /// /// @see {@link module:vwf/api/kernel.hashState} this.hashState = function() { 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} this.createNode = function( nodeComponent, nodeAnnotation, baseURI, callback_async /* ( nodeID ) */ ) { // 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( loggableComponent( nodeComponent ) ), nodeAnnotation ]; } ); var nodePatch; if ( 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 ( 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[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 ( ! components[nodeURI] ) { // uri is not loading (Array) or is loaded (id) components[nodeURI] = []; // [] => array of callbacks while loading => true 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 ( components[nodeURI] instanceof Array ) { // uri is loading callback_async && 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 ) { vwf.setNode( components[nodeURI], nodePatch, function( nodeID ) /* async */ { callback_async && callback_async( components[nodeURI] ); // TODO: is this leaving a series callback hanging if we don't call series_callback_async? } ); } else { callback_async && callback_async( 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 ( componentIsDescriptor( nodeComponent ) && nodeComponent.includes && componentIsURI( nodeComponent.includes ) ) { // TODO: for "includes:", accept an already-loaded component (which componentIsURI exludes) since the descriptor will be loaded again var prototypeURI = require( "vwf/utility" ).resolveURI( nodeComponent.includes, nodeURI || baseURI ); loadComponent( prototypeURI, undefined, function( prototypeDescriptor ) /* async */ { prototypeDescriptor = resolvedDescriptor( prototypeDescriptor, prototypeURI ); nodeComponent = 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 ( 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. vwf.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 ( componentIsID( nodeComponent ) || components[ nodeComponent ] instanceof Array ) { // ID nodeID = nodeComponent; if ( nodePatch ) { vwf.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 = components[nodeURI]; 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} this.deleteNode = function( nodeID ) { 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( components ).some( function( nodeURI ) { // components: nodeURI => nodeID if ( components[nodeURI] == nodeID ) { delete components[nodeURI]; return true; } } ); // 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. nodes.delete( nodeID ); // 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} this.setNode = function( nodeID, nodeComponent, callback_async /* ( nodeID ) */ ) { // TODO: merge with createChild? this.logger.debuggx( "setNode", function() { return [ nodeID, JSON.stringify( loggableComponent( nodeComponent ) ) ]; } ); var node = nodes.existing[nodeID]; // Set the internal state. vwf.models.object.internals( nodeID, nodeComponent ); // Suppress kernel reentry so that we can write the state without coloring from // any scripts. vwf.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 ( valueHasAccessors( propertyValue ) && propertyValue.node ) { propertyValue = vwf.kutility.nodeReference( propertyValue.node ); } // Create or initialize the property. if ( creating ) { vwf.createProperty( nodeID, propertyName, propertyValue ); } else { vwf.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 ) { vwf.createMethod( nodeID, methodName, methodHandler.parameters, methodHandler.body ); } else { vwf.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 ) { vwf.createEvent( nodeID, eventName, eventDescriptor.parameters ); vwf.setEvent( nodeID, eventName, eventDescriptor ); // set the listeners since `createEvent` can't do it yet } else { vwf.setEvent( nodeID, eventName, eventDescriptor ); } // TODO: delete when eventDescriptor === null in patch } ); // Restore kernel reentry. vwf.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 = ! nodeHasOwnChild.call( vwf, nodeID, childName ); if ( creating ) { vwf.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 { vwf.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 */ { 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 = vwf.uri( nodeID, true ); async.map( scripts, function( script, map_callback_async /* ( err, result ) */ ) { if ( valueHasType( script ) ) { if ( script.source ) { 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. vwf.models.kernel.disable(); // Create each script. scripts.forEach( function( script ) { vwf.execute( nodeID, script.text, script.type ); // TODO: callback } ); // Restore kernel reentry. vwf.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} this.getNode = function( nodeID, full, normalize ) { // TODO: options to include/exclude children, prototypes this.logger.debuggx( "getNode", nodeID, full ); var node = 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. 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. vwf.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 ); 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. vwf.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]; } } if ( Object.keys( nodeComponent.children ).length == 0 ) { delete nodeComponent.children; } else { patched = true; } // Scripts. // TODO: scripts // Normalize for consistency. if ( normalize ) { nodeComponent = require( "vwf/utility" ).transform( nodeComponent, require( "vwf/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} this.hashNode = function( 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} this.createChild = function( nodeID, childName, childComponent, childURI, callback_async /* ( childID ) */ ) { this.logger.debuggx( "createChild", function() { return [ nodeID, childName, JSON.stringify( loggableComponent( childComponent ) ), childURI ]; } ); childComponent = 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 = 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. vwf.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 = vwf.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 ( componentIsDescriptor( childComponent ) && childComponent.includes && componentIsURI( childComponent.includes ) ) { // TODO: for "includes:", accept an already-loaded component (which componentIsURI exludes) since the descriptor will be loaded again var prototypeURI = require( "vwf/utility" ).resolveURI( childComponent.includes, baseURI ); var sync = true; // will loadComponent() complete synchronously? loadComponent( prototypeURI, undefined, function( prototypeDescriptor ) /* async */ { // Resolve relative references with respect to the included component. prototypeDescriptor = resolvedDescriptor( prototypeDescriptor, prototypeURI ); // Merge the child descriptor onto the `includes` descriptor. childComponent = mergeDescriptors( childComponent, prototypeDescriptor ); // modifies prototypeDescriptor if ( sync ) { queue.suspend( "before beginning " + childID ); // suspend the queue async.nextTick( function() { series_callback_async( undefined, undefined ); queue.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 { queue.suspend( "before beginning " + childID ); // suspend the queue async.nextTick( function() { series_callback_async( undefined, undefined ); queue.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 || vwf.kutility.protoNodeURI; vwf.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 = vwf.intrinsics( prototypeID ); if ( prototype_intrinsics.source ) { var prototype_uri = vwf.uri( prototypeID ); var prototype_properties = vwf.getProperties( prototypeID ); childComponent.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 ) */ ) { vwf.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 = nodes.create( childID, childPrototypeID, childBehaviorIDs, childURI, childName, nodeID ); // For the proto-prototype node `node.vwf`, register the meta events. if ( childID === vwf.kutility.protoNodeURI ) { child.events.create( namespaceEncodedName( [ "properties", "created" ] ) ); child.events.create( namespaceEncodedName( [ "properties", "initialized" ] ) ); child.events.create( namespaceEncodedName( [ "properties", "deleted" ] ) ); child.events.create( namespaceEncodedName( [ "methods", "created" ] ) ); child.events.create( namespaceEncodedName( [ "methods", "deleted" ] ) ); child.events.create( namespaceEncodedName( [ "events", "created" ] ) ); child.events.create( namespaceEncodedName( [ "events", "deleted" ] ) ); child.events.create( namespaceEncodedName( [ "children", "added" ] ) ); child.events.create( 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. vwf.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 && 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( vwf.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() { queue.suspend( "while loading " + childComponent.source + " for " + childID + " in creatingNode" ); // suspend the queue timeoutID = window.setTimeout( function() { resume( "timeout loading " + childComponent.source ) }, vwf.configuration[ "load-timeout" ] * 1000 ); driver_ready = false; } function resume( err ) { window.clearTimeout( timeoutID ); driver_ready = true; err && vwf.logger.warnx( "createChild", nodeID, childName + ":", err ); each_callback_async( err ); // resume createChild() queue.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( vwf.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() { queue.suspend( "while loading " + childComponent.source + " for " + childID + " in createdNode" ); // suspend the queue timeoutID = window.setTimeout( function() { resume( "timeout loading " + childComponent.source ) }, vwf.configuration[ "load-timeout" ] * 1000 ); driver_ready = false; } function resume( err ) { window.clearTimeout( timeoutID ); driver_ready = true; err && vwf.logger.warnx( "createChild", nodeID, childName + ":", err ); each_callback_async( err ); // resume createChild() queue.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. vwf.models.object.internals( childID, childComponent ); // Suppress kernel reentry so that we can read the state without coloring from // any scripts. replicating && vwf.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 ( valueHasAccessors( propertyValue ) ) { value = propertyValue.node ? vwf.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 || ! nodePropertyHasSetter.call( vwf, 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 ) { vwf.createProperty( childID, propertyName, value, get, set ); } else { vwf.setProperty( childID, propertyName, value ); } } ); childComponent.methods && Object.keys( childComponent.methods ).forEach( function( methodName ) { var methodValue = childComponent.methods[ methodName ]; if ( valueHasBody( methodValue ) ) { vwf.createMethod( childID, methodName, methodValue.parameters, methodValue.body ); } else { vwf.createMethod( childID, methodName, undefined, methodValue ); } } ); childComponent.events && Object.keys( childComponent.events ).forEach( function( eventName ) { var eventValue = childComponent.events[ eventName ]; if ( valueHasBody( eventValue ) ) { vwf.createEvent( childID, eventName, eventValue.parameters ); vwf.setEvent( childID, eventName, eventValue ); // set the listeners since `createEvent` can't do it yet } else { vwf.createEvent( childID, eventName, undefined ); } } ); // Restore kernel reentry. replicating && vwf.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]; vwf.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 ( valueHasType( script ) ) { if ( script.source ) { 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. vwf.models.kernel.capturingAsyncs( function() { // Suppress kernel reentry so that initialization functions don't make // any changes during replication. replicating && vwf.models.kernel.disable(); // Create each script. scripts.forEach( function( script ) { vwf.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 ) { vwf.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 ( vwf.execute( childID, "Boolean( this.tick )" ) ) { vwf.tickable.nodeIDs.push( childID ); } // Restore kernel reentry. replicating && vwf.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( vwf.prototypes( childID, true ).reverse().concat( childID ), function( childInitializingNodeID, each_callback_async /* err */ ) { // Call initializingNode() on each model. vwf.models.kernel.capturingAsyncs( function() { vwf.models.forEach( function( model ) { // Suppress kernel reentry so that initialization functions // don't make any changes during replication. replicating && vwf.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 && vwf.models.kernel.enable(); } ); }, function() { each_callback_async( undefined ); } ); }, function( err ) /* async */ { // Call initializedNode() on each view. vwf.views.forEach( function( view ) { view.initializedNode && view.initializedNode( nodeID, childID, childPrototypeID, childBehaviorIDs, resolvedSource, childComponent.type, childIndex, childName ); } ); // Mark the node as initialized. nodes.initialize( childID ); // Send the meta event into the application. if ( ! replicating && nodeID !== 0 ) { vwf.fireEvent( nodeID, [ "children", "added" ], [ childIndex, vwf.kutility.nodeReference( childID ) ] ); } // Dismiss the loading spinner if ( childID === vwf.application() ) { var progressbar= document.getElementById( "load-progressbar" ); if (progressbar) { //document.querySelector('body').removeChild(progressbar); progressbar.classList.remove( "visible" ); progressbar.classList.add( "not-visible" ); progressbar.classList.add( "mdc-linear-progress--closed" ); } // var spinner = document.getElementById( "vwf-loading-spinner" ); // spinner && spinner.classList.remove( "pace-active" ); } 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 ) { queue.suspend( "before completing " + childID ); // suspend the queue async.nextTick( function() { callback_async( childID ); queue.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} this.deleteChild = function( 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} this.addChild = function( 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} this.removeChild = function( 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} this.setProperties = function( 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. this.logger.debuggx( "setProperties", nodeID, properties ); var node = 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 ( vwf.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} this.getProperties = function( 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. this.logger.debuggx( "getProperties", nodeID ); var node = 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 ( vwf.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} this.createProperty = function( nodeID, propertyName, propertyValue, propertyGet, propertySet ) { this.logger.debuggx( "createProperty", function() { return [ nodeID, propertyName, JSON.stringify( loggableValue( propertyValue ) ), loggableScript( propertyGet ), loggableScript( propertySet ) ]; } ); var node = 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} this.setProperty = function( nodeID, propertyName, propertyValue ) { this.logger.debuggx( "setProperty", function() { return [ nodeID, propertyName, JSON.stringify( loggableValue( propertyValue ) ) ]; } ); var node = 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; }; this.setProperty.entrants = {}; // maps ( nodeID + '-' + propertyName ) => { index: i, value: v } // -- getProperty -------------------------------------------------------------------------- /// Get a property value for a node. /// /// @name module:vwf.getProperty /// /// @see {@link module:vwf/api/kernel.getProperty} this.getProperty = function( 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; }; this.getProperty.entrants = {}; // maps ( nodeID + '-' + propertyName ) => { index: i, value: v } // -- createMethod ------------------------------------------------------------------------- /// @name module:vwf.createMethod /// /// @see {@link module:vwf/api/kernel.createMethod} this.createMethod = function( nodeID, methodName, methodParameters, methodBody ) { this.logger.debuggx( "createMethod", function() { return [ nodeID, methodName, methodParameters, loggableScript( methodBody ) ]; } ); var node = 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} this.setMethod = function( nodeID, methodName, methodHandler ) { this.logger.debuggx( "setMethod", function() { return [ nodeID, methodName ]; // TODO loggable methodHandler } ); var node = nodes.existing[nodeID]; methodHandler = 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 = require( "vwf/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} this.getMethod = function( nodeID, methodName ) { this.logger.debuggx( "getMethod", function() { return [ nodeID, methodName ]; } ); var node = 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 = require( "vwf/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} this.callMethod = function( nodeID, methodName, methodParameters ) { this.logger.debuggx( "callMethod", function() { return [ nodeID, methodName, JSON.stringify( 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} this.createEvent = function( 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 = nodes.existing[nodeID]; // Encode any namespacing into the name. (Namespaced names were added in 0.6.21.) var encodedEventName = 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} this.setEvent = function( nodeID, eventName, eventDescriptor ) { this.logger.debuggx( "setEvent", function() { return [ nodeID, eventName ]; // TODO: loggable eventDescriptor } ); var node = nodes.existing[nodeID]; // eventDescriptor = normalizedHandler( eventDescriptor ); // TODO // Encode any namespacing into the name. var encodedEventName = 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} this.getEvent = function( nodeID, eventName ) { this.logger.debuggx( "getEvent", function() { return [ nodeID, eventName ]; } ); var node = nodes.existing[nodeID]; // Encode any namespacing into the name. var encodedEventName = 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} this.addEventListener = function( nodeID, eventName, eventHandler, eventContextID, eventPhases ) { this.logger.debuggx( "addEventListener", function() { return [ nodeID, eventName, loggableScript( eventHandler ), eventContextID, eventPhases ]; } ); var node = nodes.existing[nodeID]; // Encode any namespacing into the name. var encodedEventName = 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 = 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} this.removeEventListener = function( nodeID, eventName, eventListenerID ) { this.logger.debuggx( "removeEventListener", function() { return [ nodeID, eventName, loggableScript( eventListenerID ) ]; } ); var node = nodes.existing[nodeID]; // Encode any namespacing into the name. var encodedEventName = 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} this.setEventListener = function( nodeID, eventName, eventListenerID, eventListener ) { this.logger.debuggx( "setEventListener", function() { return [ nodeID, eventName, eventListenerID ]; // TODO: loggable eventListener } ); var node = nodes.existing[nodeID]; // Encode any namespacing into the name. var encodedEventName = namespaceEncodedName( eventName ); // Locate the event in the registry. var event = node.events.existing[ encodedEventName ]; // Normalize the descriptor. eventListener = 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 = require( "vwf/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} this.getEventListener = function( nodeID, eventName, eventListenerID ) { this.logger.debuggx( "getEventListener", function() { return [ nodeID, eventName, eventListenerID ]; } ); // Encode any namespacing into the name. var encodedEventName = 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 = require( "vwf/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} this.flushEventListeners = function( nodeID, eventName, eventContextID ) { this.logger.debuggx( "flushEventListeners", nodeID, eventName, eventContextID ); // Encode any namespacing into the name. var encodedEventName = namespaceEncodedName( eventName ); // Retrieve the event in case we need to remove listeners from it var node = 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} this.fireEvent = function( nodeID, eventName, eventParameters ) { this.logger.debuggx( "fireEvent", function() { return [ nodeID, eventName, JSON.stringify( loggableValues( eventParameters ) ) ]; } ); // Encode any namespacing into the name. (Namespaced names were added in 0.6.21.) var encodedEventName = 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} this.dispatchEvent = function( nodeID, eventName, eventParameters, eventNodeParameters ) { this.logger.debuggx( "dispatchEvent", function() { return [ nodeID, eventName, JSON.stringify( loggableValues( eventParameters ) ), JSON.stringify( 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} this.execute = function( nodeID, scriptText, scriptType, callback_async /* result */ ) { this.logger.debuggx( "execute", function() { return [ nodeID, 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. vwf.models.kernel.capturingAsyncs( function() { vwf.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. vwf.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} this.random = function( nodeID ) { return this.models.object.random( nodeID ); }; // -- seed --------------------------------------------------------------------------------- /// @name module:vwf.seed /// /// @see {@link module:vwf/api/kernel.seed} this.seed = function( 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} this.time = function() { return this.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} this.client = function() { return this.client_; }; // -- moniker ------------------------------------------------------------------------------ /// The identifer the server assigned to this client. /// /// @name module:vwf.moniker /// /// @see {@link module:vwf/api/kernel.moniker} this.moniker = function() { return this.moniker_; }; // -- application -------------------------------------------------------------------------- /// @name module:vwf.application /// /// @see {@link module:vwf/api/kernel.application} this.application = function( initializedOnly ) { var applicationID; Object.keys( nodes.globals ).forEach( function( globalID ) { var global = 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} this.intrinsics = function( nodeID, result ) { return this.models.object.intrinsics( nodeID, result ); }; // -- uri ---------------------------------------------------------------------------------- /// @name module:vwf.uri /// /// @see {@link module:vwf/api/kernel.uri} this.uri = function( 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} this.name = function( nodeID ) { return this.models.object.name( nodeID ); }; // -- prototype ---------------------------------------------------------------------------- /// @name module:vwf.prototype /// /// @see {@link module:vwf/api/kernel.prototype} this.prototype = function( nodeID ) { return this.models.object.prototype( nodeID ); }; // -- prototypes --------------------------------------------------------------------------- /// @name module:vwf.prototypes /// /// @see {@link module:vwf/api/kernel.prototypes} this.prototypes = function( 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} this.behaviors = function( nodeID ) { return this.models.object.behaviors( nodeID ); }; // -- globals ------------------------------------------------------------------------------ /// @name module:vwf.globals /// /// @see {@link module:vwf/api/kernel.globals} this.globals = function( initializedOnly ) { var globals = {}; Object.keys( nodes.globals ).forEach( function( globalID ) { if ( ! initializedOnly || nodes.existing[ globalID ].initialized ) { globals[ globalID ] = undefined; } }, this ); return globals; }; // -- global ------------------------------------------------------------------------------- /// @name module:vwf.global /// /// @see {@link module:vwf/api/kernel.global} this.global = function( globalReference, initializedOnly ) { 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 ( nodes.existing[ globalID ][ field ] === globalReference ) { matchingID = globalID; return true; } } ); return matchingID; } }; // -- root --------------------------------------------------------------------------------- /// @name module:vwf.root /// /// @see {@link module:vwf/api/kernel.root} this.root = function( 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} this.ancestors = function( 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} this.parent = function( nodeID, initializedOnly ) { return this.models.object.parent( nodeID, initializedOnly ); }; // -- children ----------------------------------------------------------------------------- /// @name module:vwf.children /// /// @see {@link module:vwf/api/kernel.children} this.children = function( 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} this.child = function( 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} this.descendants = function( 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} this.sequence = function( 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} this.find = function( 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} this.findClients = function( nodeID, matchPattern, callback /* ( matchID ) */ ) { this.logger.warn( "`kernel.findClients` is deprecated. Use " + "`kernel.find( nodeID, \"doc('http://vwf.example.com/clients.vwf')/pattern\" )`" + " instead." ); var clientsMatchPattern = "doc('http://vwf.example.com/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} this.test = function( nodeID, matchPattern, testID, initializedOnly ) { // Run the query. var matchIDs = find.call( this, nodeID, matchPattern, initializedOnly ); // Search for the test node in the result. return matchIDs.some( function( matchID ) { return matchID == testID; } ); }; // == Private functions ==================================================================== var isSocketIO07 = function() { //return ( parseFloat( io.version ) >= 0.7 ); return true } // -- loadComponent ------------------------------------------------------------------------ /// @name module:vwf~loadComponent var loadComponent = function( nodeURI, baseURI, callback_async /* nodeDescriptor */, errback_async /* errorMessage */ ) { // TODO: turn this into a generic xhr loader exposed as a kernel function? if ( nodeURI == vwf.kutility.protoNodeURI ) { callback_async( vwf.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 { queue.suspend( "while loading " + nodeURI ); // suspend the queue let fetchUrl = remappedURI( require( "vwf/utility" ).resolveURI( nodeURI, baseURI ) ); fetch(fetchUrl, { method: 'get' }).then(function(response) { return response.json(); }).catch(function(err) { // Error :( }).then(function(nodeDescriptor) { callback_async( nodeDescriptor ); queue.resume( "after loading " + nodeURI ); // resume the queue; may invoke dispatch(), so call last before returning to the host }).catch(function(error) { vwf.logger.warnx( "loadComponent", "error loading", nodeURI + ":", error ); errback_async( error ); queue.resume( "after loading " + nodeURI ); // resume the queue; may invoke dispatch(), so call last before returning to the host }) // jQuery.ajax( { // url: remappedURI( require( "vwf/utility" ).resolveURI( nodeURI, baseURI ) ), // dataType: "json", // timeout: vwf.configuration["load-timeout"] * 1000, // success: function( nodeDescriptor ) /* async */ { // callback_async( nodeDescriptor ); // queue.resume( "after loading " + nodeURI ); // resume the queue; may invoke dispatch(), so call last before returning to the host // }, // error: function( xhr, status, error ) /* async */ { // vwf.logger.warnx( "loadComponent", "error loading", nodeURI + ":", error ); // errback_async( error ); // queue.resume( "after loading " + nodeURI ); // resume the queue; may invoke dispatch(), so call last before returning to the host // }, // } ); } }; // -- loadScript --------------------------------------------------------------------------- /// @name module:vwf~loadScript var loadScript = function( scriptURI, baseURI, callback_async /* scriptText */, errback_async /* errorMessage */ ) { 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 { queue.suspend( "while loading " + scriptURI ); // suspend the queue let fetchUrl = remappedURI( require( "vwf/utility" ).resolveURI( scriptURI, baseURI ) ); fetch(fetchUrl, { method: 'get' }).then(function(response) { return response.text(); }).catch(function(err) { // Error :( }).then(function(scriptText) { callback_async( scriptText ); queue.resume( "after loading " + scriptURI ); // resume the queue; may invoke dispatch(), so call last before returning to the host }).catch(function(error) { vwf.logger.warnx( "loadScript", "error loading", scriptURI + ":", error ); errback_async( error ); queue.resume( "after loading " + scriptURI ); // resume the queue; may invoke dispatch(), so call last before returning to the host }) // jQuery.ajax( { // url: remappedURI( require( "vwf/utility" ).resolveURI( scriptURI, baseURI ) ), // dataType: "text", // timeout: vwf.configuration["load-timeout"] * 1000, // success: function( scriptText ) /* async */ { // callback_async( scriptText ); // queue.resume( "after loading " + scriptURI ); // resume the queue; may invoke dispatch(), so call last before returning to the host // }, // error: function( xhr, status, error ) /* async */ { // vwf.logger.warnx( "loadScript", "error loading", scriptURI + ":", error ); // errback_async( error ); // queue.resume( "after loading " + scriptURI ); // resume the queue; may invoke dispatch(), so call last before returning to the host // }, // } ); } }; /// 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} var nodePropertyHasSetter = function( 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} var nodePropertyHasOwnSetter = function( 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} var nodeHasChild = function( 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} var nodeHasOwnChild = function( 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 http://localhost/proxy/vwf.example.com/component.vwf /// /// @name module:vwf~remappedURI var remappedURI = function( uri ) { var match = uri.match( RegExp( "http://(vwf.example.com)/(.*)" ) ); if ( match ) { uri = window.location.protocol + "//" + window.location.host + "/proxy/" + match[1] + "/" + match[2]; } return uri; }; // -- resolvedDescriptor ------------------------------------------------------------------- /// Resolve relative URIs in a component descriptor. /// /// @name module:vwf~resolvedDescriptor var resolvedDescriptor = function( component, baseURI ) { return require( "vwf/utility" ).transform( component, resolvedDescriptorTransformationWithBaseURI ); function resolvedDescriptorTransformationWithBaseURI( object, names, depth ) { return resolvedDescriptorTransformation.call( this, object, names, depth, baseURI ); } }; // -- queueTransitTransformation ----------------------------------------------------------- /// vwf/utility/transform() transformation function to convert the message queue for proper /// JSON serialization. /// /// queue: [ { ..., parameters: [ [ arguments ] ], ... }, { ... }, ... ] /// /// @name module:vwf~queueTransitTransformation var queueTransitTransformation = function( object, names, depth ) { if ( depth == 0 ) { // Omit any private direct messages for this client, then sort by arrival order // (rather than by time) so that messages will retain the same arrival order when // reinserted. return object.filter( function( fields ) { return ! ( fields.origin === "reflector" && fields.sequence > vwf.sequence_ ) && fields.action; // TODO: fields.action is here to filter out tick messages // TODO: don't put ticks on the queue but just use them to fast-forward to the current time (requires removing support for passing ticks to the drivers and nodes) } ).sort( function( fieldsA, fieldsB ) { return fieldsA.sequence - fieldsB.sequence; } ); } else if ( depth == 1 ) { // Remove the sequence fields since they're just local annotations used to keep // messages ordered by insertion order and aren't directly meaniful outside of this // client. var filtered = {}; Object.keys( object ).filter( function( key ) { return key != "sequence"; } ).forEach( function( key ) { filtered[key] = object[key]; } ); return filtered; } return object; }; // -- 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 var loggableComponentTransformation = function( object, names, depth ) { // Get our bearings at the current recusion level. var markers = descriptorMarkers( object, names, depth ); // Transform the object here. switch ( markers.containerName ) { case "extends": // Omit a component descriptor for the prototype. if ( markers.memberIndex == 0 && componentIsDescriptor( object ) ) { return {}; } break; case "implements": // Omit component descriptors for the behaviors. if ( markers.memberIndex == 0 && componentIsDescriptor( object ) ) { return {}; } break; case "properties": // Convert property values to a loggable version, and omit getter and setter // text. if ( markers.memberIndex == 0 && ! valueHasAccessors( object ) || markers.memberIndex == 1 && names[0] == "value" ) { return 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 && ! 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 && componentIsDescriptor( object ) ) { return {}; } break; case "scripts": // Shorten script text. if ( markers.memberIndex == 0 && ! 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 var resolvedDescriptorTransformation = function( object, names, depth, baseURI ) { // Get our bearings at the current recusion level. var markers = descriptorMarkers( object, names, depth ); // Resolve all the URIs. switch ( markers.containerName ) { case "extends": case "implements": case "source": case "children": if ( markers.memberIndex == 0 && componentIsURI( object ) ) { return require( "vwf/utility" ).resolveURI( object, baseURI ); } break; case "scripts": if ( markers.memberIndex == 1 && names[0] == "source" ) { return 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 var descriptorMarkers = function( 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. var find = function( 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 require( "vwf/utility" ).xpath.resolve( matchPattern, rootID, nodeID, resolverWithInitializedOnly, this ); // Wrap `xpathResolver` to pass `initializedOnly` through. function resolverWithInitializedOnly( step, contextID, resolveAttributes ) { return 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[]} var xpathResolver = function( step, contextID, resolveAttributes, initializedOnly ) { 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 xpathNodeMatchesStep.call( this, resultID, step.name ); } else { return 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] != "@" && xpathNodeMatchesStep.call( this, resultID, step.name, step.type ); // TODO: @? }, this ); break; // Attribute test. case "attribute": resultIDs = resultIDs.filter( function( resultID ) { return resultID[0] == "@" && 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} var xpathNodeMatchesStep = function( 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} var xpathPropertyMatchesStep = function( 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. var mergeDescriptors = function( 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. var enumerable = function( 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. var writable = function( 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. var configurable = function( value ) { return { value: value, enumerable: true, writable: true, configurable: true, }; }; // == Private variables ==================================================================== /// Prototype for name-based, unordered collections in the node registry, including /// `node.properties`, `node.methods`, and `node.events`. var 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, 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`. var 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. var eventCollectionPrototype = Object.create( keyedCollectionPrototype, { create: { value: function( name, changes, parameters ) { var value = parameters ? { parameters: parameters.slice(), // clone } : {}; value.listeners = Object.create( indexedCollectionPrototype, { container: enumerable( this ), containerMember: enumerable( name ), } ); return 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. var 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] ) { var self = this; var prototypeNode = behaviorIDs.reduce( function( prototypeNode, behaviorID ) { return self.proxy( prototypeNode, self.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( keyedCollectionPrototype, { existing: enumerable( Object.create( prototypeNode ? prototypeNode.properties.existing : null ) ), } ), methods: Object.create( keyedCollectionPrototype, { existing: enumerable( Object.create( prototypeNode ? prototypeNode.methods.existing : null ) ), } ), events: Object.create( eventCollectionPrototype, { existing: 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, }; // } 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 ) { if ( this.existing[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, }, }; /// Control messages from the reflector are stored here in a priority queue, ordered by /// execution time. /// /// @name module:vwf~queue var queue = this.private.queue = { /// Insert a message or messages into the queue. Optionally execute the simulation /// through the time marked on the message. /// /// When chronic (chron-ic) is set, vwf#dispatch is called to execute the simulation up /// through the indicated time. To prevent actions from executing out of order, insert /// should be the caller's last operation before returning to the host when invoked with /// chronic. /// /// @name module:vwf~queue.insert /// /// @param {Object|Object[]} fields /// @param {Boolean} [chronic] insert: function( fields, chronic ) { var messages = fields instanceof Array ? fields : [ fields ]; messages.forEach( function( fields ) { // if ( fields.action ) { // TODO: don't put ticks on the queue but just use them to fast-forward to the current time (requires removing support for passing ticks to the drivers and nodes) fields.sequence = ++this.sequence; // track the insertion order for use as a sort key this.queue.push( fields ); // } if ( chronic ) { this.time = Math.max( this.time, fields.time ); // save the latest allowed time for suspend/resume } }, this ); // Sort by time, then future messages ahead of reflector messages, then by sequence. // TODO: we probably want a priority queue here for better performance // // The sort by origin ensures that the queue is processed in a well-defined order // when future messages and reflector messages share the same time, even if the // reflector message has not arrived at the client yet. // // The sort by sequence number ensures that the messages remain in their arrival // order when the earlier sort keys don't provide the order. this.queue.sort( function( a, b ) { if ( a.time != b.time ) { return a.time - b.time; } else if ( a.origin != "reflector" && b.origin == "reflector" ) { return -1; } else if ( a.origin == "reflector" && b.origin != "reflector" ) { return 1; } else { return a.sequence - b.sequence; } } ); // Execute the simulation through the new time. // To prevent actions from executing out of order, callers should immediately return // to the host after invoking insert with chronic set. if ( chronic ) { vwf.dispatch(); } }, /// Pull the next message from the queue. /// /// @name module:vwf~queue.pull /// /// @returns {Object|undefined} The next message if available, otherwise undefined. pull: function() { if ( this.suspension == 0 && this.queue.length > 0 && this.queue[0].time <= this.time ) { return this.queue.shift(); } }, /// Update the queue to include only the messages selected by a filtering function. /// /// @name module:vwf~queue.filter /// /// @param {Function} callback /// `filter` calls `callback( fields )` once for each message in the queue. If /// `callback` returns a truthy value, the message will be retained. Otherwise it will /// be removed from the queue. filter: function( callback /* fields */ ) { this.queue = this.queue.filter( callback ); }, /// Suspend message execution. /// /// @name module:vwf~queue.suspend /// /// @returns {Boolean} true if the queue was suspended by this call. suspend: function( why ) { if ( this.suspension++ == 0 ) { vwf.logger.infox( "-queue#suspend", "suspending queue at time", vwf.now, why ? why : "" ); return true; } else { vwf.logger.debugx( "-queue#suspend", "further suspending queue at time", vwf.now, why ? why : "" ); return false; } }, /// Resume message execution. /// /// vwf#dispatch may be called to continue the simulation. To prevent actions from /// executing out of order, resume should be the caller's last operation before /// returning to the host. /// /// @name module:vwf~queue.resume /// /// @returns {Boolean} true if the queue was resumed by this call. resume: function( why ) { if ( --this.suspension == 0 ) { vwf.logger.infox( "-queue#resume", "resuming queue at time", vwf.now, why ? why : "" ); vwf.dispatch(); return true; } else { vwf.logger.debugx( "-queue#resume", "partially resuming queue at time", vwf.now, why ? why : "" ); return false; } }, /// Return the ready state of the queue. /// /// @name module:vwf~queue.ready /// /// @returns {Boolean} ready: function() { return this.suspension == 0; }, /// Current time as provided by the reflector. Messages to be executed at this time or /// earlier are available from #pull. /// /// @name module:vwf~queue.time time: 0, /// Suspension count. Queue processing is suspended when suspension is greater than 0. /// /// @name module:vwf~queue.suspension suspension: 0, /// Sequence counter for tagging messages by order of arrival. Messages are sorted by /// time, origin, then by arrival order. /// /// @name module:vwf~queue.sequence sequence: 0, /// Array containing the messages in the queue. /// /// @name module:vwf~queue.queue queue: [], }; }; } ) ( window );