src/common/channel.js (134 lines of code) (raw):

/* * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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. * */ var utils = require('cordova/utils'); var nextGuid = 1; /** * Custom pub-sub "channel" that can have functions subscribed to it * This object is used to define and control firing of events for * cordova initialization, as well as for custom events thereafter. * * The order of events during page load and Cordova startup is as follows: * * onDOMContentLoaded* Internal event that is received when the web page is loaded and parsed. * onNativeReady* Internal event that indicates the Cordova native side is ready. * onCordovaReady* Internal event fired when all Cordova JavaScript objects have been created. * onDeviceReady* User event fired to indicate that Cordova is ready * onResume User event fired to indicate a start/resume lifecycle event * onPause User event fired to indicate a pause lifecycle event * * The events marked with an * are sticky. Once they have fired, they will stay in the fired state. * All listeners that subscribe after the event is fired will be executed right away. * * The only Cordova events that user code should register for are: * deviceready Cordova native code is initialized and Cordova APIs can be called from JavaScript * pause App has moved to background * resume App has returned to foreground * * Listeners can be registered as: * document.addEventListener("deviceready", myDeviceReadyListener, false); * document.addEventListener("resume", myResumeListener, false); * document.addEventListener("pause", myPauseListener, false); * * The DOM lifecycle events should be used for saving and restoring state * window.onload * window.onunload * */ /** * Channel * @constructor * @param type String the channel name */ var Channel = function (type, sticky) { this.type = type; // Map of guid -> function. this.handlers = {}; // 0 = Non-sticky, 1 = Sticky non-fired, 2 = Sticky fired. this.state = sticky ? 1 : 0; // Used in sticky mode to remember args passed to fire(). this.fireArgs = null; // Used by onHasSubscribersChange to know if there are any listeners. this.numHandlers = 0; // Function that is called when the first listener is subscribed, or when // the last listener is unsubscribed. this.onHasSubscribersChange = null; }; var channel = { /** * Calls the provided function only after all of the channels specified * have been fired. All channels must be sticky channels. */ join: function (h, c) { var len = c.length; var i = len; var f = function () { if (!(--i)) h(); }; for (var j = 0; j < len; j++) { if (c[j].state === 0) { throw Error('Can only use join with sticky channels.'); } c[j].subscribe(f); } if (!len) h(); }, create: function (type) { return (channel[type] = new Channel(type, false)); }, createSticky: function (type) { return (channel[type] = new Channel(type, true)); }, /** * cordova Channels that must fire before "deviceready" is fired. */ deviceReadyChannelsArray: [], deviceReadyChannelsMap: {}, /** * Indicate that a feature needs to be initialized before it is ready to be used. * This holds up Cordova's "deviceready" event until the feature has been initialized * and Cordova.initComplete(feature) is called. * * @param feature {String} The unique feature name */ waitForInitialization: function (feature) { if (feature) { var c = channel[feature] || this.createSticky(feature); this.deviceReadyChannelsMap[feature] = c; this.deviceReadyChannelsArray.push(c); } }, /** * Indicate that initialization code has completed and the feature is ready to be used. * * @param feature {String} The unique feature name */ initializationComplete: function (feature) { var c = this.deviceReadyChannelsMap[feature]; if (c) { c.fire(); } } }; function checkSubscriptionArgument (argument) { if (typeof argument !== 'function' && typeof argument.handleEvent !== 'function') { throw new Error( 'Must provide a function or an EventListener object ' + 'implementing the handleEvent interface.' ); } } /** * Subscribes the given function to the channel. Any time that * Channel.fire is called so too will the function. * Optionally specify an execution context for the function * and a guid that can be used to stop subscribing to the channel. * Returns the guid. */ Channel.prototype.subscribe = function (eventListenerOrFunction, eventListener) { checkSubscriptionArgument(eventListenerOrFunction); var handleEvent, guid; if (eventListenerOrFunction && typeof eventListenerOrFunction === 'object') { // Received an EventListener object implementing the handleEvent interface handleEvent = eventListenerOrFunction.handleEvent; eventListener = eventListenerOrFunction; } else { // Received a function to handle event handleEvent = eventListenerOrFunction; } if (this.state === 2) { handleEvent.apply(eventListener || this, this.fireArgs); return; } guid = eventListenerOrFunction.observer_guid; if (typeof eventListener === 'object') { handleEvent = utils.close(eventListener, handleEvent); } if (!guid) { // First time any channel has seen this subscriber guid = '' + nextGuid++; } handleEvent.observer_guid = guid; eventListenerOrFunction.observer_guid = guid; // Don't add the same handler more than once. if (!this.handlers[guid]) { this.handlers[guid] = handleEvent; this.numHandlers++; if (this.numHandlers === 1) { this.onHasSubscribersChange && this.onHasSubscribersChange(); } } }; /** * Unsubscribes the function with the given guid from the channel. */ Channel.prototype.unsubscribe = function (eventListenerOrFunction) { checkSubscriptionArgument(eventListenerOrFunction); var handleEvent, guid, handler; if (eventListenerOrFunction && typeof eventListenerOrFunction === 'object') { // Received an EventListener object implementing the handleEvent interface handleEvent = eventListenerOrFunction.handleEvent; } else { // Received a function to handle event handleEvent = eventListenerOrFunction; } guid = handleEvent.observer_guid; handler = this.handlers[guid]; if (handler) { delete this.handlers[guid]; this.numHandlers--; if (this.numHandlers === 0) { this.onHasSubscribersChange && this.onHasSubscribersChange(); } } }; /** * Calls all functions subscribed to this channel. */ Channel.prototype.fire = function (e) { var fireArgs = Array.prototype.slice.call(arguments); // Apply stickiness. if (this.state === 1) { this.state = 2; this.fireArgs = fireArgs; } if (this.numHandlers) { // Copy the values first so that it is safe to modify it from within // callbacks. var toCall = []; for (var item in this.handlers) { toCall.push(this.handlers[item]); } for (var i = 0; i < toCall.length; ++i) { toCall[i].apply(this, fireArgs); } if (this.state === 2 && this.numHandlers) { this.numHandlers = 0; this.handlers = {}; this.onHasSubscribersChange && this.onHasSubscribersChange(); } } }; // defining them here so they are ready super fast! // DOM event that is received when the web page is loaded and parsed. channel.createSticky('onDOMContentLoaded'); // Event to indicate the Cordova native side is ready. channel.createSticky('onNativeReady'); // Event to indicate that all Cordova JavaScript objects have been created // and it's time to run plugin constructors. channel.createSticky('onCordovaReady'); // Event to indicate that all automatically loaded JS plugins are loaded and ready. // FIXME remove this channel.createSticky('onPluginsReady'); // Event to indicate that Cordova is ready channel.createSticky('onDeviceReady'); // Event to indicate a resume lifecycle event channel.create('onResume'); // Event to indicate a pause lifecycle event channel.create('onPause'); // Channels that must fire before "deviceready" is fired. channel.waitForInitialization('onCordovaReady'); channel.waitForInitialization('onDOMContentLoaded'); module.exports = channel;