/** * Ext Direct aims to streamline communication between the client and server * by providing a single interface that reduces the amount of common code * typically required to validate data and handle returned data packets * (reading data, error conditions, etc). * * The `Ext.direct` namespace includes several classes for a closer integration * with the server-side. The Ext.data namespace also includes classes for working * with Ext.data.Stores which are backed by data from an Ext Direct method. * * # Specification * * For additional information consult the [Ext Direct Specification][1]. * * # Providers * * Ext Direct uses a provider architecture, where one or more providers are used * to transport data to and from the server. There are several providers that exist * in the core at the moment: * * - {@link Ext.direct.JsonProvider JsonProvider} for simple JSON operations * - {@link Ext.direct.PollingProvider PollingProvider} for repeated requests * - {@link Ext.direct.RemotingProvider RemotingProvider} exposes server side to the client. * * A provider does not need to be invoked directly, providers are added via * {@link Ext.direct.Manager #addProvider}. RemotingProviders' API declarations * can also be loaded with {@link Ext.direct.Manager #loadProvider}, with * Provider instance created automatically after successful retrieval. * * # Router * * Ext Direct RemotingProviders utilize a "router" on the server to direct * requests from the client to the appropriate server-side method. Because * the Ext Direct API is platform-agnostic, you could completely swap out * a Java based server solution and replace it with one that uses C# * without changing the client side JavaScript at all, or vice versa. * * # Server side events * * Custom events from the server may be handled by the client by adding listeners, for example: * * {"type":"event","name":"message","data":"Successfully polled at: 11:19:30 am"} * * // add a handler for a 'message' event sent by the server * Ext.direct.Manager.on('message', function(e){ * out.append(String.format('<p><i>{0}</i></p>', e.data)); * out.el.scrollTo('t', 100000, true); * }); * * [1]: http://sencha.com/products/extjs/extdirect * * @singleton * @alternateClassName Ext.Direct */Ext.define('Ext.direct.Manager', { singleton: true, requires: [ 'Ext.util.MixedCollection' ], mixins: [ 'Ext.mixin.Observable' ], /** * Exception types. */ exceptions: { TRANSPORT: 'xhr', PARSE: 'parse', DATA: 'data', LOGIN: 'login', SERVER: 'exception' }, // Classes of Providers available to the application providerClasses: {}, // Remoting Methods registered with the Manager remotingMethods: {}, config: { /** * @cfg {String} [varName="Ext.REMOTING_API"] * Default variable name to use for Ext Direct API declaration. */ varName: 'Ext.REMOTING_API' }, apiNotFoundError: 'Ext Direct API was not found at {0}', /** * @event event * * Fires after an event. * * @param {Ext.direct.Event} event The {@link Ext.direct.Event Event} that occurred. * @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider} * that provided the event. */ /** * @event exception * * Fires after an event exception. * * @param {Ext.direct.Event} event The {@link Ext.direct.Event Event} that occurred. * @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider} * that provided the event. */ /** * @event providerload * * Fired by {@link #loadProvider} after successfully loading RemotingProvider API * declaration and creating a new Provider instance. * * @param {String} url The URL used to retrieve remoting API. * @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider} * instance that was created. */ /** * @event providerloaderror * * Fired by {@link #loadProvider} when remoting API could not be loaded, or * Provider instance could not be created. * * @param {String} url The URL used to retrieve remoting API. * @param {String} error The error that occured. */ /** * @private */ constructor: function() { var me = this; me.mixins.observable.constructor.call(me); me.transactions = new Ext.util.MixedCollection(); me.providers = new Ext.util.MixedCollection(); }, /** * Adds an Ext Direct Provider and creates the proxy or stub methods to execute * server-side methods for RemotingProviders. If the provider is not already connected, * it will auto-connect. * * var pollProv = new Ext.direct.PollingProvider({ * id: 'polling1', * url: 'php/poll2.php' * }); * * Ext.direct.Manager.addProvider({ * id: 'remoting1', * type: 'remoting', // create a Ext.direct.RemotingProvider * url: 'php/router.php', // url to connect to the Ext Direct server-side router. * actions: { // each property in actions object represents an Action * TestAction: [{ // array of Methods within each server side Action * name: 'doEcho', // name of method * len: 1 * }, { * name: 'multiply', * len: 1 * }, { * name: 'doForm', * formHandler: true // handle form on server with Ext.direct.Transaction * }] * }, * namespace: 'myApplication', // namespace to create the Remoting Provider in * }, { * id: 'polling2', * type: 'polling', // create an Ext.direct.PollingProvider * url: 'php/poll.php' * }, * pollProv); // reference to previously created instance * * @param {Ext.direct.Provider/Object...} provider * * Accepts any number of Provider descriptions (an instance or config object for * a Provider). Each Provider description instructs Ext Direct how to create * client-side stub methods. */ addProvider: function(provider) { var me = this, args = arguments, relayers = me.relayers || (me.relayers = {}), i, len; if (args.length > 1) { for (i = 0, len = args.length; i < len; ++i) { me.addProvider(args[i]); } return; } // if provider has not already been instantiated if (!provider.isProvider) { provider = Ext.create('direct.' + provider.type + 'provider', provider); } me.providers.add(provider); provider.on('data', me.onProviderData, me); if (provider.relayedEvents) { relayers[provider.id] = me.relayEvents(provider, provider.relayedEvents); } if (!provider.isConnected()) { provider.connect(); } return provider; }, /** * Load Ext Direct Provider API declaration from the server and construct * a new Provider instance. The new Provider will then auto-connect and * create stub functions for the methods exposed by the server side. See * {@link #addProvider}. * * Ext.direct.Manager.loadProvider({ * url: 'php/api.php', * varName: 'MY_REMOTING_API' // defaults to 'Ext.REMOTING_API' * }); * * @param {Object} config Remoting API configuration. * @param {String} config.url URL to retrieve remoting API declaration from. * @param {String} config.varName Name of the variable that will hold * RemotingProvider configuration block, including its Actions. * @param {Function} [callback] Optional callback to execute when * Provider is created, or when an error has occured. * @param {Object} [scope] Optional scope to execute callback function in. * * For additional information see the [Ext Direct specification][1]. */ loadProvider: function(config, callback, scope) { var me = this, classes = me.providerClasses, type, url, varName, provider, i, len; if (Ext.isArray(config)) { for (i = 0, len = config.length; i < len; i++) { me.loadProvider(config[i], callback, scope); } return; } // We may have been passed config object containing enough // information to create a Provider without further ado. type = config.type; url = config.url; if (classes[type] && classes[type].checkConfig(config)) { provider = me.addProvider(config); me.fireEventArgs('providerload', [url, provider]); Ext.callback(callback, scope, [url, provider]); // We're deliberately not returning the provider here // to make way for the future Promises based implementation // that should be consistent with the remote API declaration // retrieval below. return; } // For remote API declaration retrieval we need to know the // service discovery URL and variable name, at the minimum. // We have a default for the variable name but not for URL. varName = config.varName || me.getVarName(); delete config.varName; //<debug> if (!url) { Ext.raise("Need API discovery URL to load a Remoting provider!"); } //</debug> // The URL we are requesting API from is not the same as the // service URL, and we don't need them to mix. delete config.url; // Have to use closures here as Loader does not allow passing // options object from caller to callback. Ext.Loader.loadScript({ url: url, scope: me, onLoad: function() { this.onApiLoadSuccess({ url: url, varName: varName, config: config, callback: callback, scope: scope }); }, onError: function() { this.onApiLoadFailure({ url: url, callback: callback, scope: scope }); } }); }, /** * Retrieves a {@link Ext.direct.Provider provider} by the id specified when the * provider is added. * * @param {String/Ext.direct.Provider} id The id of the provider, or the provider instance. */ getProvider: function(id) { return id.isProvider ? id : this.providers.get(id); }, /** * Removes the provider. * * @param {String/Ext.direct.Provider} provider The provider instance or the id of the provider. * * @return {Ext.direct.Provider} The provider, null if not found. */ removeProvider: function(provider) { var me = this, providers = me.providers, relayers = me.relayers, id; provider = provider.isProvider ? provider : providers.get(provider); if (provider) { provider.un('data', me.onProviderData, me); id = provider.id; if (relayers[id]) { relayers[id].destroy(); delete relayers[id]; } providers.remove(provider); return provider; } return null; }, /** * Adds a transaction to the manager. * * @param {Ext.direct.Transaction} transaction The transaction to add * * @return {Ext.direct.Transaction} transaction * * @private */ addTransaction: function(transaction) { this.transactions.add(transaction); return transaction; }, /** * Removes a transaction from the manager. * * @param {String/Ext.direct.Transaction} transaction The transaction/id of transaction * to remove * * @return {Ext.direct.Transaction} transaction * * @private */ removeTransaction: function(transaction) { var me = this; transaction = me.getTransaction(transaction); me.transactions.remove(transaction); return transaction; }, /** * Gets a transaction * * @param {String/Ext.direct.Transaction} transaction The transaction/id of transaction to get * * @return {Ext.direct.Transaction} * * @private */ getTransaction: function(transaction) { return typeof transaction === 'object' ? transaction : this.transactions.get(transaction); }, onProviderData: function(provider, event) { var me = this, i, len; if (Ext.isArray(event)) { for (i = 0, len = event.length; i < len; ++i) { me.onProviderData(provider, event[i]); } return; } if (event.name && event.name !== 'event' && event.name !== 'exception') { me.fireEvent(event.name, event); } else if (event.status === false) { me.fireEvent('exception', event); } me.fireEvent('event', event, provider); }, /** * Parses a direct function. It may be passed in a string format, for example: * "MyApp.Person.read". * * @param {String/Function} fn The direct function * * @return {Function} The function to use in the direct call. Null if not found */ parseMethod: function(fn) { var current = Ext.global, i = 0, resolved, parts, len; if (Ext.isFunction(fn)) { resolved = fn; } else if (Ext.isString(fn)) { resolved = this.remotingMethods[fn]; // Support legacy resolution as top-down lookup // from the window scope if (!resolved) { parts = fn.split('.'); len = parts.length; while (current && i < len) { current = current[parts[i]]; ++i; } resolved = Ext.isFunction(current) ? current : null; } } return resolved || null; }, /** * @private * Iterate over an API object containing function names and resolve Direct functions. * * @param {Object} api API object * @param {Ext.Base} caller The object calling * * @return {Object} The API object with each property resolved to a Direct function. */ resolveApi: function(api, caller) { var prefix, action, method, fullName, fn; prefix = api && api.prefix; if (prefix && prefix.substr(prefix.length - 1) !== '.') { prefix += '.'; } for (action in api) { method = api[action]; if (action !== 'prefix' && typeof method !== 'function') { fullName = (prefix || '') + method; fn = this.parseMethod(fullName); if (typeof fn === 'function') { api[action] = fn; } //<debug> else { Ext.raise("Cannot resolve Direct API method '" + fullName + "' for " + action + " action in " + caller.$className + " instance with id: " + (caller.id != null ? caller.id : 'unknown')); } //</debug> } } return api; }, privates: { addProviderClass: function(type, cls) { this.providerClasses[type] = cls; }, onApiLoadSuccess: function(options) { var me = this, url = options.url, varName = options.varName, api, provider, error; try { // Variable name could be nested (default is Ext.REMOTING_API), // so we use eval() to get the actual value. api = Ext.apply(options.config, eval(varName)); provider = me.addProvider(api); } catch (e) { error = e + ''; } if (error) { me.fireEventArgs('providerloaderror', [url, error]); Ext.callback(options.callback, options.scope, [url, error]); } else { me.fireEventArgs('providerload', [url, provider]); Ext.callback(options.callback, options.scope, [url, provider]); } }, onApiLoadFailure: function(options) { var url = options.url, error; error = Ext.String.format(this.apiNotFoundError, url); this.fireEventArgs('providerloaderror', [url, error]); Ext.callback(options.callback, options.scope, [url, error]); }, registerMethod: function(name, method) { this.remotingMethods[name] = method; }, // Used for testing clearAllMethods: function() { this.remotingMethods = {}; } }}, function() { // Backwards compatibility Ext.Direct = Ext.direct.Manager;});