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.meta : allSatisfy, AliasSeq; 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 alias A = typeof(Box.tupleof); 90 91 PromiseState state; 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 static if (!is(T == void)) 117 debug (ae_promise) markAsUnused(); 118 foreach (ref handler; handlers) 119 if (handler.onFulfill) 120 handler.dg(); 121 handlers = null; 122 } 123 124 void doReject(E e) /*nothrow*/ 125 { 126 this.state = PromiseState.rejected; 127 this.error = e; 128 debug (ae_promise) markAsUnused(); 129 foreach (ref handler; handlers) 130 if (handler.onReject) 131 handler.dg(); 132 handlers = null; 133 } 134 135 /// Implements the [[Resolve]](promise, x) resolution procedure. 136 void resolve(scope lazy T valueExpr) /*nothrow*/ 137 { 138 Box box; 139 static if (is(T == void)) 140 valueExpr; 141 else 142 box.value = valueExpr; 143 144 fulfill(box.tupleof); 145 } 146 147 /// ditto 148 void resolve(Promise!(T, E) x) /*nothrow*/ 149 { 150 assert(x !is this, "Attempting to resolve a promise with itself"); 151 assert(this.state == PromiseState.pending); 152 this.state = PromiseState.following; 153 x.then(&resolveFulfill, &resolveReject); 154 } 155 156 void resolveFulfill(A value) /*nothrow*/ 157 { 158 assert(this.state == PromiseState.following); 159 doFulfill(value); 160 } 161 162 void resolveReject(E e) /*nothrow*/ 163 { 164 assert(this.state == PromiseState.following); 165 doReject(e); 166 } 167 168 // This debug machinery tracks leaked promises, i.e. promises 169 // which have been fulfilled/rejected, but their result was never 170 // used (their .then method was never called). 171 debug (ae_promise) 172 { 173 // Global doubly linked list of promises with unused results 174 static typeof(this) unusedHead, unusedTail; 175 typeof(this) unusedPrev, unusedNext; 176 bool isUnused() { return unusedPrev || (unusedHead is this); } 177 178 LeakedPromiseError leakedPromiseError; 179 bool resultUsed; 180 181 void markAsUnused() 182 { 183 if (resultUsed) 184 return; // An earlier `then` call has priority 185 assert(!isUnused); 186 if (unusedTail) 187 { 188 unusedPrev = unusedTail; 189 unusedTail.unusedNext = this; 190 } 191 unusedTail = this; 192 if (!unusedHead) 193 unusedHead = this; 194 } 195 196 void markAsUsed() 197 { 198 if (resultUsed) 199 return; 200 resultUsed = true; 201 if (isUnused) 202 { 203 if (unusedPrev) unusedPrev.unusedNext = unusedNext; else unusedHead = unusedNext; 204 if (unusedNext) unusedNext.unusedPrev = unusedPrev; else unusedTail = unusedPrev; 205 } 206 } 207 208 static ~this() 209 { 210 for (auto p = unusedHead; p; p = p.unusedNext) 211 { 212 // If these asserts fail, there is a bug in our debug machinery 213 assert(p.state != PromiseState.pending && p.state != PromiseState.following && !p.resultUsed); 214 static if (is(T == void)) 215 assert(p.state != PromiseState.fulfilled); 216 217 import core.stdc.stdio : fprintf, stderr; 218 fprintf(stderr, "Leaked %s %s\n", 219 p.state == PromiseState.fulfilled ? "fulfilled".ptr : "rejected".ptr, 220 typeof(this).stringof.ptr); 221 if (p.state == PromiseState.rejected) 222 _d_print_throwable(p.error); 223 _d_print_throwable(p.leakedPromiseError); 224 } 225 } 226 } 227 228 public: 229 debug (ae_promise) 230 this() nothrow 231 { 232 // Record instantiation point 233 try 234 throw new LeakedPromiseError(); 235 catch (LeakedPromiseError e) 236 leakedPromiseError = e; 237 catch (Throwable) {} // allow nothrow 238 } 239 240 /// A tuple of this `Promise`'s value. 241 /// Either `(T)` or an empty tuple. 242 alias ValueTuple = A; 243 244 /// Work-around for DMD bug 21804: 245 /// https://issues.dlang.org/show_bug.cgi?id=21804 246 /// If your `then` callback argument is a tuple, 247 /// insert this call before the `then` call. 248 /// (Needs to be done only once per `Promise!T` instance.) 249 typeof(this) dmd21804workaround() 250 { 251 static if (!is(T == void)) 252 if (false) 253 then((A result) {}); 254 return this; 255 } 256 257 /// Ignore this promise leaking in debug builds. 258 void ignoreResult() 259 { 260 debug (ae_promise) markAsUsed(); 261 } 262 263 /// Fulfill this promise, with the given value (if applicable). 264 void fulfill(A value) /*nothrow*/ 265 { 266 assert(this.state == PromiseState.pending, 267 "This promise is already fulfilled, rejected, or following another promise."); 268 doFulfill(value); 269 } 270 271 /// Reject this promise, with the given exception. 272 void reject(E e) /*nothrow*/ 273 { 274 assert(this.state == PromiseState.pending, 275 "This promise is already fulfilled, rejected, or following another promise."); 276 doReject(e); 277 } 278 279 /// Registers the specified fulfillment and rejection handlers. 280 /// If the promise is already resolved, they are called 281 /// as soon as possible (but not immediately). 282 Promise!(Unpromise!R, F) then(R, F = E)(R delegate(A) onFulfilled, R delegate(E) onRejected = null) /*nothrow*/ 283 { 284 static if (!is(T : R)) 285 assert(onFulfilled, "Cannot implicitly propagate " ~ T.stringof ~ " to " ~ R.stringof ~ " due to null onFulfilled"); 286 287 auto next = new typeof(return); 288 289 void fulfillHandler() /*nothrow*/ 290 { 291 assert(this.state == PromiseState.fulfilled); 292 if (onFulfilled) 293 { 294 try 295 next.resolve(onFulfilled(this.value.tupleof)); 296 catch (F e) 297 next.reject(e); 298 } 299 else 300 { 301 static if (is(R == void)) 302 next.fulfill(); 303 else 304 { 305 static if (!is(T : R)) 306 assert(false); // verified above 307 else 308 next.fulfill(this.value.tupleof); 309 } 310 } 311 } 312 313 void rejectHandler() /*nothrow*/ 314 { 315 assert(this.state == PromiseState.rejected); 316 if (onRejected) 317 { 318 try 319 next.resolve(onRejected(this.error)); 320 catch (F e) 321 next.reject(e); 322 } 323 else 324 next.reject(this.error); 325 } 326 327 final switch (this.state) 328 { 329 case PromiseState.pending: 330 case PromiseState.following: 331 handlers ~= PromiseHandler({ callSoon(&fulfillHandler); }, true, false); 332 handlers ~= PromiseHandler({ callSoon(&rejectHandler); }, false, true); 333 break; 334 case PromiseState.fulfilled: 335 callSoon(&fulfillHandler); 336 break; 337 case PromiseState.rejected: 338 callSoon(&rejectHandler); 339 break; 340 } 341 342 debug (ae_promise) markAsUsed(); 343 return next; 344 } 345 346 /// Special overload of `then` with no `onFulfilled` function. 347 /// In this scenario, `onRejected` can act as a filter, 348 /// converting errors into values for the next promise in the chain. 349 Promise!(CommonType!(Unpromise!R, T), F) then(R, F = E)(typeof(null) onFulfilled, R delegate(E) onRejected) /*nothrow*/ 350 { 351 // The returned promise will be fulfilled with either 352 // `this.value` (if `this` is fulfilled), or the return value 353 // of `onRejected` (if `this` is rejected). 354 alias C = CommonType!(Unpromise!R, T); 355 356 auto next = new typeof(return); 357 358 void fulfillHandler() /*nothrow*/ 359 { 360 assert(this.state == PromiseState.fulfilled); 361 static if (is(C == void)) 362 next.fulfill(); 363 else 364 next.fulfill(this.value.tupleof); 365 } 366 367 void rejectHandler() /*nothrow*/ 368 { 369 assert(this.state == PromiseState.rejected); 370 if (onRejected) 371 { 372 try 373 next.resolve(onRejected(this.error)); 374 catch (F e) 375 next.reject(e); 376 } 377 else 378 next.reject(this.error); 379 } 380 381 final switch (this.state) 382 { 383 case PromiseState.pending: 384 case PromiseState.following: 385 handlers ~= PromiseHandler({ callSoon(&fulfillHandler); }, true, false); 386 handlers ~= PromiseHandler({ callSoon(&rejectHandler); }, false, true); 387 break; 388 case PromiseState.fulfilled: 389 callSoon(&fulfillHandler); 390 break; 391 case PromiseState.rejected: 392 callSoon(&rejectHandler); 393 break; 394 } 395 396 debug (ae_promise) markAsUsed(); 397 return next; 398 } 399 400 /// Registers a rejection handler. 401 /// Equivalent to `then(null, onRejected)`. 402 /// Similar to the `catch` method in JavaScript promises. 403 Promise!(R, F) except(R, F = E)(R delegate(E) onRejected) 404 { 405 return this.then(null, onRejected); 406 } 407 408 /// Registers a finalization handler, which is called when the 409 /// promise is resolved (either fulfilled or rejected). 410 /// Roughly equivalent to `then(value => onResolved(), error => onResolved())`. 411 /// Similar to the `finally` method in JavaScript promises. 412 Promise!(R, F) finish(R, F = E)(R delegate() onResolved) 413 { 414 assert(onResolved, "No onResolved delegate specified in .finish"); 415 416 auto next = new typeof(return); 417 418 void handler() /*nothrow*/ 419 { 420 assert(this.state == PromiseState.fulfilled || this.state == PromiseState.rejected); 421 try 422 next.resolve(onResolved()); 423 catch (F e) 424 next.reject(e); 425 } 426 427 final switch (this.state) 428 { 429 case PromiseState.pending: 430 case PromiseState.following: 431 handlers ~= PromiseHandler({ callSoon(&handler); }, true, true); 432 break; 433 case PromiseState.fulfilled: 434 case PromiseState.rejected: 435 callSoon(&handler); 436 break; 437 } 438 439 debug (ae_promise) markAsUsed(); 440 return next; 441 } 442 } 443 444 // (These declarations are top-level because they don't need to be templated.) 445 446 private enum PromiseState 447 { 448 pending, 449 following, 450 fulfilled, 451 rejected, 452 } 453 454 debug (ae_promise) 455 { 456 private final class LeakedPromiseError : Throwable { this() { super("Created here:"); } } 457 private extern (C) void _d_print_throwable(Throwable t) @nogc; 458 } 459 460 // The reverse operation is the `.resolve` overload. 461 private template Unpromise(P) 462 { 463 static if (is(P == Promise!(T, E), T, E)) 464 alias Unpromise = T; 465 else 466 alias Unpromise = P; 467 } 468 469 // This is the only non-"pure" part of this implementation. 470 private void callSoon(void delegate() dg) @safe nothrow { socketManager.onNextTick(dg); } 471 472 // This is just a simple instantiation test. 473 // The full test suite (D translation of the Promises/A+ conformance 474 // test) is here: https://github.com/CyberShadow/ae-promises-tests 475 nothrow unittest 476 { 477 static bool never; if (never) 478 { 479 Promise!int test; 480 test.then((int i) {}); 481 test.then((int i) {}, (Exception e) {}); 482 test.then(null, (Exception e) {}); 483 test.except((Exception e) {}); 484 test.finish({}); 485 test.fulfill(1); 486 test.reject(Exception.init); 487 488 Promise!void test2; 489 test2.then({}); 490 } 491 } 492 493 // Non-Exception based errors 494 unittest 495 { 496 static bool never; if (never) 497 { 498 static class OtherException : Exception 499 { 500 this() { super(null); } 501 } 502 503 Promise!(int, OtherException) test; 504 test.then((int i) {}); 505 test.then((int i) {}, (OtherException e) {}); 506 test.then(null, (OtherException e) {}); 507 test.except((OtherException e) {}); 508 test.fulfill(1); 509 test.reject(OtherException.init); 510 } 511 } 512 513 // **************************************************************************** 514 515 /// Returns a new `Promise!void` which is resolved. 516 Promise!void resolve(E = Exception)() { auto p = new Promise!(void, E)(); p.fulfill(); return p; } 517 518 /// Returns a new `Promise` which is resolved with the given value. 519 Promise!T resolve(T, E = Exception)(T value) { auto p = new Promise!(T, E)(); p.fulfill(value); return p; } 520 521 /// Returns a new `Promise` which is rejected with the given reason. 522 Promise!(T, E) reject(T, E)(E reason) { auto p = new Promise!(T, E)(); p.reject(reason); return p; } 523 524 // **************************************************************************** 525 526 /// Return `true` if `P` is a `Promise` instantiation. 527 template isPromise(P) 528 { 529 static if (is(P == Promise!(T, E), T, E)) 530 enum isPromise = true; 531 else 532 enum isPromise = false; 533 } 534 535 /// Get the value type of the promise `P`, 536 /// i.e. its `T` parameter. 537 template PromiseValue(P) 538 { 539 /// 540 static if (is(P == Promise!(T, E), T, E)) 541 alias PromiseValue = T; 542 else 543 static assert(false); 544 } 545 546 /// Get the error type of the promise `P`, 547 /// i.e. its `E` parameter. 548 template PromiseError(P) 549 { 550 /// 551 static if (is(P == Promise!(T, E), T, E)) 552 alias PromiseError = E; 553 else 554 static assert(false); 555 } 556 557 /// Construct a new Promise type based on `P`, 558 /// if the given transformation was applied on the value type. 559 /// If `P` is a `void` Promise, then the returned promise 560 /// will also be `void`. 561 template PromiseValueTransform(P, alias transform) 562 if (is(P == Promise!(T, E), T, E)) 563 { 564 /// ditto 565 static if (is(P == Promise!(T, E), T, E)) 566 { 567 static if (is(T == void)) 568 private alias T2 = void; 569 else 570 private alias T2 = typeof({ T* value; return transform(*value); }()); 571 alias PromiseValueTransform = Promise!(T2, E); 572 } 573 } 574 575 // **************************************************************************** 576 577 /// Wait for all promises to be resolved, or for any to be rejected. 578 PromiseValueTransform!(P, x => [x]) all(P)(P[] promises) 579 if (is(P == Promise!(T, E), T, E)) 580 { 581 alias T = PromiseValue!P; 582 583 auto allPromise = new typeof(return); 584 585 typeof(return).ValueTuple results; 586 static if (!is(T == void)) 587 results[0] = new T[promises.length]; 588 589 if (promises.length) 590 { 591 size_t numResolved; 592 foreach (i, p; promises) 593 (i, p) { 594 p.dmd21804workaround.then((P.ValueTuple result) { 595 if (allPromise) 596 { 597 static if (!is(T == void)) 598 results[0][i] = result[0]; 599 if (++numResolved == promises.length) 600 allPromise.fulfill(results); 601 } 602 }, (error) { 603 if (allPromise) 604 { 605 allPromise.reject(error); 606 allPromise = null; // ignore successive resolves / rejects 607 } 608 }); 609 }(i, p); 610 } 611 else 612 allPromise.fulfill(results); 613 return allPromise; 614 } 615 616 nothrow unittest 617 { 618 import std.exception : assertNotThrown; 619 int result; 620 auto p1 = new Promise!int; 621 auto p2 = new Promise!int; 622 auto p3 = new Promise!int; 623 p2.fulfill(2); 624 auto pAll = all([p1, p2, p3]); 625 p1.fulfill(1); 626 pAll.dmd21804workaround.then((values) { result = values[0] + values[1] + values[2]; }); 627 p3.fulfill(3); 628 socketManager.loop().assertNotThrown; 629 assert(result == 6); 630 } 631 632 nothrow unittest 633 { 634 import std.exception : assertNotThrown; 635 int called; 636 auto p1 = new Promise!void; 637 auto p2 = new Promise!void; 638 auto p3 = new Promise!void; 639 p2.fulfill(); 640 auto pAll = all([p1, p2, p3]); 641 p1.fulfill(); 642 pAll.then({ called = true; }); 643 socketManager.loop().assertNotThrown; 644 assert(!called); 645 p3.fulfill(); 646 socketManager.loop().assertNotThrown; 647 assert(called); 648 } 649 650 nothrow unittest 651 { 652 import std.exception : assertNotThrown; 653 Promise!void[] promises; 654 auto pAll = all(promises); 655 bool called; 656 pAll.then({ called = true; }); 657 socketManager.loop().assertNotThrown; 658 assert(called); 659 } 660 661 private template AllResultImpl(size_t promiseIndex, size_t resultIndex, Promises...) 662 { 663 static if (Promises.length == 0) 664 { 665 alias TupleMembers = AliasSeq!(); 666 enum size_t[] mapping = []; 667 } 668 else 669 static if (is(PromiseValue!(Promises[0]) == void)) 670 { 671 alias Next = AllResultImpl!(promiseIndex + 1, resultIndex, Promises[1..$]); 672 alias TupleMembers = Next.TupleMembers; 673 enum size_t[] mapping = [size_t(-1)] ~ Next.mapping; 674 } 675 else 676 { 677 alias Next = AllResultImpl!(promiseIndex + 1, resultIndex + 1, Promises[1..$]); 678 alias TupleMembers = AliasSeq!(PromiseValue!(Promises[0]), Next.TupleMembers); 679 enum size_t[] mapping = [resultIndex] ~ Next.mapping; 680 } 681 } 682 683 // Calculates a value type for a Promise suitable to hold the values of the given promises. 684 // void-valued promises are removed; an empty list is converted to void. 685 // Also calculates an index map from Promises indices to tuple member indices. 686 private template AllResult(Promises...) 687 { 688 alias Impl = AllResultImpl!(0, 0, Promises); 689 static if (Impl.TupleMembers.length == 0) 690 alias ResultType = void; 691 else 692 { 693 import std.typecons : Tuple; 694 alias ResultType = Tuple!(Impl.TupleMembers); 695 } 696 } 697 698 private alias PromiseBox(P) = P.Box; 699 700 /// Heterogeneous variant, which resolves to a tuple. 701 /// void promises' values are omitted from the result tuple. 702 /// If all promises are void, then so is the result. 703 Promise!(AllResult!Promises.ResultType) all(Promises...)(Promises promises) 704 if (allSatisfy!(isPromise, Promises)) 705 { 706 AllResult!Promises.Impl.TupleMembers results; 707 708 auto allPromise = new typeof(return); 709 710 static if (promises.length) 711 { 712 size_t numResolved; 713 foreach (i, p; promises) 714 { 715 alias P = typeof(p); 716 alias T = PromiseValue!P; 717 p.dmd21804workaround.then((P.ValueTuple result) { 718 if (allPromise) 719 { 720 static if (!is(T == void)) 721 results[AllResult!Promises.Impl.mapping[i]] = result[0]; 722 if (++numResolved == promises.length) 723 { 724 static if (AllResult!Promises.Impl.TupleMembers.length) 725 { 726 import std.typecons : tuple; 727 allPromise.fulfill(tuple(results)); 728 } 729 else 730 allPromise.fulfill(); 731 } 732 } 733 }, (error) { 734 if (allPromise) 735 { 736 allPromise.reject(error); 737 allPromise = null; // ignore successive resolves / rejects 738 } 739 }); 740 } 741 } 742 else 743 allPromise.fulfill(); 744 return allPromise; 745 } 746 747 nothrow unittest 748 { 749 import std.exception : assertNotThrown; 750 import ae.utils.meta : I; 751 752 int result; 753 auto p1 = new Promise!byte; 754 auto p2 = new Promise!void; 755 auto p3 = new Promise!int; 756 p2.fulfill(); 757 auto pAll = all(p1, p2, p3); 758 p1.fulfill(1); 759 pAll.dmd21804workaround 760 .then(values => values.expand.I!((v1, v3) { 761 result = v1 + v3; 762 })); 763 p3.fulfill(3); 764 socketManager.loop().assertNotThrown; 765 assert(result == 4); 766 } 767 768 nothrow unittest 769 { 770 bool ok; 771 import std.exception : assertNotThrown; 772 auto p1 = new Promise!void; 773 auto p2 = new Promise!void; 774 auto p3 = new Promise!void; 775 p2.fulfill(); 776 auto pAll = all(p1, p2, p3); 777 p1.fulfill(); 778 pAll.then({ ok = true; }); 779 socketManager.loop().assertNotThrown; 780 assert(!ok); 781 p3.fulfill(); 782 socketManager.loop().assertNotThrown; 783 assert(ok); 784 } 785 786 // **************************************************************************** 787 788 /// Ordered promise queue, supporting asynchronous enqueuing / fulfillment. 789 struct PromiseQueue(T, E = Exception) 790 { 791 private alias P = Promise!(T, E); 792 793 private P[] fulfilled, waiting; 794 795 import ae.utils.array : queuePush, queuePop; 796 797 /// Retrieve the next fulfilled promise, or enqueue a waiting one. 798 P waitOne() 799 { 800 if (fulfilled.length) 801 return fulfilled.queuePop(); 802 803 auto p = new P; 804 waiting.queuePush(p); 805 return p; 806 } 807 808 /// Fulfill one waiting promise, or enqueue a fulfilled one. 809 P fulfillOne(typeof(P.Box.tupleof) value) 810 { 811 if (waiting.length) 812 { 813 waiting.queuePop.fulfill(value); 814 return null; 815 } 816 817 auto p = new P; 818 p.fulfill(value); 819 fulfilled.queuePush(p); 820 return p; 821 } 822 } 823 824 unittest 825 { 826 PromiseQueue!int q; 827 q.fulfillOne(1); 828 q.fulfillOne(2); 829 int[] result; 830 q.waitOne().then((i) { result ~= i; }); 831 q.waitOne().then((i) { result ~= i; }); 832 socketManager.loop(); 833 assert(result == [1, 2]); 834 } 835 836 unittest 837 { 838 PromiseQueue!int q; 839 int[] result; 840 q.waitOne().then((i) { result ~= i; }); 841 q.waitOne().then((i) { result ~= i; }); 842 q.fulfillOne(1); 843 q.fulfillOne(2); 844 socketManager.loop(); 845 assert(result == [1, 2]); 846 }