Source RapidContext_Procedure.js

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