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 }