Source RapidContext_Procedure.js

1/*
2 * RapidContext <https://www.rapidcontext.com/>
3 * Copyright (c) 2009-2023 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