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 }