1 /**
2  * An implementation of promises.
3  * Work in progress.
4  *
5  * License:
6  *   This Source Code Form is subject to the terms of
7  *   the Mozilla Public License, v. 2.0. If a copy of
8  *   the MPL was not distributed with this file, You
9  *   can obtain one at http://mozilla.org/MPL/2.0/.
10  *
11  * Authors:
12  *   Vladimir Panteleev <ae@cy.md>
13  */
14 
15 module ae.utils.promise;
16 
17 import std.functional;
18 import std.traits : CommonType;
19 
20 import ae.net.asockets : socketManager, onNextTick;
21 
22 debug (no_ae_promise) {} else debug debug = ae_promise;
23 
24 /**
25    A promise for a value `T` or error `E`.
26 
27    Attempts to implement the Promises/A+ spec
28    (https://promisesaplus.com/),
29    with the following deviations:
30 
31    - Sections 2.2.1.1-2, 2.2.7.3-4: Due to D strong typing, the only
32      applicable interpretation of "not a function" is `null`.
33 
34    - Section 2.2.5: JavaScript-specific, and does not apply to D.
35 
36    - Section 2.2.7.2: In D, thrown objects may only be descendants of
37      `Throwable`. By default, `Exception` objects are caught, and
38      passed to `onRejected` handlers.
39 
40    - Section 2.2.7.1/3: In the case when `onFulfilled` is `null` but
41      `onRejected` is not, the returned promise may be resolved with
42      either the fulfilled value of the current promise or the return
43      value of `onRejected`. In this case, the type of the returned
44      promise value is the D common type of the two, or `void` if none.
45 
46    - Section 2.3.1: Instead of rejecting the promise with a TypeError,
47      an assertion failure is thrown.
48 
49    - Section 2.3.3: Not implemented. This section facilitates
50      interoperability with other implementations of JavaScript
51      promises, though it could be implemented in D using DbI to
52      support arbitrary then-able objects.
53 
54    Additionally, this implementation differs from typical JavaScript
55    implementations as follows:
56 
57    - `T` may be `void`. In this case, `fulfill`, and the delegate in
58      first argument of `then`, take zero arguments instead of one.
59 
60    - Instead of the constructor accepting a function which accepts the
61      `fulfill` / `reject` functions, these functions are available as
62      regular methods.
63 
64    - Attempts to fulfill or reject a non-pending promise cause an
65      assertion failure instead of being silently ignored.
66      (The Promises/A+ standard touches on this in section 2.3.3.3.3.)
67 
68    - `catch` is called `except` (because the former is a reserved D
69      keyword).
70 
71    - `finally` is called `finish` (because the former is a reserved D
72      keyword).
73 
74    - In debug builds, resolved `Promise` instances check on
75      destruction that their value / error was passed on to a handler
76      (unless they have been successfully fulfilled to a `void` value).
77      Such leaks are reported to the standard error stream.
78 */
79 final class Promise(T, E : Throwable = Exception)
80 {
81 private:
82 	/// Box of `T`, if it's not `void`, otherwise empty `struct`.
83 	struct Box
84 	{
85 		static if (!is(T == void))
86 			T value;
87 	}
88 
89 	/// Either `(T)` or an empty tuple.
90 	alias A = typeof(Box.tupleof);
91 
92 	PromiseState state;
93 	debug (ae_promise) bool resultUsed;
94 
95 	union
96 	{
97 		Box value;
98 		E error;
99 	}
100 
101 	PromiseHandler[] handlers;
102 
103 	void doFulfill(A value) nothrow
104 	{
105 		this.state = PromiseState.fulfilled;
106 		this.value.tupleof = value;
107 		foreach (ref handler; handlers)
108 			if (handler.onFulfill)
109 				handler.dg();
110 		handlers = null;
111 	}
112 
113 	void doReject(E e) nothrow
114 	{
115 		this.state = PromiseState.rejected;
116 		this.error = e;
117 		foreach (ref handler; handlers)
118 			if (handler.onReject)
119 				handler.dg();
120 		handlers = null;
121 	}
122 
123 	/// Implements the [[Resolve]](promise, x) resolution procedure.
124 	void resolve(scope lazy T valueExpr) /* nothrow */
125 	{
126 		Box box;
127 		static if (is(T == void))
128 			valueExpr;
129 		else
130 			box.value = valueExpr;
131 
132 		fulfill(box.tupleof);
133 	}
134 
135 	/// ditto
136 	void resolve(Promise!(T, E) x) nothrow
137 	{
138 		assert(x !is this, "Attempting to resolve a promise with itself");
139 		assert(this.state == PromiseState.pending);
140 		this.state = PromiseState.following;
141 		x.then(&resolveFulfill, &resolveReject);
142 	}
143 
144 	void resolveFulfill(A value) nothrow
145 	{
146 		assert(this.state == PromiseState.following);
147 		doFulfill(value);
148 	}
149 
150 	void resolveReject(E e) nothrow
151 	{
152 		assert(this.state == PromiseState.following);
153 		doReject(e);
154 	}
155 
156 	debug (ae_promise)
157 	~this() @nogc
158 	{
159 		if (state == PromiseState.pending || state == PromiseState.following || resultUsed)
160 			return;
161 		static if (is(T == void))
162 			if (state == PromiseState.fulfilled)
163 				return;
164 		// Throwing anything here or doing anything else non-@nogc
165 		// will just cause an `InvalidMemoryOperationError`, so
166 		// `printf` is our best compromise.  Even if we could throw,
167 		// the stack trace would not be useful due to the
168 		// nondeterministic nature of the GC.
169 		import core.stdc.stdio : fprintf, stderr;
170 		fprintf(stderr, "Leaked %s %s\n",
171 			state == PromiseState.fulfilled ? "fulfilled".ptr : "rejected".ptr,
172 			typeof(this).stringof.ptr);
173 	}
174 
175 public:
176 	/// Work-around for DMD bug 21804:
177 	/// https://issues.dlang.org/show_bug.cgi?id=21804
178 	/// If your `then` callback argument is a tuple,
179 	/// insert this call before the `then` call.
180 	/// (Needs to be done only once per `Promise!T` instance.)
181 	static if (!is(T == void))
182 	typeof(this) dmd21804workaround()
183 	{
184 		if (false)
185 			then((result) {});
186 		return this;
187 	}
188 
189 	/// Fulfill this promise, with the given value (if applicable).
190 	void fulfill(A value) nothrow
191 	{
192 		assert(this.state == PromiseState.pending,
193 			"This promise is already fulfilled, rejected, or following another promise.");
194 		doFulfill(value);
195 	}
196 
197 	/// Reject this promise, with the given exception.
198 	void reject(E e) nothrow
199 	{
200 		assert(this.state == PromiseState.pending,
201 			"This promise is already fulfilled, rejected, or following another promise.");
202 		doReject(e);
203 	}
204 
205 	/// Registers the specified fulfillment and rejection handlers.
206 	/// If the promise is already resolved, they are called
207 	/// as soon as possible (but not immediately).
208 	Promise!(Unpromise!R, F) then(R, F = E)(R delegate(A) onFulfilled, R delegate(E) onRejected = null) nothrow
209 	{
210 		static if (!is(T : R))
211 			assert(onFulfilled, "Cannot implicitly propagate " ~ T.stringof ~ " to " ~ R.stringof ~ " due to null onFulfilled");
212 
213 		auto next = new typeof(return);
214 
215 		void fulfillHandler() nothrow
216 		{
217 			assert(this.state == PromiseState.fulfilled);
218 			if (onFulfilled)
219 			{
220 				try
221 					next.resolve(onFulfilled(this.value.tupleof));
222 				catch (F e)
223 					next.reject(e);
224 			}
225 			else
226 			{
227 				static if (is(R == void))
228 					next.fulfill();
229 				else
230 				{
231 					static if (!is(T : R))
232 						assert(false); // verified above
233 					else
234 						next.fulfill(this.value.tupleof);
235 				}
236 			}
237 		}
238 
239 		void rejectHandler() nothrow
240 		{
241 			assert(this.state == PromiseState.rejected);
242 			if (onRejected)
243 			{
244 				try
245 					next.resolve(onRejected(this.error));
246 				catch (F e)
247 					next.reject(e);
248 			}
249 			else
250 				next.reject(this.error);
251 		}
252 
253 		final switch (this.state)
254 		{
255 			case PromiseState.pending:
256 			case PromiseState.following:
257 				handlers ~= PromiseHandler({ callSoon(&fulfillHandler); }, true, false);
258 				handlers ~= PromiseHandler({ callSoon(&rejectHandler); }, false, true);
259 				break;
260 			case PromiseState.fulfilled:
261 				callSoon(&fulfillHandler);
262 				break;
263 			case PromiseState.rejected:
264 				callSoon(&rejectHandler);
265 				break;
266 		}
267 
268 		debug (ae_promise) resultUsed = true;
269 		return next;
270 	}
271 
272 	/// Special overload of `then` with no `onFulfilled` function.
273 	/// In this scenario, `onRejected` can act as a filter,
274 	/// converting errors into values for the next promise in the chain.
275 	Promise!(CommonType!(Unpromise!R, T), F) then(R, F = E)(typeof(null) onFulfilled, R delegate(E) onRejected) nothrow
276 	{
277 		// The returned promise will be fulfilled with either
278 		// `this.value` (if `this` is fulfilled), or the return value
279 		// of `onRejected` (if `this` is rejected).
280 		alias C = CommonType!(Unpromise!R, T);
281 
282 		auto next = new typeof(return);
283 
284 		void fulfillHandler() nothrow
285 		{
286 			assert(this.state == PromiseState.fulfilled);
287 			static if (is(C == void))
288 				next.fulfill();
289 			else
290 				next.fulfill(this.value.tupleof);
291 		}
292 
293 		void rejectHandler() nothrow
294 		{
295 			assert(this.state == PromiseState.rejected);
296 			if (onRejected)
297 			{
298 				try
299 					next.resolve(onRejected(this.error));
300 				catch (F e)
301 					next.reject(e);
302 			}
303 			else
304 				next.reject(this.error);
305 		}
306 
307 		final switch (this.state)
308 		{
309 			case PromiseState.pending:
310 			case PromiseState.following:
311 				handlers ~= PromiseHandler({ callSoon(&fulfillHandler); }, true, false);
312 				handlers ~= PromiseHandler({ callSoon(&rejectHandler); }, false, true);
313 				break;
314 			case PromiseState.fulfilled:
315 				callSoon(&fulfillHandler);
316 				break;
317 			case PromiseState.rejected:
318 				callSoon(&rejectHandler);
319 				break;
320 		}
321 
322 		debug (ae_promise) resultUsed = true;
323 		return next;
324 	}
325 
326 	/// Registers a rejection handler.
327 	/// Equivalent to `then(null, onRejected)`.
328 	/// Similar to the `catch` method in JavaScript promises.
329 	Promise!(R, F) except(R, F = E)(R delegate(E) onRejected)
330 	{
331 		return this.then(null, onRejected);
332 	}
333 
334 	/// Registers a finalization handler, which is called when the
335 	/// promise is resolved (either fulfilled or rejected).
336 	/// Roughly equivalent to `then(value => onResolved(), error => onResolved())`.
337 	/// Similar to the `finally` method in JavaScript promises.
338 	Promise!(R, F) finish(R, F = E)(R delegate() onResolved)
339 	{
340 		assert(onResolved, "No onResolved delegate specified in .finish");
341 
342 		auto next = new typeof(return);
343 
344 		void handler() nothrow
345 		{
346 			assert(this.state == PromiseState.fulfilled || this.state == PromiseState.rejected);
347 			try
348 				next.resolve(onResolved());
349 			catch (F e)
350 				next.reject(e);
351 		}
352 
353 		final switch (this.state)
354 		{
355 			case PromiseState.pending:
356 			case PromiseState.following:
357 				handlers ~= PromiseHandler({ callSoon(&handler); }, true, true);
358 				break;
359 			case PromiseState.fulfilled:
360 			case PromiseState.rejected:
361 				callSoon(&handler);
362 				break;
363 		}
364 
365 		debug (ae_promise) resultUsed = true;
366 		return next;
367 	}
368 }
369 
370 // (These declarations are top-level because they don't need to be templated.)
371 
372 private enum PromiseState
373 {
374 	pending,
375 	following,
376 	fulfilled,
377 	rejected,
378 }
379 
380 private struct PromiseHandler
381 {
382 	void delegate() nothrow dg;
383 	bool onFulfill, onReject;
384 }
385 
386 // The reverse operation is the `.resolve` overload.
387 private template Unpromise(P)
388 {
389 	static if (is(P == Promise!(T, E), T, E))
390 		alias Unpromise = T;
391 	else
392 		alias Unpromise = P;
393 }
394 
395 // This is the only non-"pure" part of this implementation.
396 private void callSoon(void delegate() dg) @safe nothrow { socketManager.onNextTick(dg); }
397 
398 // This is just a simple instantiation test.
399 // The full test suite (D translation of the Promises/A+ conformance
400 // test) is here: https://github.com/CyberShadow/ae-promises-tests
401 unittest
402 {
403 	if (false)
404 	{
405 		Promise!int test;
406 		test.then((int i) {});
407 		test.then((int i) {}, (Exception e) {});
408 		test.then(null, (Exception e) {});
409 		test.except((Exception e) {});
410 		test.finish({});
411 
412 		Promise!void test2;
413 		test2.then({});
414 	}
415 }
416 
417 // ****************************************************************************
418 
419 /// Wait for all promises to be resolved, or for any to be rejected.
420 Promise!(T[], E) all(T, E)(Promise!(T, E)[] promises...)
421 if (!is(T == void))
422 {
423 	auto allPromise = new typeof(return);
424 	auto results = new T[promises.length];
425 	size_t numResolved;
426 	foreach (i, p; promises)
427 		(i, p) {
428 			p.then((result) {
429 				if (allPromise)
430 				{
431 					results[i] = result;
432 					if (++numResolved == promises.length)
433 						allPromise.fulfill(results);
434 				}
435 			}, (error) {
436 				allPromise.reject(error);
437 				allPromise = null; // ignore successive resolves
438 			});
439 		}(i, p);
440 	return allPromise;
441 }
442 
443 /// ditto
444 Promise!(void, E) all(E)(Promise!(void, E)[] promises...)
445 {
446 	auto allPromise = new typeof(return);
447 	size_t numResolved;
448 	foreach (i, p; promises)
449 		(i, p) {
450 			p.then({
451 				if (allPromise && ++numResolved == promises.length)
452 					allPromise.fulfill();
453 			}, (error) {
454 				allPromise.reject(error);
455 				allPromise = null; // ignore successive resolves
456 			});
457 		}(i, p);
458 	return allPromise;
459 }
460 
461 unittest
462 {
463 	int result;
464 	auto p1 = new Promise!int;
465 	auto p2 = new Promise!int;
466 	auto p3 = new Promise!int;
467 	p2.fulfill(2);
468 	auto pAll = all([p1, p2, p3]);
469 	p1.fulfill(1);
470 	pAll.dmd21804workaround.then((values) { result = values[0] + values[1] + values[2]; });
471 	p3.fulfill(3);
472 	socketManager.loop();
473 	assert(result == 6);
474 }
475 
476 unittest
477 {
478 	int called;
479 	auto p1 = new Promise!void;
480 	auto p2 = new Promise!void;
481 	auto p3 = new Promise!void;
482 	p2.fulfill();
483 	auto pAll = all([p1, p2, p3]);
484 	p1.fulfill();
485 	pAll.then({ called = true; });
486 	socketManager.loop();
487 	assert(!called);
488 	p3.fulfill();
489 	socketManager.loop();
490 	assert(called);
491 }