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