observable-from.any.js (52280B)
1 // Because we test that the global error handler is called at various times. 2 setup({allow_uncaught_exception: true}); 3 4 test(() => { 5 assert_equals(typeof Observable.from, "function", 6 "Observable.from() is a function"); 7 }, "from(): Observable.from() is a function"); 8 9 test(() => { 10 assert_throws_js(TypeError, () => Observable.from(10), 11 "Number cannot convert to an Observable"); 12 assert_throws_js(TypeError, () => Observable.from(true), 13 "Boolean cannot convert to an Observable"); 14 assert_throws_js(TypeError, () => Observable.from("String"), 15 "String cannot convert to an Observable"); 16 assert_throws_js(TypeError, () => Observable.from({a: 10}), 17 "Object cannot convert to an Observable"); 18 assert_throws_js(TypeError, () => Observable.from(Symbol.iterator), 19 "Bare Symbol.iterator cannot convert to an Observable"); 20 assert_throws_js(TypeError, () => Observable.from(Promise), 21 "Promise constructor cannot convert to an Observable"); 22 }, "from(): Failed conversions"); 23 24 test(() => { 25 const target = new EventTarget(); 26 const observable = target.when('custom'); 27 const from_observable = Observable.from(observable); 28 assert_equals(observable, from_observable); 29 }, "from(): Given an observable, it returns that exact observable"); 30 31 test(() => { 32 let completeCalled = false; 33 const results = []; 34 const array = [1, 2, 3, 'a', new Date(), 15, [12]]; 35 const observable = Observable.from(array); 36 observable.subscribe({ 37 next: v => results.push(v), 38 error: e => assert_unreached('error is not called'), 39 complete: () => completeCalled = true 40 }); 41 42 assert_array_equals(results, array); 43 assert_true(completeCalled); 44 }, "from(): Given an array"); 45 46 test(() => { 47 const iterable = { 48 [Symbol.iterator]() { 49 let n = 0; 50 return { 51 next() { 52 n++; 53 if (n <= 3) { 54 return { value: n, done: false }; 55 } 56 return { value: undefined, done: true }; 57 }, 58 }; 59 }, 60 }; 61 62 const observable = Observable.from(iterable); 63 64 assert_true(observable instanceof Observable, "Observable.from() returns an Observable"); 65 66 const results = []; 67 68 observable.subscribe({ 69 next: (value) => results.push(value), 70 error: () => assert_unreached("should not error"), 71 complete: () => results.push("complete"), 72 }); 73 74 assert_array_equals(results, [1, 2, 3, "complete"], 75 "Subscription pushes iterable values out to Observable"); 76 77 // A second subscription should restart iteration. 78 observable.subscribe({ 79 next: (value) => results.push(value), 80 error: () => assert_unreached("should not error"), 81 complete: () => results.push("complete2"), 82 }); 83 84 assert_array_equals(results, [1, 2, 3, "complete", 1, 2, 3, "complete2"], 85 "Subscribing again causes another fresh iteration on an un-exhausted iterable"); 86 }, "from(): Iterable converts to Observable"); 87 88 // This test, and the variants below it, test the web-observable side-effects of 89 // converting an iterable object to an Observable. Specifically, it tracks 90 // exactly when the %Symbol.iterator% method is *retrieved* from the object, 91 // invoked, and what its error-throwing side-effects are. 92 // 93 // Even more specifically, we assert that the %Symbol.iterator% method is 94 // retrieved a single time when converting to an Observable, and then again when 95 // subscribing to the converted Observable. This makes it possible for the 96 // %Symbol.iterator% method getter to change return values in between conversion 97 // and subscription. See https://github.com/WICG/observable/issues/127 for 98 // related discussion. 99 test(() => { 100 const results = []; 101 102 const iterable = { 103 get [Symbol.iterator]() { 104 results.push("[Symbol.iterator] method GETTER"); 105 return function() { 106 results.push("[Symbol.iterator implementation]"); 107 return { 108 get next() { 109 results.push("next() method GETTER"); 110 return function() { 111 results.push("next() implementation"); 112 return {value: undefined, done: true}; 113 }; 114 }, 115 }; 116 }; 117 }, 118 }; 119 120 const observable = Observable.from(iterable); 121 assert_array_equals(results, ["[Symbol.iterator] method GETTER"]); 122 123 let thrownError = null; 124 observable.subscribe(); 125 assert_array_equals(results, [ 126 "[Symbol.iterator] method GETTER", 127 "[Symbol.iterator] method GETTER", 128 "[Symbol.iterator implementation]", 129 "next() method GETTER", 130 "next() implementation" 131 ]); 132 }, "from(): [Symbol.iterator] side-effects (one observable)"); 133 134 // This tests that once `Observable.from()` detects a non-null and non-undefined 135 // `[Symbol.iterator]` property, we've committed to converting as an iterable. 136 // If the value of that property is then not callable, we don't silently move on 137 // to the next conversion type — we throw a TypeError. 138 // 139 // That's because that's what TC39's `GetMethod()` [1] calls for, which is what 140 // `Observable.from()` first uses in the iterable conversion branch [2]. 141 // 142 // [1]: https://tc39.es/ecma262/multipage/abstract-operations.html#sec-getmethod 143 // [2]: http://wicg.github.io/observable/#from-iterable-conversion 144 test(() => { 145 let results = []; 146 const iterable = { 147 [Symbol.iterator]: 10, 148 }; 149 150 let errorThrown = null; 151 try { 152 Observable.from(iterable); 153 } catch(e) { 154 errorThrown = e; 155 } 156 157 assert_true(errorThrown instanceof TypeError); 158 }, "from(): [Symbol.iterator] not callable"); 159 160 test(() => { 161 let results = []; 162 const iterable = { 163 calledOnce: false, 164 get [Symbol.iterator]() { 165 if (this.calledOnce) { 166 // Return a non-callable primitive the second time `@@iterator` is 167 // called. 168 return 10; 169 } 170 171 this.calledOnce = true; 172 return this.validImplementation; 173 }, 174 validImplementation: () => { 175 return { 176 next() { return {done: true}; } 177 } 178 } 179 }; 180 181 let errorThrown = null; 182 183 const observable = Observable.from(iterable); 184 observable.subscribe({ 185 next: v => results.push("should not be called"), 186 error: e => { 187 errorThrown = e; 188 results.push(e); 189 }, 190 }); 191 192 assert_array_equals(results, [errorThrown], 193 "An error was plumbed through the Observable"); 194 assert_true(errorThrown instanceof TypeError); 195 }, "from(): [Symbol.iterator] not callable AFTER SUBSCRIBE throws"); 196 197 test(() => { 198 let results = []; 199 const iterable = { 200 calledOnce: false, 201 validImplementation: () => { 202 return { 203 next() { return {done: true}; } 204 } 205 }, 206 get [Symbol.iterator]() { 207 if (this.calledOnce) { 208 // Return null the second time `@@iterator` is called. 209 return null; 210 } 211 212 this.calledOnce = true; 213 return this.validImplementation; 214 } 215 }; 216 217 let errorThrown = null; 218 219 const observable = Observable.from(iterable); 220 observable.subscribe({ 221 next: v => results.push("should not be called"), 222 error: e => { 223 errorThrown = e; 224 results.push(e); 225 }, 226 }); 227 228 assert_array_equals(results, [errorThrown], 229 "An error was plumbed through the Observable"); 230 assert_true(errorThrown instanceof TypeError); 231 }, "from(): [Symbol.iterator] returns null AFTER SUBSCRIBE throws"); 232 233 test(() => { 234 let results = []; 235 const customError = new Error("@@iterator override error"); 236 237 const iterable = { 238 numTimesCalled: 0, 239 240 // The first time this getter is called, it returns a legitimate function 241 // that, when called, returns an iterator. Every other time it returns an 242 // error-throwing function that does not return an iterator. 243 get [Symbol.iterator]() { 244 this.numTimesCalled++; 245 results.push("[Symbol.iterator] method GETTER"); 246 247 if (this.numTimesCalled === 1) { 248 return this.validIteratorImplementation; 249 } else { 250 return this.errorThrowingIteratorImplementation; 251 } 252 }, 253 254 validIteratorImplementation: function() { 255 results.push("[Symbol.iterator implementation]"); 256 return { 257 get next() { 258 results.push("next() method GETTER"); 259 return function() { 260 results.push("next() implementation"); 261 return {value: undefined, done: true}; 262 } 263 } 264 }; 265 }, 266 errorThrowingIteratorImplementation: function() { 267 results.push("Error-throwing [Symbol.iterator] implementation"); 268 throw customError; 269 }, 270 }; 271 272 const observable = Observable.from(iterable); 273 assert_array_equals(results, [ 274 "[Symbol.iterator] method GETTER", 275 ]); 276 277 // Override iterable's `[Symbol.iterator]` protocol with an error-throwing 278 // function. We assert that on subscription, this method (the new `@@iterator` 279 // implementation), is called because only the raw JS object gets stored in 280 // the Observable that results in conversion. This raw value must get 281 // re-converted to an iterable once iteration is about to start. 282 283 let thrownError = null; 284 observable.subscribe({ 285 error: e => thrownError = e, 286 }); 287 288 assert_equals(thrownError, customError, 289 "Error thrown from next() is passed to the error() handler"); 290 assert_array_equals(results, [ 291 // Old: 292 "[Symbol.iterator] method GETTER", 293 // New: 294 "[Symbol.iterator] method GETTER", 295 "Error-throwing [Symbol.iterator] implementation" 296 ]); 297 }, "from(): [Symbol.iterator] is not cached"); 298 299 // Similar to the above test, but with more Observables! 300 test(() => { 301 const results = []; 302 let numTimesSymbolIteratorCalled = 0; 303 let numTimesNextCalled = 0; 304 305 const iterable = { 306 get [Symbol.iterator]() { 307 results.push("[Symbol.iterator] method GETTER"); 308 return this.internalIteratorImplementation; 309 }, 310 set [Symbol.iterator](func) { 311 this.internalIteratorImplementation = func; 312 }, 313 314 internalIteratorImplementation: function() { 315 results.push("[Symbol.iterator] implementation"); 316 return { 317 get next() { 318 results.push("next() method GETTER"); 319 return function() { 320 results.push("next() implementation"); 321 return {value: undefined, done: true}; 322 }; 323 }, 324 }; 325 }, 326 }; 327 328 const obs1 = Observable.from(iterable); 329 const obs2 = Observable.from(iterable); 330 const obs3 = Observable.from(iterable); 331 const obs4 = Observable.from(obs3); 332 assert_equals(obs3, obs4); 333 334 assert_array_equals(results, [ 335 "[Symbol.iterator] method GETTER", 336 "[Symbol.iterator] method GETTER", 337 "[Symbol.iterator] method GETTER", 338 ]); 339 340 obs1.subscribe(); 341 assert_array_equals(results, [ 342 // Old: 343 "[Symbol.iterator] method GETTER", 344 "[Symbol.iterator] method GETTER", 345 "[Symbol.iterator] method GETTER", 346 // New: 347 "[Symbol.iterator] method GETTER", 348 "[Symbol.iterator] implementation", 349 "next() method GETTER", 350 "next() implementation", 351 ]); 352 353 iterable[Symbol.iterator] = () => { 354 results.push("Error-throwing [Symbol.iterator] implementation"); 355 throw new Error('Symbol.iterator override error'); 356 }; 357 358 let errorCount = 0; 359 360 const observer = {error: e => errorCount++}; 361 obs2.subscribe(observer); 362 obs3.subscribe(observer); 363 obs4.subscribe(observer); 364 assert_equals(errorCount, 3, 365 "Error-throwing `@@iterator` implementation is called once per " + 366 "subscription"); 367 368 assert_array_equals(results, [ 369 // Old: 370 "[Symbol.iterator] method GETTER", 371 "[Symbol.iterator] method GETTER", 372 "[Symbol.iterator] method GETTER", 373 "[Symbol.iterator] method GETTER", 374 "[Symbol.iterator] implementation", 375 "next() method GETTER", 376 "next() implementation", 377 // New: 378 "[Symbol.iterator] method GETTER", 379 "Error-throwing [Symbol.iterator] implementation", 380 "[Symbol.iterator] method GETTER", 381 "Error-throwing [Symbol.iterator] implementation", 382 "[Symbol.iterator] method GETTER", 383 "Error-throwing [Symbol.iterator] implementation", 384 ]); 385 }, "from(): [Symbol.iterator] side-effects (many observables)"); 386 387 test(() => { 388 const customError = new Error('@@iterator next() error'); 389 const iterable = { 390 [Symbol.iterator]() { 391 return { 392 next() { 393 throw customError; 394 } 395 }; 396 } 397 }; 398 399 let thrownError = null; 400 Observable.from(iterable).subscribe({ 401 error: e => thrownError = e, 402 }); 403 404 assert_equals(thrownError, customError, 405 "Error thrown from next() is passed to the error() handler"); 406 }, "from(): [Symbol.iterator] next() throws error"); 407 408 promise_test(async () => { 409 const promise = Promise.resolve('value'); 410 const observable = Observable.from(promise); 411 412 assert_true(observable instanceof Observable, "Converts to Observable"); 413 414 const results = []; 415 416 observable.subscribe({ 417 next: (value) => results.push(value), 418 error: () => assert_unreached("error() is not called"), 419 complete: () => results.push("complete()"), 420 }); 421 422 assert_array_equals(results, [], "Observable does not emit synchronously"); 423 424 await promise; 425 426 assert_array_equals(results, ["value", "complete()"], 427 "Observable emits and completes after Promise resolves"); 428 }, "from(): Converts Promise to Observable"); 429 430 promise_test(async t => { 431 let unhandledRejectionHandlerCalled = false; 432 const unhandledRejectionHandler = () => { 433 unhandledRejectionHandlerCalled = true; 434 }; 435 436 self.addEventListener("unhandledrejection", unhandledRejectionHandler); 437 t.add_cleanup(() => self.removeEventListener("unhandledrejection", unhandledRejectionHandler)); 438 439 const promise = Promise.reject("reason"); 440 const observable = Observable.from(promise); 441 442 assert_true(observable instanceof Observable, "Converts to Observable"); 443 444 const results = []; 445 446 observable.subscribe({ 447 next: (value) => assert_unreached("next() not called"), 448 error: (error) => results.push(error), 449 complete: () => assert_unreached("complete() not called"), 450 }); 451 452 assert_array_equals(results, [], "Observable does not emit synchronously"); 453 454 let catchBlockEntered = false; 455 try { 456 await promise; 457 } catch { 458 catchBlockEntered = true; 459 } 460 461 assert_true(catchBlockEntered, "Catch block entered"); 462 assert_false(unhandledRejectionHandlerCalled, "No unhandledrejection event"); 463 assert_array_equals(results, ["reason"], 464 "Observable emits error() after Promise rejects"); 465 }, "from(): Converts rejected Promise to Observable. No " + 466 "`unhandledrejection` event when error is handled by subscription"); 467 468 promise_test(async t => { 469 let unhandledRejectionHandlerCalled = false; 470 const unhandledRejectionHandler = () => { 471 unhandledRejectionHandlerCalled = true; 472 }; 473 474 self.addEventListener("unhandledrejection", unhandledRejectionHandler); 475 t.add_cleanup(() => self.removeEventListener("unhandledrejection", unhandledRejectionHandler)); 476 477 let errorReported = null; 478 self.addEventListener("error", e => errorReported = e, { once: true }); 479 480 let catchBlockEntered = false; 481 try { 482 const promise = Promise.reject("custom reason"); 483 const observable = Observable.from(promise); 484 485 observable.subscribe(); 486 await promise; 487 } catch { 488 catchBlockEntered = true; 489 } 490 491 assert_true(catchBlockEntered, "Catch block entered"); 492 assert_false(unhandledRejectionHandlerCalled, 493 "No unhandledrejection event, because error got reported to global"); 494 assert_not_equals(errorReported, null, "Error was reported to the global"); 495 496 assert_true(errorReported.message.includes("custom reason"), 497 "Error message matches"); 498 assert_equals(errorReported.lineno, 0, "Error lineno is 0"); 499 assert_equals(errorReported.colno, 0, "Error lineno is 0"); 500 assert_equals(errorReported.error, "custom reason", 501 "Error object is equivalent"); 502 }, "from(): Rejections not handled by subscription are reported to the " + 503 "global, and still not sent as an unhandledrejection event"); 504 505 test(() => { 506 const results = []; 507 const observable = new Observable(subscriber => { 508 subscriber.next('from Observable'); 509 subscriber.complete(); 510 }); 511 512 observable[Symbol.iterator] = () => { 513 results.push('Symbol.iterator() called'); 514 return { 515 next() { 516 return {value: 'from @@iterator', done: true}; 517 } 518 }; 519 }; 520 521 Observable.from(observable).subscribe({ 522 next: v => results.push(v), 523 complete: () => results.push("complete"), 524 }); 525 526 assert_array_equals(results, ["from Observable", "complete"]); 527 }, "from(): Observable that implements @@iterator protocol gets converted " + 528 "as an Observable, not iterator"); 529 530 test(() => { 531 const results = []; 532 const promise = new Promise(resolve => { 533 resolve('from Promise'); 534 }); 535 536 promise[Symbol.iterator] = () => { 537 let done = false; 538 return { 539 next() { 540 if (!done) { 541 done = true; 542 return {value: 'from @@iterator', done: false}; 543 } else { 544 return {value: undefined, done: true}; 545 } 546 } 547 }; 548 }; 549 550 Observable.from(promise).subscribe({ 551 next: v => results.push(v), 552 complete: () => results.push("complete"), 553 }); 554 555 assert_array_equals(results, ["from @@iterator", "complete"]); 556 }, "from(): Promise that implements @@iterator protocol gets converted as " + 557 "an iterable, not Promise"); 558 559 // When the [Symbol.iterator] method on a given object is undefined, we don't 560 // try to convert the object to an Observable via the iterable protocol. The 561 // Observable specification *also* does the same thing if the [Symbol.iterator] 562 // method is *null*. That is, in that case we also skip the conversion via 563 // iterable protocol, and continue to try and convert the object as another type 564 // (in this case, a Promise). 565 promise_test(async () => { 566 const promise = new Promise(resolve => resolve('from Promise')); 567 assert_equals(promise[Symbol.iterator], undefined); 568 promise[Symbol.iterator] = null; 569 assert_equals(promise[Symbol.iterator], null); 570 571 const value = await new Promise(resolve => { 572 Observable.from(promise).subscribe(value => resolve(value)); 573 }); 574 575 assert_equals(value, 'from Promise'); 576 }, "from(): Promise whose [Symbol.iterator] returns null converts as Promise"); 577 578 // This is a more sensitive test, which asserts that even just trying to reach 579 // for the [Symbol.iterator] method on an object whose *getter* for the 580 // [Symbol.iterator] method throws an error, results in `Observable#from()` 581 // rethrowing that error. 582 test(() => { 583 const error = new Error('thrown from @@iterator getter'); 584 const obj = { 585 get [Symbol.iterator]() { 586 throw error; 587 } 588 } 589 590 try { 591 Observable.from(obj); 592 assert_unreached("from() conversion throws"); 593 } catch(e) { 594 assert_equals(e, error); 595 } 596 }, "from(): Rethrows the error when Converting an object whose @@iterator " + 597 "method *getter* throws an error"); 598 599 // This test exercises the line of spec prose that says: 600 // 601 // "If |asyncIteratorMethodRecord|'s [[Value]] is undefined or null, then jump 602 // to the step labeled 'From iterable'." 603 test(() => { 604 const sync_iterable = { 605 [Symbol.asyncIterator]: null, 606 [Symbol.iterator]() { 607 return { 608 value: 0, 609 next() { 610 if (this.value === 2) 611 return {value: undefined, done: true}; 612 else 613 return {value: this.value++, done: false}; 614 } 615 } 616 }, 617 }; 618 619 const results = []; 620 const source = Observable.from(sync_iterable).subscribe(v => results.push(v)); 621 assert_array_equals(results, [0, 1]); 622 }, "from(): Async iterable protocol null, converts as iterator"); 623 624 promise_test(async t => { 625 const results = []; 626 const async_iterable = { 627 [Symbol.asyncIterator]() { 628 results.push("[Symbol.asyncIterator]() invoked"); 629 return { 630 val: 0, 631 next() { 632 return new Promise(resolve => { 633 t.step_timeout(() => { 634 resolve({ 635 value: this.val, 636 done: this.val++ === 4 ? true : false, 637 }); 638 }, 400); 639 }); 640 }, 641 }; 642 }, 643 }; 644 645 const source = Observable.from(async_iterable); 646 assert_array_equals(results, []); 647 648 await new Promise(resolve => { 649 source.subscribe({ 650 next: v => { 651 results.push(`Observing ${v}`); 652 queueMicrotask(() => results.push(`next() microtask interleaving (v=${v})`)); 653 }, 654 complete: () => { 655 results.push('complete()'); 656 resolve(); 657 }, 658 }); 659 }); 660 661 assert_array_equals(results, [ 662 "[Symbol.asyncIterator]() invoked", 663 "Observing 0", 664 "next() microtask interleaving (v=0)", 665 "Observing 1", 666 "next() microtask interleaving (v=1)", 667 "Observing 2", 668 "next() microtask interleaving (v=2)", 669 "Observing 3", 670 "next() microtask interleaving (v=3)", 671 "complete()", 672 ]); 673 }, "from(): Asynchronous iterable conversion"); 674 675 // This test is a more chaotic version of the above. It ensures that a single 676 // Observable can handle multiple in-flight subscriptions to the same underlying 677 // async iterable without the two subscriptions competing. It asserts that the 678 // asynchronous values are pushed to the observers in the correct order. 679 promise_test(async t => { 680 const async_iterable = { 681 [Symbol.asyncIterator]() { 682 return { 683 val: 0, 684 next() { 685 // Returns a Promise that resolves in a random amount of time less 686 // than a second. 687 return new Promise(resolve => { 688 t.step_timeout(() => resolve({ 689 value: this.val, 690 done: this.val++ === 4 ? true : false, 691 }), 200); 692 }); 693 }, 694 }; 695 }, 696 }; 697 698 const results = []; 699 const source = Observable.from(async_iterable); 700 701 const promise = new Promise(resolve => { 702 source.subscribe({ 703 next: v => { 704 results.push(`${v}-first-sub`); 705 706 // Half-way through the first subscription, start another subscription. 707 if (v === 0) { 708 source.subscribe({ 709 next: v => results.push(`${v}-second-sub`), 710 complete: () => { 711 results.push('complete-second-sub'); 712 resolve(); 713 } 714 }); 715 } 716 }, 717 complete: () => { 718 results.push('complete-first-sub'); 719 resolve(); 720 } 721 }); 722 }); 723 724 await promise; 725 assert_array_equals(results, [ 726 '0-first-sub', 727 728 '1-first-sub', 729 '1-second-sub', 730 731 '2-first-sub', 732 '2-second-sub', 733 734 '3-first-sub', 735 '3-second-sub', 736 737 'complete-first-sub', 738 'complete-second-sub', 739 ]); 740 }, "from(): Asynchronous iterable multiple in-flight subscriptions"); 741 // This test is like the above, ensuring that multiple subscriptions to the same 742 // sync-iterable-converted-Observable can exist at a time. Since sync iterables 743 // push all of their values to the Observable synchronously, the way to do this 744 // is subscribe to the sync iterable Observable *inside* the next handler of the 745 // same Observable. 746 test(() => { 747 const results = []; 748 749 const array = [1, 2, 3, 4, 5]; 750 const source = Observable.from(array); 751 source.subscribe({ 752 next: v => { 753 results.push(`${v}-first-sub`); 754 if (v === 3) { 755 // Pushes all 5 values to `results` right after the first instance of `3`. 756 source.subscribe({ 757 next: v => results.push(`${v}-second-sub`), 758 complete: () => results.push('complete-second-sub'), 759 }); 760 } 761 }, 762 complete: () => results.push('complete-first-sub'), 763 }); 764 765 assert_array_equals(results, [ 766 // These values are pushed when there is only a single subscription. 767 '1-first-sub', '2-first-sub', '3-first-sub', 768 // These values are pushed in the correct order, for two subscriptions. 769 '4-first-sub', '4-second-sub', 770 '5-first-sub', '5-second-sub', 771 'complete-first-sub', 'complete-second-sub', 772 ]); 773 }, "from(): Sync iterable multiple in-flight subscriptions"); 774 775 promise_test(async () => { 776 const async_generator = async function*() { 777 yield 1; 778 yield 2; 779 yield 3; 780 }; 781 782 const results = []; 783 const source = Observable.from(async_generator()); 784 785 const subscribeFunction = function(resolve) { 786 source.subscribe({ 787 next: v => results.push(v), 788 complete: () => resolve(), 789 }); 790 } 791 await new Promise(subscribeFunction); 792 assert_array_equals(results, [1, 2, 3]); 793 await new Promise(subscribeFunction); 794 assert_array_equals(results, [1, 2, 3]); 795 }, "from(): Asynchronous generator conversion: can only be used once"); 796 797 // The value returned by an async iterator object's `next()` method is supposed 798 // to be a Promise. But this requirement "isn't enforced": see [1]. Therefore, 799 // the Observable spec unconditionally wraps the return value in a resolved 800 // Promise, as is standard practice [2]. 801 // 802 // This test ensures that even if the object returned from an async iterator's 803 // `next()` method is a synchronously-available object with `done: true` 804 // (instead of a Promise), the `done` property is STILL not retrieved 805 // synchronously. In other words, we test that the Promise-wrapping is 806 // implemented. 807 // 808 // [1]: https://tc39.es/ecma262/#table-async-iterator-r 809 // [2]: https://matrixlogs.bakkot.com/WHATWG/2024-08-30#L30 810 promise_test(async () => { 811 const results = []; 812 813 const async_iterable = { 814 [Symbol.asyncIterator]() { 815 return { 816 next() { 817 return { 818 value: undefined, 819 get done() { 820 results.push('done() GETTER called'); 821 return true; 822 }, 823 }; 824 }, 825 }; 826 }, 827 }; 828 829 const source = Observable.from(async_iterable); 830 assert_array_equals(results, []); 831 832 queueMicrotask(() => results.push('Microtask queued before subscription')); 833 source.subscribe(); 834 assert_array_equals(results, []); 835 836 await Promise.resolve(); 837 assert_array_equals(results, [ 838 "Microtask queued before subscription", 839 "done() GETTER called", 840 ]); 841 }, "from(): Promise-wrapping semantics of IteratorResult interface"); 842 843 // Errors thrown from [Symbol.asyncIterator] are propagated to the observer 844 // synchronously. This is because in language constructs (i.e., for-await of 845 // loops) that invoke [Symbol.asyncIterator]() that throw errors, the errors are 846 // synchronously propagated to script outside of the loop, and are catchable. 847 // Observables follow this precedent. 848 test(() => { 849 const error = new Error("[Symbol.asyncIterator] error"); 850 const results = []; 851 const async_iterable = { 852 [Symbol.asyncIterator]() { 853 results.push("[Symbol.asyncIterator]() invoked"); 854 throw error; 855 } 856 }; 857 858 Observable.from(async_iterable).subscribe({ 859 error: e => results.push(e), 860 }); 861 862 assert_array_equals(results, [ 863 "[Symbol.asyncIterator]() invoked", 864 error, 865 ]); 866 }, "from(): Errors thrown in Symbol.asyncIterator() are propagated synchronously"); 867 868 // AsyncIterable: next() throws exception instead of return Promise. Any errors 869 // that occur during the retrieval of `next()` always result in a rejected 870 // Promise. Therefore, the error makes it to the Observer with microtask timing. 871 promise_test(async () => { 872 const nextError = new Error('next error'); 873 const async_iterable = { 874 [Symbol.asyncIterator]() { 875 return { 876 get next() { 877 throw nextError; 878 } 879 }; 880 } 881 }; 882 883 const results = []; 884 Observable.from(async_iterable).subscribe({ 885 error: e => results.push(e), 886 }); 887 888 assert_array_equals(results, []); 889 // Wait one microtask since the error will be propagated through a rejected 890 // Promise managed by the async iterable conversion semantics. 891 await Promise.resolve(); 892 assert_array_equals(results, [nextError]); 893 }, "from(): Errors thrown in async iterator's next() GETTER are propagated " + 894 "in a microtask"); 895 promise_test(async () => { 896 const nextError = new Error('next error'); 897 const async_iterable = { 898 [Symbol.asyncIterator]() { 899 return { 900 next() { 901 throw nextError; 902 } 903 }; 904 } 905 }; 906 907 const results = []; 908 Observable.from(async_iterable).subscribe({ 909 error: e => results.push(e), 910 }); 911 912 assert_array_equals(results, []); 913 await Promise.resolve(); 914 assert_array_equals(results, [nextError]); 915 }, "from(): Errors thrown in async iterator's next() are propagated in a microtask"); 916 917 test(() => { 918 const results = []; 919 const iterable = { 920 [Symbol.iterator]() { 921 return { 922 val: 0, 923 next() { 924 results.push(`IteratorRecord#next() pushing ${this.val}`); 925 return { 926 value: this.val, 927 done: this.val++ === 10 ? true : false, 928 }; 929 }, 930 return() { 931 results.push(`IteratorRecord#return() called with this.val=${this.val}`); 932 }, 933 }; 934 }, 935 }; 936 937 const ac = new AbortController(); 938 Observable.from(iterable).subscribe(v => { 939 results.push(`Observing ${v}`); 940 if (v === 3) { 941 ac.abort(); 942 } 943 }, {signal: ac.signal}); 944 945 assert_array_equals(results, [ 946 "IteratorRecord#next() pushing 0", 947 "Observing 0", 948 "IteratorRecord#next() pushing 1", 949 "Observing 1", 950 "IteratorRecord#next() pushing 2", 951 "Observing 2", 952 "IteratorRecord#next() pushing 3", 953 "Observing 3", 954 "IteratorRecord#return() called with this.val=4", 955 ]); 956 }, "from(): Aborting sync iterable midway through iteration both stops iteration " + 957 "and invokes `IteratorRecord#return()"); 958 // Like the above test, but for async iterables. 959 promise_test(async t => { 960 const results = []; 961 const iterable = { 962 [Symbol.asyncIterator]() { 963 return { 964 val: 0, 965 next() { 966 results.push(`IteratorRecord#next() pushing ${this.val}`); 967 return { 968 value: this.val, 969 done: this.val++ === 10 ? true : false, 970 }; 971 }, 972 return(reason) { 973 results.push(`IteratorRecord#return() called with reason=${reason}`); 974 return {done: true}; 975 }, 976 }; 977 }, 978 }; 979 980 const ac = new AbortController(); 981 await new Promise(resolve => { 982 Observable.from(iterable).subscribe(v => { 983 results.push(`Observing ${v}`); 984 if (v === 3) { 985 ac.abort(`Aborting because v=${v}`); 986 resolve(); 987 } 988 }, {signal: ac.signal}); 989 }); 990 991 assert_array_equals(results, [ 992 "IteratorRecord#next() pushing 0", 993 "Observing 0", 994 "IteratorRecord#next() pushing 1", 995 "Observing 1", 996 "IteratorRecord#next() pushing 2", 997 "Observing 2", 998 "IteratorRecord#next() pushing 3", 999 "Observing 3", 1000 "IteratorRecord#return() called with reason=Aborting because v=3", 1001 ]); 1002 }, "from(): Aborting async iterable midway through iteration both stops iteration " + 1003 "and invokes `IteratorRecord#return()"); 1004 1005 test(() => { 1006 const iterable = { 1007 [Symbol.iterator]() { 1008 return { 1009 val: 0, 1010 next() { 1011 return {value: this.val, done: this.val++ === 10 ? true : false}; 1012 }, 1013 // Not returning an Object results in a TypeError being thrown. 1014 return(reason) {}, 1015 }; 1016 }, 1017 }; 1018 1019 let thrownError = null; 1020 const ac = new AbortController(); 1021 Observable.from(iterable).subscribe(v => { 1022 if (v === 3) { 1023 try { 1024 ac.abort(`Aborting because v=${v}`); 1025 } catch (e) { 1026 thrownError = e; 1027 } 1028 } 1029 }, {signal: ac.signal}); 1030 1031 assert_not_equals(thrownError, null, "abort() threw an Error"); 1032 assert_true(thrownError instanceof TypeError); 1033 assert_true(thrownError.message.includes('return()')); 1034 assert_true(thrownError.message.includes('Object')); 1035 }, "from(): Sync iterable: `Iterator#return()` must return an Object, or an " + 1036 "error is thrown"); 1037 // This test is just like the above but for async iterables. It asserts that a 1038 // Promise is rejected when `return()` does not return an Object. 1039 promise_test(async t => { 1040 const iterable = { 1041 [Symbol.asyncIterator]() { 1042 return { 1043 val: 0, 1044 next() { 1045 return {value: this.val, done: this.val++ === 10 ? true : false}; 1046 }, 1047 // Not returning an Object results in a rejected Promise. 1048 return(reason) {}, 1049 }; 1050 }, 1051 }; 1052 1053 const unhandled_rejection_promise = new Promise((resolve, reject) => { 1054 const unhandled_rejection_handler = e => resolve(e.reason); 1055 self.addEventListener("unhandledrejection", unhandled_rejection_handler); 1056 t.add_cleanup(() => 1057 self.removeEventListener("unhandledrejection", unhandled_rejection_handler)); 1058 1059 t.step_timeout(() => reject('Timeout'), 3000); 1060 }); 1061 1062 const ac = new AbortController(); 1063 await new Promise(resolve => { 1064 Observable.from(iterable).subscribe(v => { 1065 if (v === 3) { 1066 ac.abort(`Aborting because v=${v}`); 1067 resolve(); 1068 } 1069 }, {signal: ac.signal}); 1070 }); 1071 1072 const reason = await unhandled_rejection_promise; 1073 assert_true(reason instanceof TypeError); 1074 assert_true(reason.message.includes('return()')); 1075 assert_true(reason.message.includes('Object')); 1076 }, "from(): Async iterable: `Iterator#return()` must return an Object, or a " + 1077 "Promise rejects asynchronously"); 1078 1079 // This test exercises the logic of `GetIterator()` async->sync fallback 1080 // logic. Specifically, we have an object that is an async iterable — that is, 1081 // it has a callback [Symbol.asyncIterator] implementation. Observable.from() 1082 // detects this, and commits to converting the object from the async iterable 1083 // protocol. Then, after conversion but before subscription, the object is 1084 // modified such that it no longer implements the async iterable protocol. 1085 // 1086 // But since it still implements the *iterable* protocol, ECMAScript's 1087 // `GetIterator()` abstract algorithm [1] is fully exercised, which is spec'd to 1088 // fall-back to the synchronous iterable protocol if it exists, and create a 1089 // fully async iterable out of the synchronous iterable. 1090 // 1091 // [1]: https://tc39.es/ecma262/#sec-getiterator 1092 promise_test(async () => { 1093 const results = []; 1094 const async_iterable = { 1095 asyncIteratorGotten: false, 1096 get [Symbol.asyncIterator]() { 1097 results.push("[Symbol.asyncIterator] GETTER"); 1098 if (this.asyncIteratorGotten) { 1099 return null; // Both null and undefined work here. 1100 } 1101 1102 this.asyncIteratorGotten = true; 1103 // The only requirement for `this` to be converted as an async 1104 // iterable -> Observable is that the return value be callable (i.e., a function). 1105 return function() {}; 1106 }, 1107 1108 [Symbol.iterator]() { 1109 results.push('[Symbol.iterator]() invoked as fallback'); 1110 return { 1111 val: 0, 1112 next() { 1113 return { 1114 value: this.val, 1115 done: this.val++ === 4 ? true : false, 1116 }; 1117 }, 1118 }; 1119 }, 1120 }; 1121 1122 const source = Observable.from(async_iterable); 1123 assert_array_equals(results, [ 1124 "[Symbol.asyncIterator] GETTER", 1125 ]); 1126 1127 await new Promise((resolve, reject) => { 1128 source.subscribe({ 1129 next: v => { 1130 results.push(`Observing ${v}`); 1131 queueMicrotask(() => results.push(`next() microtask interleaving (v=${v})`)); 1132 }, 1133 error: e => reject(e), 1134 complete: () => { 1135 results.push('complete()'); 1136 resolve(); 1137 }, 1138 }); 1139 }); 1140 1141 assert_array_equals(results, [ 1142 // Old: 1143 "[Symbol.asyncIterator] GETTER", 1144 // New: 1145 "[Symbol.asyncIterator] GETTER", 1146 "[Symbol.iterator]() invoked as fallback", 1147 "Observing 0", 1148 "next() microtask interleaving (v=0)", 1149 "Observing 1", 1150 "next() microtask interleaving (v=1)", 1151 "Observing 2", 1152 "next() microtask interleaving (v=2)", 1153 "Observing 3", 1154 "next() microtask interleaving (v=3)", 1155 "complete()", 1156 ]); 1157 }, "from(): Asynchronous iterable conversion, with synchronous iterable fallback"); 1158 1159 test(() => { 1160 const results = []; 1161 let generatorFinalized = false; 1162 1163 const generator = function*() { 1164 try { 1165 for (let n = 0; n < 10; n++) { 1166 yield n; 1167 } 1168 } finally { 1169 generatorFinalized = true; 1170 } 1171 }; 1172 1173 const observable = Observable.from(generator()); 1174 const abortController = new AbortController(); 1175 1176 observable.subscribe(n => { 1177 results.push(n); 1178 if (n === 3) { 1179 abortController.abort(); 1180 } 1181 }, {signal: abortController.signal}); 1182 1183 assert_array_equals(results, [0, 1, 2, 3]); 1184 assert_true(generatorFinalized); 1185 }, "from(): Generator finally block runs when subscription is aborted"); 1186 1187 test(() => { 1188 const results = []; 1189 let generatorFinalized = false; 1190 1191 const generator = function*() { 1192 try { 1193 for (let n = 0; n < 10; n++) { 1194 yield n; 1195 } 1196 } catch { 1197 assert_unreached("generator should not be aborted"); 1198 } finally { 1199 generatorFinalized = true; 1200 } 1201 }; 1202 1203 const observable = Observable.from(generator()); 1204 1205 observable.subscribe((n) => { 1206 results.push(n); 1207 }); 1208 1209 assert_array_equals(results, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); 1210 assert_true(generatorFinalized); 1211 }, "from(): Generator finally block run when Observable completes"); 1212 1213 test(() => { 1214 const results = []; 1215 let generatorFinalized = false; 1216 1217 const generator = function*() { 1218 try { 1219 for (let n = 0; n < 10; n++) { 1220 yield n; 1221 } 1222 throw new Error('from the generator'); 1223 } finally { 1224 generatorFinalized = true; 1225 } 1226 }; 1227 1228 const observable = Observable.from(generator()); 1229 1230 observable.subscribe({ 1231 next: n => results.push(n), 1232 error: e => results.push(e.message) 1233 }); 1234 1235 assert_array_equals(results, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, "from the generator"]); 1236 assert_true(generatorFinalized); 1237 }, "from(): Generator finally block run when Observable errors"); 1238 1239 promise_test(async t => { 1240 const results = []; 1241 let generatorFinalized = false; 1242 1243 async function* asyncGenerator() { 1244 try { 1245 for (let n = 0; n < 10; n++) { 1246 yield n; 1247 } 1248 } finally { 1249 generatorFinalized = true; 1250 } 1251 } 1252 1253 const observable = Observable.from(asyncGenerator()); 1254 const abortController = new AbortController(); 1255 1256 await new Promise((resolve) => { 1257 observable.subscribe((n) => { 1258 results.push(n); 1259 if (n === 3) { 1260 abortController.abort(); 1261 resolve(); 1262 } 1263 }, {signal: abortController.signal}); 1264 }); 1265 1266 assert_array_equals(results, [0, 1, 2, 3]); 1267 assert_true(generatorFinalized); 1268 }, "from(): Async generator finally block run when subscription is aborted"); 1269 1270 promise_test(async t => { 1271 const results = []; 1272 let generatorFinalized = false; 1273 1274 async function* asyncGenerator() { 1275 try { 1276 for (let n = 0; n < 10; n++) { 1277 yield n; 1278 } 1279 } finally { 1280 generatorFinalized = true; 1281 } 1282 } 1283 1284 const observable = Observable.from(asyncGenerator()); 1285 1286 await new Promise(resolve => { 1287 observable.subscribe({ 1288 next: n => results.push(n), 1289 complete: () => resolve(), 1290 }); 1291 }); 1292 1293 assert_array_equals(results, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); 1294 assert_true(generatorFinalized); 1295 }, "from(): Async generator finally block runs when Observable completes"); 1296 1297 promise_test(async t => { 1298 const results = []; 1299 let generatorFinalized = false; 1300 1301 async function* asyncGenerator() { 1302 try { 1303 for (let n = 0; n < 10; n++) { 1304 if (n === 4) { 1305 throw new Error('from the async generator'); 1306 } 1307 yield n; 1308 } 1309 } finally { 1310 generatorFinalized = true; 1311 } 1312 } 1313 1314 const observable = Observable.from(asyncGenerator()); 1315 1316 await new Promise((resolve) => { 1317 observable.subscribe({ 1318 next: (n) => results.push(n), 1319 error: (e) => { 1320 results.push(e.message); 1321 resolve(); 1322 } 1323 }); 1324 }); 1325 1326 assert_array_equals(results, [0, 1, 2, 3, "from the async generator"]); 1327 assert_true(generatorFinalized); 1328 }, "from(): Async generator finally block run when Observable errors"); 1329 1330 // Test what happens when `return()` throws an error upon abort. 1331 test(() => { 1332 const results = []; 1333 const iterable = { 1334 [Symbol.iterator]() { 1335 return { 1336 val: 0, 1337 next() { 1338 results.push('next() called'); 1339 return {value: this.val, done: this.val++ === 10 ? true : false}; 1340 }, 1341 return() { 1342 results.push('return() about to throw an error'); 1343 throw new Error('return() error'); 1344 }, 1345 }; 1346 } 1347 }; 1348 1349 const ac = new AbortController(); 1350 const source = Observable.from(iterable); 1351 source.subscribe(v => { 1352 if (v === 3) { 1353 try { 1354 ac.abort(); 1355 } catch (e) { 1356 results.push(`AbortController#abort() threw an error: ${e.message}`); 1357 } 1358 } 1359 }, {signal: ac.signal}); 1360 1361 assert_array_equals(results, [ 1362 'next() called', 1363 'next() called', 1364 'next() called', 1365 'next() called', 1366 'return() about to throw an error', 1367 'AbortController#abort() threw an error: return() error', 1368 ]); 1369 }, "from(): Sync iterable: error thrown from IteratorRecord#return() can be " + 1370 "synchronously caught"); 1371 promise_test(async t => { 1372 const results = []; 1373 const iterable = { 1374 [Symbol.asyncIterator]() { 1375 return { 1376 val: 0, 1377 next() { 1378 results.push('next() called'); 1379 return {value: this.val, done: this.val++ === 10 ? true : false}; 1380 }, 1381 return() { 1382 results.push('return() about to throw an error'); 1383 // For async iterables, errors thrown in `return()` end up in a 1384 // returned rejected Promise, so no error appears on the stack 1385 // immediately. See [1]. 1386 // 1387 // [1]: https://whatpr.org/webidl/1397.html#async-iterator-close. 1388 throw new Error('return() error'); 1389 }, 1390 }; 1391 } 1392 }; 1393 1394 const unhandled_rejection_promise = new Promise((resolve, reject) => { 1395 const unhandled_rejection_handler = e => resolve(e.reason); 1396 self.addEventListener("unhandledrejection", unhandled_rejection_handler); 1397 t.add_cleanup(() => 1398 self.removeEventListener("unhandledrejection", unhandled_rejection_handler)); 1399 1400 t.step_timeout(() => reject('Timeout'), 1500); 1401 }); 1402 1403 const ac = new AbortController(); 1404 const source = Observable.from(iterable); 1405 await new Promise((resolve, reject) => { 1406 source.subscribe(v => { 1407 if (v === 3) { 1408 try { 1409 ac.abort(); 1410 results.push('No error thrown synchronously'); 1411 resolve('No error thrown synchronously'); 1412 } catch (e) { 1413 results.push(`AbortController#abort() threw an error: ${e.message}`); 1414 reject(e); 1415 } 1416 } 1417 }, {signal: ac.signal}); 1418 }); 1419 1420 assert_array_equals(results, [ 1421 'next() called', 1422 'next() called', 1423 'next() called', 1424 'next() called', 1425 'return() about to throw an error', 1426 'No error thrown synchronously', 1427 ]); 1428 1429 const reason = await unhandled_rejection_promise; 1430 assert_true(reason instanceof Error); 1431 assert_equals(reason.message, "return() error", 1432 "Custom error text passed through rejected Promise"); 1433 }, "from(): Async iterable: error thrown from IteratorRecord#return() is " + 1434 "wrapped in rejected Promise"); 1435 1436 test(() => { 1437 const results = []; 1438 const iterable = { 1439 getter() { 1440 results.push('GETTER called'); 1441 return () => { 1442 results.push('Obtaining iterator'); 1443 return { 1444 next() { 1445 results.push('next() running'); 1446 return {done: true}; 1447 } 1448 }; 1449 }; 1450 } 1451 }; 1452 1453 Object.defineProperty(iterable, Symbol.iterator, { 1454 get: iterable.getter 1455 }); 1456 { 1457 const source = Observable.from(iterable); 1458 assert_array_equals(results, ["GETTER called"]); 1459 source.subscribe({}, {signal: AbortSignal.abort()}); 1460 assert_array_equals(results, ["GETTER called"]); 1461 } 1462 iterable[Symbol.iterator] = undefined; 1463 Object.defineProperty(iterable, Symbol.asyncIterator, { 1464 get: iterable.getter 1465 }); 1466 { 1467 const source = Observable.from(iterable); 1468 assert_array_equals(results, ["GETTER called", "GETTER called"]); 1469 source.subscribe({}, {signal: AbortSignal.abort()}); 1470 assert_array_equals(results, ["GETTER called", "GETTER called"]); 1471 } 1472 }, "from(): Subscribing to an iterable Observable with an aborted signal " + 1473 "does not call next()"); 1474 1475 test(() => { 1476 let results = []; 1477 1478 const iterable = { 1479 controller: null, 1480 calledOnce: false, 1481 getter() { 1482 results.push('GETTER called'); 1483 if (!this.calledOnce) { 1484 this.calledOnce = true; 1485 return () => { 1486 results.push('NOT CALLED'); 1487 // We don't need to return anything here. The only time this path is 1488 // hit is during `Observable.from()` which doesn't actually obtain an 1489 // iterator. It just samples the iterable protocol property to ensure 1490 // that it's valid. 1491 }; 1492 } 1493 1494 // This path is only called the second time the iterator protocol getter 1495 // is run. 1496 this.controller.abort(); 1497 return () => { 1498 results.push('iterator obtained'); 1499 return { 1500 val: 0, 1501 next() { 1502 results.push('next() called'); 1503 return {done: true}; 1504 }, 1505 return() { 1506 results.push('return() called'); 1507 } 1508 }; 1509 }; 1510 } 1511 }; 1512 1513 // Test for sync iterators. 1514 { 1515 const ac = new AbortController(); 1516 iterable.controller = ac; 1517 Object.defineProperty(iterable, Symbol.iterator, { 1518 get: iterable.getter, 1519 }); 1520 1521 const source = Observable.from(iterable); 1522 assert_false(ac.signal.aborted, "[Sync iterator]: signal is not yet aborted after from() conversion"); 1523 assert_array_equals(results, ["GETTER called"]); 1524 1525 source.subscribe({ 1526 next: n => results.push(n), 1527 complete: () => results.push('complete'), 1528 }, {signal: ac.signal}); 1529 assert_true(ac.signal.aborted, "[Sync iterator]: signal is aborted during subscription"); 1530 assert_array_equals(results, ["GETTER called", "GETTER called", "iterator obtained"]); 1531 } 1532 1533 results = []; 1534 1535 // Test for async iterators. 1536 { 1537 // Reset `iterable` so it can be reused. 1538 const ac = new AbortController(); 1539 iterable.controller = ac; 1540 iterable.calledOnce = false; 1541 iterable[Symbol.iterator] = undefined; 1542 Object.defineProperty(iterable, Symbol.asyncIterator, { 1543 get: iterable.getter 1544 }); 1545 1546 const source = Observable.from(iterable); 1547 assert_false(ac.signal.aborted, "[Async iterator]: signal is not yet aborted after from() conversion"); 1548 assert_array_equals(results, ["GETTER called"]); 1549 1550 source.subscribe({ 1551 next: n => results.push(n), 1552 complete: () => results.push('complete'), 1553 }, {signal: ac.signal}); 1554 assert_true(ac.signal.aborted, "[Async iterator]: signal is aborted during subscription"); 1555 assert_array_equals(results, ["GETTER called", "GETTER called", "iterator obtained"]); 1556 } 1557 }, "from(): When iterable conversion aborts the subscription, next() is " + 1558 "never called"); 1559 1560 // This test asserts some very subtle behavior with regard to async iterables 1561 // and a mid-subscription signal abort. Specifically it detects that a signal 1562 // abort ensures that the `next()` method is not called again on the iterator 1563 // again, BUT detects that pending Promise from the *previous* `next()` call 1564 // still has its IteratorResult object examined. I.e., the implementation 1565 // inspecting the `done` attribute on the resolved IteratorResult is observable 1566 // event after abort() takes place. 1567 promise_test(async () => { 1568 const results = []; 1569 let resolveNext = null; 1570 1571 const iterable = { 1572 [Symbol.asyncIterator]() { 1573 return { 1574 next() { 1575 results.push('next() called'); 1576 return new Promise(resolve => { 1577 resolveNext = resolve; 1578 }); 1579 }, 1580 return() { 1581 results.push('return() called'); 1582 } 1583 }; 1584 } 1585 }; 1586 1587 const ac = new AbortController(); 1588 const source = Observable.from(iterable); 1589 source.subscribe({ 1590 next: v => results.push(v), 1591 complete: () => results.push('complete'), 1592 }, {signal: ac.signal}); 1593 1594 assert_array_equals(results, [ 1595 "next() called", 1596 ]); 1597 1598 // First abort, ensuring `return()` is called. 1599 ac.abort(); 1600 1601 assert_array_equals(results, [ 1602 "next() called", 1603 "return() called", 1604 ]); 1605 1606 // Then resolve the pending `next()` Promise to an object whose `done` getter 1607 // reports to the test whether it was accessed. We have to wait one microtask 1608 // for the internal Observable implementation to finish "reacting" to said 1609 // `next()` promise resolution, for it to grab the `done` attribute. 1610 await new Promise(resolveOuter => { 1611 resolveNext({ 1612 get done() { 1613 results.push('IteratorResult.done GETTER'); 1614 resolveOuter(); 1615 return true; 1616 } 1617 }); 1618 }); 1619 1620 assert_array_equals(results, [ 1621 "next() called", 1622 "return() called", 1623 "IteratorResult.done GETTER", 1624 // Note that "next() called" does not make another appearance. 1625 ]); 1626 }, "from(): Aborting an async iterable subscription stops subsequent next() " + 1627 "calls, but old next() Promise reactions are web-observable"); 1628 1629 test(() => { 1630 const results = []; 1631 const iterable = { 1632 [Symbol.iterator]() { 1633 return { 1634 val: 0, 1635 next() { 1636 return {value: this.val, done: this.val++ === 4 ? true : false}; 1637 }, 1638 return() { 1639 results.push('return() called'); 1640 }, 1641 }; 1642 } 1643 }; 1644 1645 const source = Observable.from(iterable); 1646 const ac = new AbortController(); 1647 source.subscribe({ 1648 next: v => results.push(v), 1649 complete: () => results.push('complete'), 1650 }, {signal: ac.signal}); 1651 1652 ac.abort(); // Must do nothing! 1653 assert_array_equals(results, [0, 1, 2, 3, 'complete']); 1654 }, "from(): Abort after complete does NOT call IteratorRecord#return()"); 1655 1656 test(() => { 1657 const controller = new AbortController(); 1658 // Invalid @@asyncIterator protocol that also aborts the subscription. By the 1659 // time the invalid-ness of the protocol is detected, the controller has been 1660 // aborted, meaning that invalid-ness cannot manifest itself in the form of an 1661 // error that goes to the Observable's subscriber. Instead, it gets reported 1662 // to the global. 1663 const asyncIterable = { 1664 calledOnce: false, 1665 get[Symbol.asyncIterator]() { 1666 // This `calledOnce` path is to ensure the Observable first converts 1667 // correctly via `Observable.from()`, but *later* fails in the path where 1668 // `@@asyncIterator` is null. 1669 if (this.calledOnce) { 1670 controller.abort(); 1671 return null; 1672 } else { 1673 this.calledOnce = true; 1674 return this.validImplementation; 1675 } 1676 }, 1677 validImplementation() { 1678 controller.abort(); 1679 return null; 1680 } 1681 }; 1682 1683 let reportedError = null; 1684 self.addEventListener("error", e => reportedError = e.error, {once: true}); 1685 1686 let errorThrown = null; 1687 const observable = Observable.from(asyncIterable); 1688 observable.subscribe({ 1689 error: e => errorThrown = e, 1690 }, {signal: controller.signal}); 1691 1692 assert_equals(errorThrown, null, "Protocol error is not surfaced to the Subscriber"); 1693 1694 assert_not_equals(reportedError, null, "Protocol error is reported to the global"); 1695 assert_true(reportedError instanceof TypeError); 1696 }, "Invalid async iterator protocol error is surfaced before Subscriber#signal is consulted"); 1697 1698 test(() => { 1699 const controller = new AbortController(); 1700 const iterable = { 1701 calledOnce: false, 1702 get[Symbol.iterator]() { 1703 if (this.calledOnce) { 1704 controller.abort(); 1705 return null; 1706 } else { 1707 this.calledOnce = true; 1708 return this.validImplementation; 1709 } 1710 }, 1711 validImplementation() { 1712 controller.abort(); 1713 return null; 1714 } 1715 }; 1716 1717 let reportedError = null; 1718 self.addEventListener("error", e => reportedError = e.error, {once: true}); 1719 1720 let errorThrown = null; 1721 const observable = Observable.from(iterable); 1722 observable.subscribe({ 1723 error: e => errorThrown = e, 1724 }, {signal: controller.signal}); 1725 1726 assert_equals(errorThrown, null, "Protocol error is not surfaced to the Subscriber"); 1727 1728 assert_not_equals(reportedError, null, "Protocol error is reported to the global"); 1729 assert_true(reportedError instanceof TypeError); 1730 }, "Invalid iterator protocol error is surfaced before Subscriber#signal is consulted"); 1731 1732 // Regression test for https://github.com/WICG/observable/issues/208. 1733 promise_test(async () => { 1734 let errorReported = false; 1735 self.onerror = e => errorReported = true; 1736 1737 // `first()` aborts the subscription after the first item is encountered. 1738 const value = await Observable.from([1, 2, 3]).first(); 1739 assert_false(errorReported); 1740 }, "No error is reported when aborting a subscription to a sync iterator " + 1741 "that has no `return()` implementation");