ripple 0.3.91 → 0.3.93
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.
- package/CHANGELOG.md +32 -0
- package/package.json +1 -1
- package/src/constants.js +13 -0
- package/src/runtime/index-client.js +5 -1
- package/src/runtime/internal/client/hydration.js +4 -1
- package/src/runtime/internal/client/template.js +2 -1
- package/src/runtime/internal/client/try.js +199 -9
- package/src/runtime/internal/client/types.d.ts +25 -0
- package/src/runtime/internal/server/blocks.js +47 -9
- package/src/runtime/internal/server/css-registry.js +1 -1
- package/src/runtime/internal/server/index.js +546 -69
- package/src/runtime/internal/server/stream-runtime.js +173 -0
- package/src/server/index.js +2 -2
- package/src/utils/track-async-serialization.js +1 -1
- package/tests/hydration/compiled/client/streaming.js +448 -0
- package/tests/hydration/compiled/server/streaming.js +476 -0
- package/tests/hydration/components/streaming.tsrx +164 -0
- package/tests/hydration/streaming.test.js +342 -0
- package/tests/hydration/track-async-serialization.test.js +3 -3
- package/tests/hydration/try.test.js +4 -4
- package/tests/server/streaming-ssr.test.tsrx +714 -4
- package/tests/server/track-async-serialization.test.tsrx +9 -9
- package/types/server.d.ts +22 -1
|
@@ -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 {
|
|
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_text } 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
|
-
|
|
512
|
-
|
|
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
|
-
|
|
584
|
+
head.unshift(str);
|
|
539
585
|
} else {
|
|
540
|
-
|
|
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
|
-
|
|
593
|
+
body.unshift(str);
|
|
547
594
|
} else {
|
|
548
|
-
|
|
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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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_text(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
|
-
|
|
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_text(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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1905
|
-
//
|
|
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;
|