1/*
2 * RapidContext <https://www.rapidcontext.com/>
3 * Copyright (c) 2009-2024 Per Cederberg. All rights reserved.
4 *
5 * This program is free software: you can redistribute it and/or
6 * modify it under the terms of the BSD license.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11 *
12 * See the RapidContext LICENSE for more details.
13 */
14
15(function (window) {
16
17 /**
18 * Creates a new procedure caller function. This function can be called either
19 * as a constructor or as a plain function. In both cases it returns a new
20 * JavaScript function with additional methods.
21 *
22 * @constructor
23 * @param {string} procedure the procedure name
24 * @property {string} procedure The procedure name.
25 * @property {Array} args The arguments used in the last call.
26 *
27 * @name RapidContext.Procedure
28 * @class The procedure wrapper function. Used to provide a simplified way of
29 * calling a procedure and connecting results through signals (instead of
30 * using promise callbacks).
31 *
32 * The actual calls are performed with normal function calls, but the results
33 * are asynchronous. When called, the procedure function returns a
34 * `RapidContext.Async` promise (as the normal API call), but the results
35 * will also be signalled through the `onsuccess` signal.
36 *
37 * Differing from normal functions, a procedure function will also ensure
38 * that only a single call is in progress at any time, automatically
39 * cancelling any previous call if needed.
40 */
41 function Procedure(procedure) {
42 function self() {
43 self.args = Array.from(arguments);
44 return self.recall();
45 }
46 self.procedure = procedure;
47 self.args = null;
48 self._promise = null;
49 for (var k in Procedure.prototype) {
50 if (!self[k]) {
51 self[k] = Procedure.prototype[k];
52 }
53 }
54 return self;
55 }
56
57 /**
58 * Emitted when the procedure is called. Each call corresponds to exactly one
59 * `oncall` and one `onresponse` event (even if the call was cancelled). No
60 * event data will be sent.
61 *
62 * @name RapidContext.Procedure#oncall
63 * @event
64 */
65
66 /**
67 * Emitted if a partial procedure result is available. This event will only be
68 * emitted when performing a multi-call, along with the `oncall` and
69 * `onresponse` events (for each call). The partial procedure result will be
70 * sent as the event data.
71 *
72 * @name RapidContext.Procedure#onupdate
73 * @event
74 */
75
76 /**
77 * Emitted when the procedure response has been received. Each call corresponds
78 * to exactly one `oncall` and one `onresponse` event (even if the call was
79 * cancelled). The call response or error object will be sent as the event
80 * data.
81 *
82 * @name RapidContext.Procedure#onresponse
83 * @event
84 */
85
86 /**
87 * Emitted when a procedure call returned a result. This event is emitted after
88 * the `onresponse` event, but only if the procedure call actually succeeded.
89 * Use the `onerror` or `oncancel` signals for other result statuses. The call
90 * response object will be sent as the event data.
91 *
92 * @name RapidContext.Procedure#onsuccess
93 * @event
94 */
95
96 /**
97 * Emitted when a procedure call failed. This event is emitted after the
98 * `onresponse` event, but only if the procedure call returned an error. Use
99 * the `onsuccess` or `oncancel` for other result statuses. The call error
100 * object will be sent as the event data.
101 *
102 * @name RapidContext.Procedure#onerror
103 * @event
104 */
105
106 /**
107 * Emitted when a procedure call was cancelled. This event is emitted after the
108 * `onresponse` event, but only if the procedure call was cancelled. Use the
109 * `onsuccess` or `onerror` for other result statuses. No event data will be
110 * sent.
111 *
112 * @name RapidContext.Procedure#oncancel
113 * @event
114 */
115
116 /**
117 * Calls the procedure with the same arguments as used in the last call. The
118 * call is asynchronous, so results will not be returned by this method.
119 * Instead the results will be available through the `onsuccess` signal, for
120 * example.
121 *
122 * Note that any previously running call will automatically be cancelled, since
123 * only a single call can be processed at any time.
124 *
125 * @memberof RapidContext.Procedure.prototype
126 * @return {Promise} a `RapidContext.Async` promise that will resolve with
127 * the response data or error
128 */
129 function recall() {
130 if (this.args === null) {
131 throw new Error("No arguments supplied for procedure call to " + this.procedure);
132 }
133 this.cancel();
134 signal(this, "oncall");
135 var cb = callback.bind(this);
136 this._promise = RapidContext.App.callProc(this.procedure, this.args).then(cb, cb);
137 return this._promise;
138 }
139
140 // The procedure promise callback handler. Dispatches the appropriate
141 // signals depending on the result.
142 function callback(res) {
143 this._promise = null;
144 signal(this, "onresponse", res);
145 if (res instanceof Error) {
146 signal(this, "onerror", res);
147 return Promise.reject(res);
148 } else {
149 signal(this, "onsuccess", res);
150 return res;
151 }
152 }
153
154 /**
155 * Calls the procedure multiple times (in sequence) with different arguments
156 * (supplied as an array of argument arrays). The calls are asynchronous, so
157 * results will not be returned by this method. Instead an array with the
158 * results will be available through the `onupdate` and `onsuccess` signals,
159 * for example.
160 *
161 * Note that any previously running call will automatically be cancelled, since
162 * only a single call can be processed at any time. A result `transform`
163 * function can be supplied to transform each individual result. If the
164 * `transform` function throws an error, that result will be omitted.
165 *
166 * @memberof RapidContext.Procedure.prototype
167 * @param {Array} args the array of argument arrays
168 * @param {function} [transform] the optional result transform function
169 * @return {Promise} a `RapidContext.Async` promise that will resolve with
170 * the response data or error
171 */
172 function multicall(args, transform) {
173 this.cancel();
174 this._mapArgs = args;
175 this._mapPos = 0;
176 this._mapRes = [];
177 this._mapTransform = transform;
178 nextCall.call(this);
179 }
180
181 // The multicall promise callback handler. Dispatches the appropriate
182 // signals depending on the result.
183 function nextCall(res) {
184 this._promise = null;
185 if (typeof(res) != "undefined") {
186 signal(this, "onresponse", res);
187 if (res instanceof Error) {
188 signal(this, "onerror", res);
189 return Promise.reject(res);
190 } else {
191 if (this._mapTransform == null) {
192 this._mapRes.push(res);
193 } else {
194 try {
195 res = this._mapTransform(res);
196 this._mapRes.push(res);
197 } catch (ignore) {
198 // Skip results with mapping errors
199 }
200 }
201 signal(this, "onupdate", this._mapRes);
202 }
203 }
204 if (this._mapPos < this._mapArgs.length) {
205 this.args = this._mapArgs[this._mapPos++];
206 signal(this, "oncall");
207 var cb = nextCall.bind(this);
208 this._promise = RapidContext.App.callProc(this.procedure, this.args).then(cb, cb);
209 return this._promise;
210 } else {
211 signal(this, "onsuccess", this._mapRes);
212 return this._mapRes;
213 }
214 }
215
216 /**
217 * Cancels any current execution of this procedure. This method does nothing if
218 * no procedure call was currently executing.
219 *
220 * @memberof RapidContext.Procedure.prototype
221 */
222 function cancel() {
223 if (this._promise !== null) {
224 this._promise.cancel();
225 this._promise = null;
226 return true;
227 } else {
228 return false;
229 }
230 }
231
232 /**
233 * Cancels any current execution and removes the reference to the arguments of
234 * this procedure.
235 *
236 * @memberof RapidContext.Procedure.prototype
237 */
238 function reset() {
239 this.cancel();
240 this.args = null;
241 }
242
243 /**
244 * Creates a new procedure caller for each key-value-pair in the specified
245 * object.
246 *
247 * @memberOf RapidContext.Procedure
248 * @param {Object} obj an object mapping keys to procedure names
249 * @return {Object} an object mapping keys to procedure instances
250 */
251 function mapAll(obj) {
252 var res = {};
253 for (var k in obj) {
254 res[k] = Procedure(obj[k]);
255 }
256 return res;
257 }
258
259 // Emits a signal via MochiKit.Signal
260 function signal(src, sig, value) {
261 try {
262 if (value === undefined) {
263 MochiKit.Signal.signal(src, sig);
264 } else {
265 MochiKit.Signal.signal(src, sig, value);
266 }
267 } catch (e) {
268 var msg = ["exception in", src.procedure, sig, "handler:"].join(" ");
269 (e.errors || [e]).forEach(function (err) {
270 console.error(msg, err);
271 });
272 }
273 }
274
275 // Create namespace and export API
276 var RapidContext = window.RapidContext || (window.RapidContext = {});
277 RapidContext.Procedure = Procedure;
278 Object.assign(Procedure.prototype, { recall, multicall, cancel, reset });
279 Object.assign(Procedure, { mapAll });
280
281})(this);
282
RapidContext
Access · Discovery · Insight
www.rapidcontext.com