| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 5fbf6c5 commit dc51c79
4 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -431,14 +431,16 @@ the write. Use [`ondrain()`][] to wait for capacity rather than polling. | |||
| 431 | 431 | the pending `end()` call; it does not fail the writer itself. | |
| 432 | 432 | * Returns: {Promise} Fulfills with the total number of bytes written. | |
| 433 | 433 | ||
| 434 | - Signal that no more data will be written. | ||
| 434 | + Signals that no more data will be written and waits for buffered data to drain. | ||
| 435 | 435 | ||
| 436 | 436 | #### `writer.endSync()` | |
| 437 | 437 | ||
| 438 | - * Returns: {number} Total bytes written, or `-1` if the writer is not open. | ||
| 438 | + * Returns: {number} Total bytes written, or `-1` if ending cannot complete | ||
| 439 | + synchronously. | ||
| 439 | 440 | ||
| 440 | - Synchronous variant of `writer.end()`. Returns `-1` if the writer is already | ||
| 441 | - closed or errored. Can be used as a try-fallback pattern: | ||
| 441 | + Synchronous variant of `writer.end()`. A return value of `-1` means closing has | ||
| 442 | + started but requires asynchronous draining. Use the try-fallback pattern to | ||
| 443 | + await completion: | ||
| 442 | 444 | ||
| 443 | 445 | ```cjs | |
| 444 | 446 | const result = writer.endSync(); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -14,6 +14,8 @@ const { | |||
| 14 | 14 | PromiseReject, | |
| 15 | 15 | PromiseResolve, | |
| 16 | 16 | PromiseWithResolvers, | |
| 17 | + SafePromisePrototypeFinally, | ||
| 18 | + SafePromiseRace, | ||
| 17 | 19 | SafeSet, | |
| 18 | 20 | Symbol, | |
| 19 | 21 | SymbolAsyncDispose, | |
@@ -77,6 +79,22 @@ const kEnd = Symbol('kEnd'); | |||
| 77 | 79 | const kAbort = Symbol('kAbort'); | |
| 78 | 80 | const kCanWrite = Symbol('kCanWrite'); | |
| 79 | 81 | const kOnBufferDrained = Symbol('kOnBufferDrained'); | |
| 82 | + const kOnEndDrained = Symbol('kOnEndDrained'); | ||
| 83 | + const kPendingWriteRemoved = Symbol('kPendingWriteRemoved'); | ||
| 84 | + | ||
| 85 | + function raceEndWithSignal(promise, signal) { | ||
| 86 | + if (!signal) return promise; | ||
| 87 | + | ||
| 88 | + const { promise: aborted, reject } = PromiseWithResolvers(); | ||
| 89 | + const onAbort = () => reject(signal.reason); | ||
| 90 | + signal.addEventListener('abort', onAbort, { __proto__: null, once: true }); | ||
| 91 | + if (signal.aborted) onAbort(); | ||
| 92 | + | ||
| 93 | + return SafePromisePrototypeFinally( | ||
| 94 | + SafePromiseRace([promise, aborted]), | ||
| 95 | + () => signal.removeEventListener('abort', onAbort), | ||
| 96 | + ); | ||
| 97 | + } | ||
| 80 | 98 | ||
| 81 | 99 | // ============================================================================= | |
| 82 | 100 | // Broadcast Implementation | |
@@ -100,6 +118,7 @@ class BroadcastImpl { | |||
| 100 | 118 | constructor(options) { | |
| 101 | 119 | this.#options = options; | |
| 102 | 120 | this[kOnBufferDrained] = null; | |
| 121 | + this[kOnEndDrained] = null; | ||
| 103 | 122 | } | |
| 104 | 123 | ||
| 105 | 124 | setWriter(writer) { | |
@@ -183,6 +202,7 @@ class BroadcastImpl { | |||
| 183 | 202 | if (self.#deleteConsumer(state)) { | |
| 184 | 203 | self.#tryTrimBuffer(); | |
| 185 | 204 | } | |
| 205 | + self.#notifyEndDrained(); | ||
| 186 | 206 | } | |
| 187 | 207 | ||
| 188 | 208 | return { | |
@@ -358,10 +378,11 @@ class BroadcastImpl { | |||
| 358 | 378 | } | |
| 359 | 379 | } | |
| 360 | 380 | } | |
| 381 | + this.#notifyEndDrained(); | ||
| 361 | 382 | } | |
| 362 | 383 | ||
| 363 | 384 | [kAbort](reason) { | |
| 364 | - if (this.#ended || this.#error !== undefined) return; | ||
| 385 | + if (this.#error !== undefined) return; | ||
| 365 | 386 | this.#error = reason; | |
| 366 | 387 | this.#ended = true; | |
| 367 | 388 | ||
@@ -396,6 +417,12 @@ class BroadcastImpl { | |||
| 396 | 417 | ||
| 397 | 418 | // Private methods | |
| 398 | 419 | ||
| 420 | + #notifyEndDrained() { | ||
| 421 | + if (this.#ended && this.#consumers.size === 0) { | ||
| 422 | + this[kOnEndDrained]?.(); | ||
| 423 | + } | ||
| 424 | + } | ||
| 425 | + | ||
| 399 | 426 | #recomputeMinCursor() { | |
| 400 | 427 | const { minCursor, minCursorConsumers } = getMinCursor( | |
| 401 | 428 | this.#consumers, this.#bufferStart + this.#buffer.length); | |
@@ -516,8 +543,9 @@ let getBroadcastPendingWrites; | |||
| 516 | 543 | class BroadcastWriter { | |
| 517 | 544 | #broadcast; | |
| 518 | 545 | #totalBytes = 0; | |
| 519 | - #closed; | ||
| 520 | - #aborted = false; | ||
| 546 | + #state = 'open'; | ||
| 547 | + #error; | ||
| 548 | + #pendingEnd; | ||
| 521 | 549 | #pendingWrites = new RingBuffer(); | |
| 522 | 550 | #pendingDrains = []; | |
| 523 | 551 | ||
@@ -532,8 +560,11 @@ class BroadcastWriter { | |||
| 532 | 560 | ||
| 533 | 561 | this.#broadcast[kOnBufferDrained] = () => { | |
| 534 | 562 | this.#resolvePendingWrites(); | |
| 535 | - this.#resolvePendingDrains(true); | ||
| 563 | + if (this.#state === 'open') { | ||
| 564 | + this.#resolvePendingDrains(true); | ||
| 565 | + } | ||
| 536 | 566 | }; | |
| 567 | + this.#broadcast[kOnEndDrained] = () => this.#endDrained(); | ||
| 537 | 568 | } | |
| 538 | 569 | ||
| 539 | 570 | // The drainable protocol works with Stream.ondrain to provide a notification | |
@@ -547,20 +578,12 @@ class BroadcastWriter { | |||
| 547 | 578 | return promise; | |
| 548 | 579 | } | |
| 549 | 580 | ||
| 550 | - #isClosed() { | ||
| 551 | - return this.#closed !== undefined; | ||
| 552 | - } | ||
| 553 | - | ||
| 554 | - #isClosedOrAborted() { | ||
| 555 | - return this.#isClosed() || this.#aborted; | ||
| 556 | - } | ||
| 557 | - | ||
| 558 | 581 | get canWrite() { | |
| 559 | - return this.#isClosedOrAborted() ? null : this.#broadcast[kCanWrite](); | ||
| 582 | + return this.#state === 'open' ? this.#broadcast[kCanWrite]() : null; | ||
| 560 | 583 | } | |
| 561 | 584 | ||
| 562 | 585 | #canUseWriteFastPath(signal) { | |
| 563 | - return !signal && !this.#isClosed() && !this.#aborted && | ||
| 586 | + return !signal && this.#state === 'open' && | ||
| 564 | 587 | this.#broadcast[kCanWrite](); | |
| 565 | 588 | } | |
| 566 | 589 | ||
@@ -592,13 +615,15 @@ class BroadcastWriter { | |||
| 592 | 615 | } | |
| 593 | 616 | ||
| 594 | 617 | async #writevSlow(chunks, signal) { | |
| 595 | - // Check for pre-aborted | ||
| 596 | - signal?.throwIfAborted(); | ||
| 597 | - | ||
| 598 | - if (this.#isClosedOrAborted()) { | ||
| 618 | + if (this.#state === 'errored') { | ||
| 619 | + throw this.#error; | ||
| 620 | + } | ||
| 621 | + if (this.#state !== 'open') { | ||
| 599 | 622 | throw new ERR_INVALID_STATE.TypeError('Writer is closed'); | |
| 600 | 623 | } | |
| 601 | 624 | ||
| 625 | + signal?.throwIfAborted(); | ||
| 626 | + | ||
| 602 | 627 | const converted = convertChunks(chunks); | |
| 603 | 628 | ||
| 604 | 629 | if (this.#broadcast[kWrite](converted)) { | |
@@ -624,7 +649,7 @@ class BroadcastWriter { | |||
| 624 | 649 | } | |
| 625 | 650 | ||
| 626 | 651 | writeSync(chunk) { | |
| 627 | - if (this.#isClosedOrAborted()) return false; | ||
| 652 | + if (this.#state !== 'open') return false; | ||
| 628 | 653 | if (!this.#broadcast[kCanWrite]()) return false; | |
| 629 | 654 | const converted = | |
| 630 | 655 | toUint8Array(chunk); | |
@@ -637,7 +662,7 @@ class BroadcastWriter { | |||
| 637 | 662 | ||
| 638 | 663 | writevSync(chunks) { | |
| 639 | 664 | validateArray(chunks, 'chunks'); | |
| 640 | - if (this.#isClosedOrAborted()) return false; | ||
| 665 | + if (this.#state !== 'open') return false; | ||
| 641 | 666 | if (!this.#broadcast[kCanWrite]()) return false; | |
| 642 | 667 | const converted = convertChunks(chunks); | |
| 643 | 668 | if (this.#broadcast[kWrite](converted)) { | |
@@ -651,34 +676,43 @@ class BroadcastWriter { | |||
| 651 | 676 | ||
| 652 | 677 | end(options) { | |
| 653 | 678 | const signal = getWriterSignal(options); | |
| 679 | + if (this.#state === 'errored') return PromiseReject(this.#error); | ||
| 680 | + if (this.#state === 'closed') return PromiseResolve(this.#totalBytes); | ||
| 654 | 681 | if (signal?.aborted) return PromiseReject(signal.reason); | |
| 655 | 682 | ||
| 656 | - if (this.#isClosed()) return this.#closed; | ||
| 657 | - this.#closed = PromiseResolve(this.#totalBytes); | ||
| 658 | - this.#broadcast[kEnd](); | ||
| 659 | - this.#resolvePendingDrains(false); | ||
| 660 | - return this.#closed; | ||
| 683 | + const endPromise = this.#getEndPromise(); | ||
| 684 | + if (this.#state === 'open') { | ||
| 685 | + this.#state = 'closing'; | ||
| 686 | + this.#resolvePendingDrains(false); | ||
| 687 | + this.#finishEndIfReady(); | ||
| 688 | + } | ||
| 689 | + | ||
| 690 | + return raceEndWithSignal(endPromise, signal); | ||
| 661 | 691 | } | |
| 662 | 692 | ||
| 663 | 693 | endSync() { | |
| 664 | - if (this.#closed) return this.#totalBytes; | ||
| 665 | - this.#closed = PromiseResolve(this.#totalBytes); | ||
| 666 | - this.#broadcast[kEnd](); | ||
| 694 | + if (this.#state === 'closed') return this.#totalBytes; | ||
| 695 | + if (this.#state === 'errored' || this.#state === 'closing') return -1; | ||
| 696 | + | ||
| 697 | + this.#state = 'closing'; | ||
| 667 | 698 | this.#resolvePendingDrains(false); | |
| 668 | - return this.#totalBytes; | ||
| 699 | + this.#finishEndIfReady(); | ||
| 700 | + return this.#state === 'closed' ? this.#totalBytes : -1; | ||
| 669 | 701 | } | |
| 670 | 702 | ||
| 671 | 703 | fail(reason) { | |
| 672 | - if (this.#isClosedOrAborted()) return; | ||
| 673 | - this.#aborted = true; | ||
| 674 | - this.#closed = PromiseResolve(this.#totalBytes); | ||
| 704 | + if (this.#state === 'errored' || this.#state === 'closed') return; | ||
| 705 | + this.#state = 'errored'; | ||
| 675 | 706 | const error = reason ?? new ERR_INVALID_STATE.TypeError('Failed'); | |
| 707 | + this.#error = error; | ||
| 676 | 708 | this.#rejectPendingWrites(error); | |
| 677 | 709 | this.#rejectPendingDrains(error); | |
| 710 | + this.#pendingEnd?.reject(error); | ||
| 678 | 711 | this.#broadcast[kAbort](error); | |
| 679 | 712 | } | |
| 680 | 713 | ||
| 681 | 714 | [SymbolAsyncDispose]() { | |
| 715 | + if (this.#state === 'closing') return this.#getEndPromise(); | ||
| 682 | 716 | this.fail(); | |
| 683 | 717 | return PromiseResolve(); | |
| 684 | 718 | } | |
@@ -688,11 +722,33 @@ class BroadcastWriter { | |||
| 688 | 722 | } | |
| 689 | 723 | ||
| 690 | 724 | [kCancelWriter]() { | |
| 691 | - if (this.#isClosed()) return; | ||
| 692 | - this.#closed = PromiseResolve(this.#totalBytes); | ||
| 725 | + if (this.#state === 'closed' || this.#state === 'errored') return; | ||
| 726 | + this.#state = 'closed'; | ||
| 693 | 727 | this.#rejectPendingWrites( | |
| 694 | 728 | lazyDOMException('Broadcast cancelled', 'AbortError')); | |
| 695 | 729 | this.#resolvePendingDrains(false); | |
| 730 | + this.#pendingEnd?.resolve(this.#totalBytes); | ||
| 731 | + } | ||
| 732 | + | ||
| 733 | + #getEndPromise() { | ||
| 734 | + this.#pendingEnd ??= PromiseWithResolvers(); | ||
| 735 | + return this.#pendingEnd.promise; | ||
| 736 | + } | ||
| 737 | + | ||
| 738 | + #finishEndIfReady() { | ||
| 739 | + if (this.#state === 'closing' && this.#pendingWrites.length === 0) { | ||
| 740 | + this.#broadcast[kEnd](); | ||
| 741 | + } | ||
| 742 | + } | ||
| 743 | + | ||
| 744 | + #endDrained() { | ||
| 745 | + if (this.#state !== 'closing') return; | ||
| 746 | + this.#state = 'closed'; | ||
| 747 | + this.#pendingEnd?.resolve(this.#totalBytes); | ||
| 748 | + } | ||
| 749 | + | ||
| 750 | + [kPendingWriteRemoved]() { | ||
| 751 | + this.#finishEndIfReady(); | ||
| 696 | 752 | } | |
| 697 | 753 | ||
| 698 | 754 | /** | |
@@ -724,6 +780,7 @@ class BroadcastWriter { | |||
| 724 | 780 | break; | |
| 725 | 781 | } | |
| 726 | 782 | } | |
| 783 | + this.#finishEndIfReady(); | ||
| 727 | 784 | } | |
| 728 | 785 | ||
| 729 | 786 | #rejectPendingWrites(error) { | |
@@ -756,6 +813,7 @@ function wireBroadcastWriteSignal(entry, signal, resolve, reject, self) { | |||
| 756 | 813 | if (idx !== -1) pendingWrites.removeAt(idx); | |
| 757 | 814 | entry.chunk = null; | |
| 758 | 815 | reject(signal.reason ?? lazyDOMException('Aborted', 'AbortError')); | |
| 816 | + if (idx !== -1) self[kPendingWriteRemoved](); | ||
| 759 | 817 | }; | |
| 760 | 818 | entry.resolve = function() { | |
| 761 | 819 | signal.removeEventListener('abort', onAbort); | |
| Back | FazBrowse Home | New Git URL |
0 commit comments