/** * @class Ext.direct.RemotingProvider * * The {@link Ext.direct.RemotingProvider RemotingProvider} exposes access to * server side methods on the client (a remote procedure call (RPC) type of * connection where the client can initiate a procedure on the server). * * This allows for code to be organized in a fashion that is maintainable, * while providing a clear path between client and server, something that is * not always apparent when using URLs. * * To accomplish this the server-side needs to describe what classes and methods * are available on the client-side. This configuration will typically be * outputted by the server-side Ext.Direct stack when the API description is built. */ Ext.define('Ext.direct.RemotingProvider', { alias: 'direct.remotingprovider', extend: 'Ext.direct.JsonProvider', requires: [ 'Ext.util.MixedCollection', 'Ext.util.DelayedTask', 'Ext.direct.Transaction', 'Ext.direct.RemotingMethod' ], config: { /** * @cfg {String/Object} namespace * Namespace for the Remoting Provider (defaults to the browser global scope of _window_). * Explicitly specify the namespace Object, or specify a String to have a * {@link Ext#namespace namespace created} implicitly. */ namespace: undefined, /** * @cfg {String} url (required) The url to connect to the {@link Ext.direct.Manager} server-side router. */ url: null, /** * @cfg {String} enableUrlEncode * Specify which param will hold the arguments for the method. */ enableUrlEncode: null, /** * @cfg {Number/Boolean} enableBuffer * * `true` or `false` to enable or disable combining of method * calls. If a number is specified this is the amount of time in milliseconds * to wait before sending a batched request. * * Calls which are received within the specified timeframe will be * concatenated together and sent in a single request, optimizing the * application by reducing the amount of round trips that have to be made * to the server. */ enableBuffer: 10, /** * @cfg {Number} maxRetries * Number of times to re-attempt delivery on failure of a call. */ maxRetries: 1, /** * @cfg {Number} timeout * The timeout to use for each request. */ timeout: undefined, /** * @cfg {Object} actions * Object literal defining the server side actions and methods. For example, if * the Provider is configured with: * * actions: { // each property within the 'actions' object represents a server side Class * // array of methods within each server side Class to be stubbed out on client * TestAction: [{ * name: "doEcho", * len: 1 * }, { * "name": "multiply", // name of method * "len": 2 // The number of parameters that will be used to create an * // array of data to send to the server side function. * // Ensure the server sends back a Number, not a String. * }, { * name: "doForm", * formHandler: true, // direct the client to use specialized form handling method * len: 1 * }] * } * * __Note:__ A Store is not required, a server method can be called at any time. * In the following example a **client side** handler is used to call the * server side method "multiply" in the server-side "TestAction" Class: * * TestAction.multiply( * 2, 4, // pass two arguments to server, so specify len=2 * // callback function after the server is called * // result: the result returned by the server * // e: Ext.direct.RemotingEvent object * function(result, e) { * var t = e.getTransaction(); * var action = t.action; // server side Class called * var method = t.method; // server side method called * if (e.getStatus()) { * var answer = Ext.encode(result); // 8 * } else { * var msg = e.getMessage(); // failure message * } * } * ); * * In the example above, the server side "multiply" function will be passed two * arguments (2 and 4). The "multiply" method should return the value 8 which will be * available as the `result` in the example above. */ actions: {} }, /** * @event beforecall * Fires immediately before the client-side sends off the RPC call. * By returning `false` from an event handler you can prevent the call from * executing. * @param {Ext.direct.RemotingProvider} provider * @param {Ext.direct.Transaction} transaction * @param {Object} meta The meta data. */ /** * @event call * Fires immediately after the request to the server-side is sent. This does * NOT fire after the response has come back from the call. * @param {Ext.direct.RemotingProvider} provider * @param {Ext.direct.Transaction} transaction * @param {Object} meta The meta data. */ constructor : function(config) { var me = this; me.callParent(arguments); me.transactions = Ext.create('Ext.util.Collection', function(item) { return item.getId(); }); me.callBuffer = []; }, applyNamespace: function(namespace) { if (Ext.isString(namespace)) { return Ext.ns(namespace); } return namespace || window; }, /** * Initialize the API * @private */ initAPI : function() { var actions = this.getActions(), namespace = this.getNamespace(), action, cls, methods, i, ln, method; for (action in actions) { if (actions.hasOwnProperty(action)) { cls = namespace[action]; if (!cls) { cls = namespace[action] = {}; } methods = actions[action]; for (i = 0, ln = methods.length; i < ln; ++i) { method = Ext.create('Ext.direct.RemotingMethod', methods[i]); cls[method.getName()] = this.createHandler(action, method); } } } }, /** * Create a handler function for a direct call. * @private * @param {String} action The action the call is for. * @param {Object} method The details of the method. * @return {Function} A JavaScript function that will kick off the call. */ createHandler : function(action, method) { var me = this, handler; if (!method.getFormHandler()) { handler = function() { me.configureRequest(action, method, Array.prototype.slice.call(arguments, 0)); }; } else { handler = function(form, callback, scope) { me.configureFormRequest(action, method, form, callback, scope); }; } handler.directCfg = { action: action, method: method }; return handler; }, // @inheritdoc isConnected: function() { return !!this.connected; }, // @inheritdoc connect: function() { var me = this; if (me.getUrl()) { me.initAPI(); me.connected = true; me.fireEvent('connect', me); } else { //<debug> Ext.Error.raise('Error initializing RemotingProvider, no url configured.'); //</debug> } }, // @inheritdoc disconnect: function() { var me = this; if (me.connected) { me.connected = false; me.fireEvent('disconnect', me); } }, /** * Run any callbacks related to the transaction. * @private * @param {Ext.direct.Transaction} transaction The transaction * @param {Ext.direct.Event} event The event */ runCallback: function(transaction, event) { var success = !!event.getStatus(), functionName = success ? 'success' : 'failure', callback = transaction && transaction.getCallback(), result; if (callback) { // this doesnt make any sense. why do we have both result and data? // result = Ext.isDefined(event.getResult()) ? event.result : event.data; result = event.getResult(); if (Ext.isFunction(callback)) { callback(result, event, success); } else { Ext.callback(callback[functionName], callback.scope, [result, event, success]); Ext.callback(callback.callback, callback.scope, [result, event, success]); } } }, /** * React to the AJAX request being completed. * @private */ onData: function(options, success, response) { var me = this, i = 0, ln, events, event, transaction, transactions; if (success) { events = me.createEvents(response); for (ln = events.length; i < ln; ++i) { event = events[i]; transaction = me.getTransaction(event); me.fireEvent('data', me, event); if (transaction) { me.runCallback(transaction, event, true); Ext.direct.Manager.removeTransaction(transaction); } } } else { transactions = [].concat(options.transaction); for (ln = transactions.length; i < ln; ++i) { transaction = me.getTransaction(transactions[i]); if (transaction && transaction.getRetryCount() < me.getMaxRetries()) { transaction.retry(); } else { event = Ext.create('Ext.direct.ExceptionEvent', { data: null, transaction: transaction, code: Ext.direct.Manager.exceptions.TRANSPORT, message: 'Unable to connect to the server.', xhr: response }); me.fireEvent('data', me, event); if (transaction) { me.runCallback(transaction, event, false); Ext.direct.Manager.removeTransaction(transaction); } } } } }, /** * Get transaction from XHR options. * @private * @param {Object} options The options sent to the AJAX request. * @return {Ext.direct.Transaction/null} The transaction, `null` if not found. */ getTransaction: function(options) { return options && options.getTid ? Ext.direct.Manager.getTransaction(options.getTid()) : null; }, /** * Configure a direct request. * @private * @param {String} action The action being executed. * @param {Object} method The method being executed. * @param {Array} args */ configureRequest: function(action, method, args) { var me = this, callData = method.getCallData(args), data = callData.data, callback = callData.callback, scope = callData.scope, transaction; transaction = Ext.create('Ext.direct.Transaction', { provider: me, args: args, action: action, method: method.getName(), data: data, callback: scope && Ext.isFunction(callback) ? Ext.Function.bind(callback, scope) : callback }); if (me.fireEvent('beforecall', me, transaction, method) !== false) { Ext.direct.Manager.addTransaction(transaction); me.queueTransaction(transaction); me.fireEvent('call', me, transaction, method); } }, /** * Gets the AJAX call info for a transaction. * @private * @param {Ext.direct.Transaction} transaction The transaction. * @return {Object} The call params. */ getCallData: function(transaction) { return { action: transaction.getAction(), method: transaction.getMethod(), data: transaction.getData(), type: 'rpc', tid: transaction.getId() }; }, /** * Sends a request to the server. * @private * @param {Object/Array} data The data to send. */ sendRequest : function(data) { var me = this, request = { url: me.getUrl(), callback: me.onData, scope: me, transaction: data, timeout: me.getTimeout() }, callData, enableUrlEncode = me.getEnableUrlEncode(), i = 0, ln, params; if (Ext.isArray(data)) { callData = []; for (ln = data.length; i < ln; ++i) { callData.push(me.getCallData(data[i])); } } else { callData = me.getCallData(data); } if (enableUrlEncode) { params = {}; params[Ext.isString(enableUrlEncode) ? enableUrlEncode : 'data'] = Ext.encode(callData); request.params = params; } else { request.jsonData = callData; } Ext.Ajax.request(request); }, /** * Add a new transaction to the queue. * @private * @param {Ext.direct.Transaction} transaction The transaction. */ queueTransaction: function(transaction) { var me = this, enableBuffer = me.getEnableBuffer(); if (transaction.getForm()) { me.sendFormRequest(transaction); return; } me.callBuffer.push(transaction); if (enableBuffer) { if (!me.callTask) { me.callTask = Ext.create('Ext.util.DelayedTask', me.combineAndSend, me); } me.callTask.delay(Ext.isNumber(enableBuffer) ? enableBuffer : 10); } else { me.combineAndSend(); } }, /** * Combine any buffered requests and send them off. * @private */ combineAndSend : function() { var buffer = this.callBuffer, ln = buffer.length; if (ln > 0) { this.sendRequest(ln == 1 ? buffer[0] : buffer); this.callBuffer = []; } }, /** * Configure a form submission request * * @param {String} action The action being executed * @param {Object} method The method being executed * @param {HTMLElement} form The form being submitted * @param {Function} [callback] A callback to run after the form submits * @param {Object} [scope] A scope to execute the callback in * * @private */ configureFormRequest : function(action, method, form, callback, scope) { var me = this, transaction, isUpload, params; transaction = new Ext.direct.Transaction({ provider: me, action: action, method: method.getName(), args: [form, callback, scope], callback: scope && Ext.isFunction(callback) ? Ext.Function.bind(callback, scope) : callback, isForm: true }); if (me.fireEvent('beforecall', me, transaction, method) !== false) { Ext.direct.Manager.addTransaction(transaction); isUpload = String(form.getAttribute('enctype')).toLowerCase() == 'multipart/form-data'; params = { extTID: transaction.id, extAction: action, extMethod: method.getName(), extType: 'rpc', extUpload: String(isUpload) }; // change made from typeof callback check to callback.params // to support addl param passing in DirectSubmit EAC 6/2 Ext.apply(transaction, { form: Ext.getDom(form), isUpload: isUpload, params: callback && Ext.isObject(callback.params) ? Ext.apply(params, callback.params) : params }); me.fireEvent('call', me, transaction, method); me.sendFormRequest(transaction); } }, /** * Sends a form request * * @param {Ext.direct.Transaction} transaction The transaction to send * * @private */ sendFormRequest : function(transaction) { var me = this; Ext.Ajax.request({ url: me.getUrl(), params: transaction.params, callback: me.onData, scope: me, form: transaction.form, isUpload: transaction.isUpload, transaction: transaction }); } });