ripple 0.3.90 → 0.3.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,6 +17,31 @@
17
17
  /** @typedef {TrackedValue} Tracked */
18
18
  /** @typedef {DerivedValue} Derived */
19
19
 
20
+ /**
21
+ * A streaming flush unit: a try boundary whose body suspended during
22
+ * rendering. Its slot (wrapper markers + pending fallback) ships with the
23
+ * enclosing region while the real body — kept as a detached buffer tree whose
24
+ * holes async re-runs fill in place — streams later as a framed chunk once
25
+ * every async operation registered on the boundary settles.
26
+ * @typedef {{
27
+ * id: number;
28
+ * block: Block;
29
+ * output: Output;
30
+ * content: NestedArray<string>;
31
+ * head: NestedArray<string>;
32
+ * css: Set<string>;
33
+ * scripts: string[];
34
+ * settled: boolean;
35
+ * flushed: boolean;
36
+ * slot_sent: boolean;
37
+ * sealed: boolean;
38
+ * errored: boolean;
39
+ * error: string | null;
40
+ * }} FlushUnit
41
+ */
42
+
43
+ /** @typedef {{ css: Set<string>, scripts: string[], head: string }} StreamChunk */
44
+
20
45
  import {
21
46
  DERIVED,
22
47
  UNINITIALIZED,
@@ -37,7 +62,17 @@ import {
37
62
  } from '@tsrx/core/runtime/html';
38
63
  import { clsx } from 'clsx';
39
64
  import { create_ref_prop } from '@tsrx/core/runtime/ref';
40
- import { BLOCK_CLOSE, BLOCK_OPEN } from '../../../constants.js';
65
+ import {
66
+ BLOCK_CLOSE,
67
+ BLOCK_OPEN,
68
+ HYDRATION_START_PENDING,
69
+ HYDRATION_START_ERRORED,
70
+ STREAM_CHUNK_ATTR,
71
+ STREAM_HEAD_ATTR,
72
+ STREAM_ERROR_SCRIPT_PREFIX,
73
+ } from '../../../constants.js';
74
+ import { get_css_for_hashes } from './css-registry.js';
75
+ import { STREAM_RUNTIME_SCRIPT } from './stream-runtime.js';
41
76
  import { is_tsrx_element, normalize_children, tsrx_element } from '../../element.js';
42
77
  import {
43
78
  is_tag_valid_with_parent,
@@ -54,9 +89,10 @@ import {
54
89
  cancel_async_operations,
55
90
  component_block,
56
91
  get_closest_catch_block,
92
+ get_closest_try_block,
57
93
  try_block,
58
94
  } from './blocks.js';
59
- import { COMPONENT_BLOCK, TRY_BLOCK } from './constants.js';
95
+ import { COMPONENT_BLOCK, ROOT_BLOCK, TRY_BLOCK } from './constants.js';
60
96
 
61
97
  export { escape };
62
98
  export { register_component_css as register_css } from './css-registry.js';
@@ -451,6 +487,8 @@ export class Output {
451
487
  #parent = null;
452
488
  /** @type {StreamSink | null} */
453
489
  #streamOutput = null;
490
+ /** @type {{ before: string, between: string, after: string } | null} */
491
+ #stream_template = null;
454
492
  #stream_started = false;
455
493
  #stream_finished = false;
456
494
  /** @type {null | number} */
@@ -467,6 +505,21 @@ export class Output {
467
505
  #async_operations = new Set();
468
506
  /** @type {null | 'head'} */
469
507
  target = null;
508
+ /** @type {null | FlushUnit} */
509
+ unit = null;
510
+ /** @type {NestedArray<string> | null} */
511
+ #content_redirect = null;
512
+ /** @type {NestedArray<string> | null} */
513
+ #head_redirect = null;
514
+ // streaming state, only used on the root instance
515
+ #next_unit_id = 1;
516
+ /** @type {FlushUnit[]} */
517
+ #units = [];
518
+ /** @type {WeakMap<NestedArray<string>, FlushUnit>} */
519
+ #unit_slots = new WeakMap();
520
+ /** @type {Set<string>} */
521
+ #sent_css = new Set();
522
+ #shell_flushed = false;
470
523
 
471
524
  get root() {
472
525
  return this.#root;
@@ -508,8 +561,10 @@ export class Output {
508
561
  } else {
509
562
  this.#root = parent.root;
510
563
  this.#parent = parent;
511
- this.#parent.body.push(this.body);
512
- this.#parent.head.push(this.head);
564
+ // branches created after a flush unit sealed its slot belong to the
565
+ // unit's detached content tree, not to the already-shaped slot
566
+ (parent.#content_redirect ?? parent.#body).push(this.#body);
567
+ (parent.#head_redirect ?? parent.#head).push(this.#head);
513
568
  }
514
569
  }
515
570
 
@@ -520,32 +575,24 @@ export class Output {
520
575
  * @returns {void}
521
576
  */
522
577
  #push(str, is_root = false, is_prepend = false) {
523
- if (this.isStreamMode() && !this.isSyncRun()) {
524
- // TODO - we need to wrap the resulting block output into something that
525
- // the client-side can understand and append them appropriately,
526
- // or actually, first append and hydrate when the full block is finished
527
- // without waiting for the all blocks to finish streaming to make hydration faster
528
- /** @type {StreamSink} */
529
- (this.#root.#streamOutput).push(str);
530
- return;
531
- }
532
-
533
578
  var instance = is_root ? this.#root : this;
534
579
 
535
580
  // we never write to `head` in the root instance
536
581
  if (instance !== this.#root && instance.target === 'head') {
582
+ var head = instance.#head_redirect ?? instance.#head;
537
583
  if (is_prepend) {
538
- instance.#head.unshift(str);
584
+ head.unshift(str);
539
585
  } else {
540
- instance.#head.push(str);
586
+ head.push(str);
541
587
  }
542
588
  return;
543
589
  }
544
590
 
591
+ var body = instance.#content_redirect ?? instance.#body;
545
592
  if (is_prepend) {
546
- instance.#body.unshift(str);
593
+ body.unshift(str);
547
594
  } else {
548
- instance.#body.push(str);
595
+ body.push(str);
549
596
  }
550
597
  }
551
598
 
@@ -562,6 +609,14 @@ export class Output {
562
609
  * @returns {void}
563
610
  */
564
611
  push_serialized_error(str) {
612
+ var root = this.#root;
613
+ if (root.#streamOutput !== null && root.#shell_flushed) {
614
+ // past the shell the root buffer is already on the wire — emit the
615
+ // envelope directly so it reaches the document before any chunk
616
+ // whose hydration depends on it
617
+ root.#streamOutput.push(str);
618
+ return;
619
+ }
565
620
  // prepend to the root block to avoid messing up the hydration markers
566
621
  // writing to the root to avoid being cleared in the local instance when an error occurs
567
622
  this.#push(str, true, true);
@@ -572,10 +627,31 @@ export class Output {
572
627
  * @returns {void}
573
628
  */
574
629
  push_serialized_result(str) {
630
+ var root = this.#root;
631
+ if (root.#streamOutput !== null && !root.#sync_run) {
632
+ var unit = this.#nearest_unflushed_unit();
633
+ if (unit !== null) {
634
+ // ships with the unit's chunk, ahead of the template, so the
635
+ // boundary can hydrate from it synchronously
636
+ unit.scripts.push(str);
637
+ return;
638
+ }
639
+ if (root.#shell_flushed) {
640
+ root.#streamOutput.push(str);
641
+ return;
642
+ }
643
+ }
575
644
  this.#push(str);
576
645
  }
577
646
 
578
647
  clear() {
648
+ if (this.#content_redirect !== null) {
649
+ // a sealed flush unit clears its detached content, never the slot
650
+ this.#content_redirect.length = 0;
651
+ /** @type {NestedArray<string>} */ (this.#head_redirect).length = 0;
652
+ /** @type {FlushUnit} */ (this.unit).scripts.length = 0;
653
+ return;
654
+ }
579
655
  this.#head.length = 0;
580
656
  this.#body.length = 0;
581
657
  this.#css.clear();
@@ -586,13 +662,67 @@ export class Output {
586
662
  * @returns {void}
587
663
  */
588
664
  register_css(hash) {
589
- if (this.isStreamMode() && !this.isSyncRun()) {
590
- // TODO - when we're in the streaming mode and finished the sync render,
591
- // We should wrap the css into something that the client-side can understand
592
- // and append them into the head immediately
593
- return;
665
+ var root = this.#root;
666
+ if (root.#streamOutput !== null && !root.#sync_run) {
667
+ var unit = this.#nearest_unflushed_unit();
668
+ if (unit !== null) {
669
+ unit.css.add(hash);
670
+ return;
671
+ }
672
+ if (root.#shell_flushed) {
673
+ if (!root.#sent_css.has(hash)) {
674
+ root.#sent_css.add(hash);
675
+ var css_text = get_css_for_hashes(new Set([hash]));
676
+ if (css_text) {
677
+ root.#streamOutput.push('<style data-ripple-ssr>' + css_text + '</style>');
678
+ }
679
+ }
680
+ return;
681
+ }
594
682
  }
595
- this.#root.#css.add(hash);
683
+ root.#css.add(hash);
684
+ }
685
+
686
+ /**
687
+ * @returns {FlushUnit | null}
688
+ */
689
+ #nearest_unflushed_unit() {
690
+ /** @type {Output | null} */
691
+ var output = this;
692
+ while (output !== null) {
693
+ var unit = output.unit;
694
+ // an unsealed unit is still rendering its slot (wrapper + fallback),
695
+ // which ships with the enclosing region — keep walking up
696
+ if (unit !== null && !unit.flushed && unit.sealed) {
697
+ return unit;
698
+ }
699
+ output = output.#parent;
700
+ }
701
+ return null;
702
+ }
703
+
704
+ /**
705
+ * @returns {FlushUnit | null}
706
+ */
707
+ nearestUnflushedUnit() {
708
+ return this.#nearest_unflushed_unit();
709
+ }
710
+
711
+ /**
712
+ * Whether the region this output writes into has already been streamed —
713
+ * its bytes are on the wire and can no longer be replaced server-side.
714
+ * @returns {boolean}
715
+ */
716
+ isRegionSent() {
717
+ /** @type {Output | null} */
718
+ var output = this;
719
+ while (output !== null) {
720
+ if (output.unit !== null) {
721
+ return output.unit.flushed;
722
+ }
723
+ output = output.#parent;
724
+ }
725
+ return this.#root.#shell_flushed;
596
726
  }
597
727
 
598
728
  /**
@@ -611,6 +741,11 @@ export class Output {
611
741
  resolveAsync(operation) {
612
742
  this.#async_operations.delete(operation);
613
743
  this.#root._decrementPending();
744
+ var unit = this.unit;
745
+ if (unit !== null && !unit.flushed && !unit.settled && this.#async_operations.size === 0) {
746
+ unit.settled = true;
747
+ this.#root._maybeFlushUnit(unit);
748
+ }
614
749
  }
615
750
 
616
751
  cancelAsyncOperations() {
@@ -622,6 +757,266 @@ export class Output {
622
757
  }
623
758
  }
624
759
 
760
+ /**
761
+ * @returns {boolean}
762
+ */
763
+ hasPendingAsyncOperations() {
764
+ return this.#async_operations.size > 0;
765
+ }
766
+
767
+ /**
768
+ * Marks the flush unit of a boundary abandoned by an ancestor error as
769
+ * done so it can never stream a stale chunk.
770
+ * @returns {void}
771
+ */
772
+ abandonUnit() {
773
+ var unit = this.unit;
774
+ if (unit !== null && !unit.flushed) {
775
+ unit.settled = true;
776
+ unit.flushed = true;
777
+ }
778
+ }
779
+
780
+ /**
781
+ * Converts this try-block output into a streaming flush unit: the
782
+ * partially-rendered body (whose holes async re-runs fill in place) is
783
+ * detached as the unit's content tree, and the now-empty live buffer
784
+ * becomes the slot that receives the wrapper markers and fallback.
785
+ * @param {Block} block
786
+ * @returns {FlushUnit}
787
+ */
788
+ _createFlushUnit(block) {
789
+ var root = this.#root;
790
+ /** @type {FlushUnit} */
791
+ var unit = {
792
+ id: root.#next_unit_id++,
793
+ block,
794
+ output: this,
795
+ content: this.#body.splice(0, this.#body.length),
796
+ head: this.#head.splice(0, this.#head.length),
797
+ css: new Set(),
798
+ scripts: [],
799
+ settled: false,
800
+ flushed: false,
801
+ slot_sent: false,
802
+ sealed: false,
803
+ errored: false,
804
+ error: null,
805
+ };
806
+ this.unit = unit;
807
+ root.#unit_slots.set(this.#body, unit);
808
+ root.#units.push(unit);
809
+ return unit;
810
+ }
811
+
812
+ /**
813
+ * Called once the slot (wrapper markers + fallback) is fully rendered:
814
+ * from here on, direct writes to this output (e.g. a late catch render)
815
+ * belong to the unit's content, not to the already-shaped slot.
816
+ * @returns {void}
817
+ */
818
+ _sealSlot() {
819
+ var unit = /** @type {FlushUnit} */ (this.unit);
820
+ unit.sealed = true;
821
+ this.#content_redirect = unit.content;
822
+ this.#head_redirect = unit.head;
823
+ }
824
+
825
+ /**
826
+ * Flattens a buffer tree to HTML. A nested array that is the slot of a
827
+ * flush unit gets special treatment: a settled, unflushed, non-errored
828
+ * unit is inlined (coalesced into the current chunk, merging its css,
829
+ * scripts and head); anything else emits the slot as-is (wrapper +
830
+ * fallback) and is marked sent so the unit may flush itself later.
831
+ * @param {NestedArray<string>} tree
832
+ * @param {StreamChunk} chunk
833
+ * @returns {string}
834
+ */
835
+ #serialize(tree, chunk) {
836
+ var out = '';
837
+ for (var i = 0; i < tree.length; i++) {
838
+ var item = tree[i];
839
+ if (typeof item === 'string') {
840
+ out += item;
841
+ continue;
842
+ }
843
+ var unit = this.#unit_slots.get(item);
844
+ if (unit !== undefined && !unit.flushed) {
845
+ if (unit.settled && !unit.errored) {
846
+ out += this.#collect_unit(unit, chunk);
847
+ continue;
848
+ }
849
+ unit.slot_sent = true;
850
+ }
851
+ out += this.#serialize(item, chunk);
852
+ }
853
+ return out;
854
+ }
855
+
856
+ /**
857
+ * @param {FlushUnit} unit
858
+ * @param {StreamChunk} chunk
859
+ * @returns {string} the unit's content HTML
860
+ */
861
+ #collect_unit(unit, chunk) {
862
+ unit.flushed = true;
863
+ for (var hash of unit.css) {
864
+ chunk.css.add(hash);
865
+ }
866
+ for (var i = 0; i < unit.scripts.length; i++) {
867
+ chunk.scripts.push(unit.scripts[i]);
868
+ }
869
+ chunk.head += this.#serialize(unit.head, chunk);
870
+ return this.#serialize(unit.content, chunk);
871
+ }
872
+
873
+ /**
874
+ * @param {Set<string>} hashes
875
+ * @returns {string}
876
+ */
877
+ #chunk_css(hashes) {
878
+ /** @type {Set<string>} */
879
+ var fresh = new Set();
880
+ for (var hash of hashes) {
881
+ if (!this.#sent_css.has(hash)) {
882
+ this.#sent_css.add(hash);
883
+ fresh.add(hash);
884
+ }
885
+ }
886
+ if (fresh.size === 0) {
887
+ return '';
888
+ }
889
+ var css_text = get_css_for_hashes(fresh);
890
+ return css_text ? '<style data-ripple-ssr>' + css_text + '</style>' : '';
891
+ }
892
+
893
+ /**
894
+ * @param {FlushUnit} unit
895
+ * @returns {void}
896
+ */
897
+ _maybeFlushUnit(unit) {
898
+ if (!this.#is_root) {
899
+ throw new Error('_maybeFlushUnit() is an internal method.');
900
+ }
901
+ if (this.#streamOutput === null || this.#stream_finished || !this.#shell_flushed) {
902
+ return;
903
+ }
904
+ if (!unit.settled || unit.flushed || !unit.slot_sent) {
905
+ return;
906
+ }
907
+ this.#flush_unit(unit);
908
+ this.#sweep_units();
909
+ }
910
+
911
+ /**
912
+ * @param {FlushUnit} unit
913
+ * @returns {void}
914
+ */
915
+ #flush_unit(unit) {
916
+ var sink = /** @type {StreamSink} */ (this.#streamOutput);
917
+ if (unit.errored) {
918
+ unit.flushed = true;
919
+ sink.push(
920
+ '<script id="' +
921
+ STREAM_ERROR_SCRIPT_PREFIX +
922
+ unit.id +
923
+ '" type="application/json">' +
924
+ escape_script(JSON.stringify({ message: unit.error })) +
925
+ '</script>' +
926
+ '<script>__RIPPLE_S__(' +
927
+ unit.id +
928
+ ',1)</script>',
929
+ );
930
+ return;
931
+ }
932
+ /** @type {StreamChunk} */
933
+ var chunk = { css: new Set(), scripts: [], head: '' };
934
+ var html = this.#collect_unit(unit, chunk);
935
+ var out = this.#chunk_css(chunk.css);
936
+ if (chunk.head !== '') {
937
+ out += '<template ' + STREAM_HEAD_ATTR + '="' + unit.id + '">' + chunk.head + '</template>';
938
+ }
939
+ out += chunk.scripts.join('');
940
+ out += '<template ' + STREAM_CHUNK_ATTR + '="' + unit.id + '">' + html + '</template>';
941
+ out += '<script>__RIPPLE_S__(' + unit.id + ')</script>';
942
+ sink.push(out);
943
+ }
944
+
945
+ /**
946
+ * Flushes every unit whose slot has been sent and whose async work has
947
+ * settled. Flushing a parent chunk marks nested unsettled slots as sent,
948
+ * which can make further units eligible — loop until a fixpoint.
949
+ * @returns {void}
950
+ */
951
+ #sweep_units() {
952
+ var units = this.#units;
953
+ var progressed = true;
954
+ while (progressed) {
955
+ progressed = false;
956
+ for (var i = 0; i < units.length; i++) {
957
+ var unit = units[i];
958
+ if (unit.settled && !unit.flushed && unit.slot_sent) {
959
+ this.#flush_unit(unit);
960
+ progressed = true;
961
+ }
962
+ }
963
+ }
964
+ }
965
+
966
+ /**
967
+ * Emits the shell: everything rendered synchronously (suspended
968
+ * boundaries show their fallback inside `<!--[?N-->…<!--]-->` slots), all
969
+ * CSS registered so far, and — when unresolved slots remain — the inline
970
+ * swap runtime that later chunks call into.
971
+ * @returns {void}
972
+ */
973
+ _streamShell() {
974
+ if (!this.#is_root) {
975
+ throw new Error('_streamShell() is an internal method.');
976
+ }
977
+ var sink = /** @type {StreamSink} */ (this.#streamOutput);
978
+ this._startStream();
979
+ /** @type {StreamChunk} */
980
+ var chunk = { css: new Set(), scripts: [], head: '' };
981
+ var head_html = this.#serialize(this.#head, chunk);
982
+ var body = this.#serialize(this.#body, chunk);
983
+ if (this.unit !== null && !this.unit.flushed) {
984
+ // the root boundary's slot is the shell body itself
985
+ this.unit.slot_sent = true;
986
+ }
987
+ for (var hash of this.#css) {
988
+ chunk.css.add(hash);
989
+ }
990
+ var template = this.#stream_template;
991
+ var out = template !== null ? template.before : '';
992
+ out += head_html + chunk.head + this.#chunk_css(chunk.css) + chunk.scripts.join('');
993
+ if (template !== null) {
994
+ out += template.between;
995
+ }
996
+ if (this.unit !== null) {
997
+ // the root boundary suspended: its slot IS the body (the body
998
+ // markers travel with the unit's content instead), so the slot
999
+ // marker doubles as the hydration anchor and is unambiguously
1000
+ // root-owned
1001
+ out += body;
1002
+ } else {
1003
+ out += BLOCK_OPEN + body + BLOCK_CLOSE;
1004
+ }
1005
+ var has_open_units = false;
1006
+ for (var i = 0; i < this.#units.length; i++) {
1007
+ if (!this.#units[i].flushed) {
1008
+ has_open_units = true;
1009
+ break;
1010
+ }
1011
+ }
1012
+ if (has_open_units) {
1013
+ out += STREAM_RUNTIME_SCRIPT;
1014
+ }
1015
+ sink.push(out);
1016
+ this.#shell_flushed = true;
1017
+ this.#sweep_units();
1018
+ }
1019
+
625
1020
  _incrementPending() {
626
1021
  if (this.#is_root) {
627
1022
  /** @type {number} */ (this.#pending_count)++;
@@ -653,10 +1048,12 @@ export class Output {
653
1048
 
654
1049
  /**
655
1050
  * @param {StreamSink} stream
1051
+ * @param {{ before: string, between: string, after: string } | null} [template]
656
1052
  */
657
- _setStream(stream) {
1053
+ _setStream(stream, template = null) {
658
1054
  if (this.#is_root) {
659
1055
  this.#streamOutput = stream;
1056
+ this.#stream_template = template;
660
1057
  return;
661
1058
  }
662
1059
 
@@ -676,6 +1073,9 @@ export class Output {
676
1073
  if (this.#is_root) {
677
1074
  if (this.#streamOutput && this.#stream_started && !this.#stream_finished) {
678
1075
  this.#stream_finished = true;
1076
+ if (this.#stream_template !== null && this.#stream_template.after !== '') {
1077
+ this.#streamOutput.push(this.#stream_template.after);
1078
+ }
679
1079
  this.#streamOutput.close();
680
1080
  }
681
1081
  return;
@@ -743,21 +1143,11 @@ export async function render(component, passed_in_options = {}) {
743
1143
  root_block = /** @type {Block} */ (active_block);
744
1144
  const output = root_block.o;
745
1145
  if (options.stream) {
746
- output._setStream(options.stream);
1146
+ output._setStream(options.stream, options.streamTemplate ?? null);
747
1147
  }
748
1148
  render_component(component, {});
749
1149
  output._decrementPending();
750
1150
  output._finishSyncRun();
751
-
752
- if (output.isStreamMode()) {
753
- sync_buffers_to_string(output);
754
- output._startStream();
755
- output.push(head);
756
- output.push(body);
757
- // TODO - how do we handle css?, in needs to be inside the head
758
- // We probably can allocate a buffer inside the head for this
759
- // We should have the same order of insertion as for the full async render
760
- }
761
1151
  },
762
1152
  (error) => {
763
1153
  // We're not going to send the error in the stream stream.error()
@@ -783,10 +1173,15 @@ export async function render(component, passed_in_options = {}) {
783
1173
  },
784
1174
  );
785
1175
 
786
- await /** @type {Block} */ (/** @type {unknown} */ (root_block)).o.promise;
1176
+ const output = /** @type {Block} */ (/** @type {unknown} */ (root_block)).o;
1177
+ if (output.isStreamMode()) {
1178
+ // shell: sync content + fallbacks in open slots + css + swap runtime
1179
+ output._streamShell();
1180
+ }
1181
+
1182
+ await output.promise;
787
1183
  reset_state();
788
1184
 
789
- const output = /** @type {Block} */ (/** @type {unknown} */ (root_block)).o;
790
1185
  if (output.isStreamMode() && options.closeStream) {
791
1186
  output._closeStream();
792
1187
  }
@@ -1569,6 +1964,102 @@ function route_track_async_error_to_catch_block_with_boundary(catch_block, hash,
1569
1964
  serialize_track_async_error(hash, public_error);
1570
1965
  }
1571
1966
 
1967
+ /**
1968
+ * Turns a try boundary whose body suspended into a streaming flush unit:
1969
+ * the partially-rendered body is detached (async re-runs keep filling its
1970
+ * holes in place) and the live slot receives the `<!--[?N-->` wrapper with
1971
+ * the pending fallback (nothing for catch-only boundaries).
1972
+ * @param {Block} block
1973
+ * @param {(() => void) | null} pending_fn
1974
+ * @returns {void}
1975
+ */
1976
+ export function begin_stream_unit(block, pending_fn) {
1977
+ var output = block.o;
1978
+ var unit = output._createFlushUnit(block);
1979
+ if (block.f & ROOT_BLOCK) {
1980
+ // a user try body carries its own compiled <!--[-->…<!--]--> markers;
1981
+ // the root boundary's content does not — add them so every unit's
1982
+ // content hydrates (and swaps) through the same shape
1983
+ unit.content.unshift(BLOCK_OPEN);
1984
+ unit.content.push(BLOCK_CLOSE);
1985
+ }
1986
+ output.push('<!--' + HYDRATION_START_PENDING + unit.id + '-->');
1987
+ if (pending_fn !== null) {
1988
+ pending_fn();
1989
+ }
1990
+ output.push(BLOCK_CLOSE);
1991
+ output._sealSlot();
1992
+ }
1993
+
1994
+ /**
1995
+ * Handles an error surfaced by async rendering work after the initial sync
1996
+ * pass: routes it to the nearest catch boundary when that boundary's region
1997
+ * can still be rendered server-side, otherwise marks the nearest unflushed
1998
+ * flush unit as errored so the client resolves the boundary at hydration.
1999
+ * @param {Block} origin_block
2000
+ * @param {Block} op_block
2001
+ * @param {string | null} hash
2002
+ * @param {any} error
2003
+ * @returns {void}
2004
+ */
2005
+ function handle_async_render_error(origin_block, op_block, hash, error) {
2006
+ var catch_block = get_closest_catch_block(origin_block);
2007
+
2008
+ if (catch_block.o.isStreamMode() && catch_block.o.isRegionSent()) {
2009
+ fail_stream_unit(op_block, error);
2010
+ return;
2011
+ }
2012
+
2013
+ if (hash !== null) {
2014
+ route_track_async_error_to_catch_block_with_boundary(catch_block, hash, error);
2015
+ } else {
2016
+ route_error_to_catch_block(catch_block, error);
2017
+ }
2018
+ settle_unit_after_catch(catch_block);
2019
+ }
2020
+
2021
+ /**
2022
+ * The nearest catch boundary's bytes are already on the wire — emit a
2023
+ * unit-level error envelope instead, so the client routes the error into the
2024
+ * live boundary during hydration.
2025
+ * @param {Block} op_block
2026
+ * @param {any} error
2027
+ * @returns {void}
2028
+ */
2029
+ function fail_stream_unit(op_block, error) {
2030
+ var unit = op_block.o.nearestUnflushedUnit();
2031
+ if (unit === null) {
2032
+ // everything around the failure is already streamed — nothing the
2033
+ // server can do beyond reporting it
2034
+ console.error(error);
2035
+ return;
2036
+ }
2037
+ cancel_async_operations(unit.block);
2038
+ unit.errored = true;
2039
+ unit.error = get_public_track_async_error_message(error);
2040
+ unit.settled = true;
2041
+ unit.output.root._maybeFlushUnit(unit);
2042
+ }
2043
+
2044
+ /**
2045
+ * After a catch branch rendered into a boundary's buffers, the enclosing
2046
+ * flush unit may have no outstanding async work left (it was cancelled) —
2047
+ * settle and flush it.
2048
+ * @param {Block} catch_block
2049
+ * @returns {void}
2050
+ */
2051
+ function settle_unit_after_catch(catch_block) {
2052
+ var output = catch_block.o;
2053
+ if (!output.isStreamMode()) {
2054
+ return;
2055
+ }
2056
+ var unit = output.nearestUnflushedUnit();
2057
+ if (unit !== null && !unit.settled && !unit.output.hasPendingAsyncOperations()) {
2058
+ unit.settled = true;
2059
+ unit.output.root._maybeFlushUnit(unit);
2060
+ }
2061
+ }
2062
+
1572
2063
  /**
1573
2064
  * @param {(str: string) => void} push_fn
1574
2065
  * @param {string} hash
@@ -1666,11 +2157,7 @@ function run_track_async(t, fn, block, dr, dj) {
1666
2157
  if (dj) {
1667
2158
  dj(error);
1668
2159
  }
1669
- route_track_async_error_to_catch_block_with_boundary(
1670
- get_closest_catch_block(block),
1671
- t.h,
1672
- error,
1673
- );
2160
+ handle_async_render_error(block, get_closest_try_block(block), t.h, error);
1674
2161
  },
1675
2162
  );
1676
2163
  return;
@@ -1715,11 +2202,7 @@ function run_track_async(t, fn, block, dr, dj) {
1715
2202
  if (dj) {
1716
2203
  dj(error);
1717
2204
  }
1718
- route_track_async_error_to_catch_block_with_boundary(
1719
- get_closest_catch_block(block),
1720
- t.h,
1721
- error,
1722
- );
2205
+ handle_async_render_error(block, get_closest_try_block(block), t.h, error);
1723
2206
  },
1724
2207
  );
1725
2208
  }
@@ -1833,7 +2316,10 @@ function register_block_rerun(block) {
1833
2316
  }
1834
2317
 
1835
2318
  var cancelled = false;
1836
- var try_catch_block = get_closest_catch_block(block);
2319
+ // async work is accounted on the nearest try boundary (in stream mode
2320
+ // that boundary becomes the flush unit); errors still route to the
2321
+ // nearest catch boundary separately
2322
+ var op_block = get_closest_try_block(block);
1837
2323
  var operation = {
1838
2324
  cancel: () => {
1839
2325
  cancelled = true;
@@ -1845,7 +2331,7 @@ function register_block_rerun(block) {
1845
2331
  },
1846
2332
  };
1847
2333
 
1848
- try_catch_block.o.registerAsync(operation);
2334
+ op_block.o.registerAsync(operation);
1849
2335
  /** @type {PromiseLike<any>} */ (/** @type {Tracked} */ (t).ap).then(
1850
2336
  () => {
1851
2337
  if (cancelled) {
@@ -1854,17 +2340,16 @@ function register_block_rerun(block) {
1854
2340
  reset_state();
1855
2341
  try {
1856
2342
  run_block(block);
1857
- try_catch_block.o.resolveAsync(operation);
2343
+ op_block.o.resolveAsync(operation);
1858
2344
  } catch (error) {
1859
2345
  if (error instanceof TrackAsyncRunError) {
1860
2346
  var {
1861
2347
  cause,
1862
2348
  tracked: { h: hash },
1863
2349
  } = /** @type {InstanceType<typeof TrackAsyncRunError>} */ (error);
1864
- error = cause;
1865
- route_track_async_error_to_catch_block_with_boundary(try_catch_block, hash, error);
2350
+ handle_async_render_error(block, op_block, hash, cause);
1866
2351
  } else {
1867
- route_error_to_catch_block(try_catch_block, error);
2352
+ handle_async_render_error(block, op_block, null, error);
1868
2353
  }
1869
2354
  }
1870
2355
  },
@@ -1872,11 +2357,7 @@ function register_block_rerun(block) {
1872
2357
  if (cancelled) {
1873
2358
  return;
1874
2359
  }
1875
- route_track_async_error_to_catch_block_with_boundary(
1876
- try_catch_block,
1877
- /** @type {Tracked} */ (t).h,
1878
- error,
1879
- );
2360
+ handle_async_render_error(block, op_block, /** @type {Tracked} */ (t).h, error);
1880
2361
  },
1881
2362
  );
1882
2363
  // clear all output buffers as we'll rerun the block rendering
@@ -1899,18 +2380,14 @@ export function run_block(block) {
1899
2380
  active_dependency = null;
1900
2381
  block.fn(block.o);
1901
2382
  } catch (error) {
1902
- var output = block.o;
1903
2383
  if (error === ASYNC_DERIVED_READ_THROWN) {
1904
- // regardless of the render mode (stream, etc.)
1905
- // we need to rerun the block when the dependency's promise resolves
2384
+ // regardless of the render mode we re-run the block when the
2385
+ // dependency's promise resolves; the block's buffer keeps its place
2386
+ // in the output tree, so the surrounding markup renders on with a
2387
+ // hole here. In stream mode the enclosing try boundary checks after
2388
+ // its body ran whether async work registered on it and becomes a
2389
+ // flush unit if so.
1906
2390
  register_block_rerun(block);
1907
-
1908
- if (output.isStreamMode() && output.isSyncRun()) {
1909
- // rethrowing so that the pending block catches it
1910
- // we should only render fallback/pending in the streaming mode
1911
- // when in the synchronous phase
1912
- throw error;
1913
- }
1914
2391
  } else {
1915
2392
  // always re-throw real errors
1916
2393
  // during sync, try_block's catch handles it;