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