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.
- package/CHANGELOG.md +30 -0
- package/package.json +3 -3
- 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/index.js +546 -69
- package/src/runtime/internal/server/stream-runtime.js +173 -0
- 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 +710 -0
- package/tests/server/track-async-serialization.test.tsrx +9 -9
- package/types/server.d.ts +15 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# ripple
|
|
2
2
|
|
|
3
|
+
## 0.3.92
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#1326](https://github.com/Ripple-TS/ripple/pull/1326)
|
|
8
|
+
[`fea49bf`](https://github.com/Ripple-TS/ripple/commit/fea49bfb4410a05e0c915dfa39acdaba7f542737)
|
|
9
|
+
Thanks [@leonidaz](https://github.com/leonidaz)! - Streaming SSR:
|
|
10
|
+
`render(App, { stream })` now streams progressively. The synchronous shell (with
|
|
11
|
+
pending fallbacks for suspended `@try` boundaries and all CSS registered so far)
|
|
12
|
+
is flushed immediately; each boundary's content streams out of order as a framed
|
|
13
|
+
chunk once its async work settles, including per-chunk CSS, trackAsync envelopes
|
|
14
|
+
and `<head>` content. A tiny inline runtime swaps chunks into their slots before
|
|
15
|
+
hydration, and hydrated boundaries activate streamed chunks in place afterwards
|
|
16
|
+
— claiming the streamed DOM without re-rendering. Catch-only async boundaries
|
|
17
|
+
stream an empty slot and resolve to their body or server-rendered catch; errors
|
|
18
|
+
whose catch region is already on the wire hand off to the client boundary via an
|
|
19
|
+
error envelope. `render` also gains a `streamTemplate` option for document
|
|
20
|
+
scaffolding, and the vite plugin streams render-route responses when
|
|
21
|
+
`ssr.streaming` is enabled in ripple.config.ts (falling back to buffered SSR
|
|
22
|
+
when index.html lacks the `<!--ssr-head-->`/`<!--ssr-body-->` markers).
|
|
23
|
+
|
|
24
|
+
## 0.3.91
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
- Updated dependencies
|
|
29
|
+
[[`a109586`](https://github.com/Ripple-TS/ripple/commit/a109586774227b4026ffbd813a956e231edb1005)]:
|
|
30
|
+
- @tsrx/core@0.1.37
|
|
31
|
+
- @tsrx/ripple@0.1.38
|
|
32
|
+
|
|
3
33
|
## 0.3.90
|
|
4
34
|
|
|
5
35
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Ripple is an elegant TypeScript UI framework",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dominic Gannaway",
|
|
6
|
-
"version": "0.3.
|
|
6
|
+
"version": "0.3.92",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index-client.js",
|
|
9
9
|
"main": "src/runtime/index-client.js",
|
|
@@ -73,8 +73,8 @@
|
|
|
73
73
|
"clsx": "^2.1.1",
|
|
74
74
|
"devalue": "^5.8.1",
|
|
75
75
|
"esm-env": "^1.2.2",
|
|
76
|
-
"@tsrx/core": "0.1.
|
|
77
|
-
"@tsrx/ripple": "0.1.
|
|
76
|
+
"@tsrx/core": "0.1.37",
|
|
77
|
+
"@tsrx/ripple": "0.1.38"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
80
|
"@types/estree": "^1.0.8",
|
package/src/constants.js
CHANGED
|
@@ -13,6 +13,19 @@ export const HYDRATION_START = '[';
|
|
|
13
13
|
export const HYDRATION_END = ']';
|
|
14
14
|
export const HYDRATION_ERROR = {};
|
|
15
15
|
|
|
16
|
+
// Streaming SSR slot markers. A `<!--[?N-->` comment opens the slot of flush
|
|
17
|
+
// unit N whose content has not been streamed yet (the slot shows the pending
|
|
18
|
+
// fallback, or nothing for catch-only boundaries). The inline stream runtime
|
|
19
|
+
// rewrites the slot to plain `<!--[-->…<!--]-->` when the unit's chunk
|
|
20
|
+
// arrives, or to `<!--[!N-->` when the unit errored after its region was
|
|
21
|
+
// already on the wire.
|
|
22
|
+
export const HYDRATION_START_PENDING = '[?';
|
|
23
|
+
export const HYDRATION_START_ERRORED = '[!';
|
|
24
|
+
|
|
25
|
+
export const STREAM_CHUNK_ATTR = 'data-ripple-chunk';
|
|
26
|
+
export const STREAM_HEAD_ATTR = 'data-ripple-head';
|
|
27
|
+
export const STREAM_ERROR_SCRIPT_PREFIX = '__ripple_te_';
|
|
28
|
+
|
|
16
29
|
export const BLOCK_OPEN = `<!--${HYDRATION_START}-->`;
|
|
17
30
|
export const BLOCK_CLOSE = `<!--${HYDRATION_END}-->`;
|
|
18
31
|
export const EMPTY_COMMENT = `<!---->`;
|
|
@@ -133,7 +133,11 @@ export function hydrate(component, options) {
|
|
|
133
133
|
try {
|
|
134
134
|
while (
|
|
135
135
|
anchor &&
|
|
136
|
-
(anchor.nodeType !== COMMENT_NODE ||
|
|
136
|
+
(anchor.nodeType !== COMMENT_NODE ||
|
|
137
|
+
// any `[`-prefixed marker anchors the root boundary — a streamed
|
|
138
|
+
// shell whose root suspended starts with a `<!--[?N-->` slot
|
|
139
|
+
// marker instead of a plain `<!--[-->`
|
|
140
|
+
!(/** @type {Comment} */ (anchor).data.startsWith(HYDRATION_START)))
|
|
137
141
|
) {
|
|
138
142
|
anchor = get_next_sibling(anchor);
|
|
139
143
|
}
|
|
@@ -73,6 +73,9 @@ export function pop(node) {
|
|
|
73
73
|
* Scans forward from the current hydrate_node to find the matching HYDRATION_END
|
|
74
74
|
* comment, handling nested blocks by tracking depth.
|
|
75
75
|
* Should be called after hydrate_next() has consumed the opening HYDRATION_START.
|
|
76
|
+
* Any `[`-prefixed comment opens a nested region — this includes the plain
|
|
77
|
+
* `<!--[-->` markers as well as streaming slot markers (`<!--[?N-->`,
|
|
78
|
+
* `<!--[!N-->`), which always pair with a `<!--]-->`.
|
|
76
79
|
* @returns {Node} The HYDRATION_END comment node.
|
|
77
80
|
*/
|
|
78
81
|
export function skip_to_hydration_end() {
|
|
@@ -84,7 +87,7 @@ export function skip_to_hydration_end() {
|
|
|
84
87
|
if (data === HYDRATION_END) {
|
|
85
88
|
if (depth === 0) return node;
|
|
86
89
|
depth -= 1;
|
|
87
|
-
} else if (data
|
|
90
|
+
} else if (data.startsWith(HYDRATION_START)) {
|
|
88
91
|
depth += 1;
|
|
89
92
|
}
|
|
90
93
|
}
|
|
@@ -223,7 +223,8 @@ function is_after_hydration_block(start, target) {
|
|
|
223
223
|
if (current.nodeType === COMMENT_NODE) {
|
|
224
224
|
var data = /** @type {Comment} */ (current).data;
|
|
225
225
|
|
|
226
|
-
|
|
226
|
+
// `[`-prefixed covers plain block starts and streaming slot markers
|
|
227
|
+
if (data.startsWith(HYDRATION_START)) {
|
|
227
228
|
depth += 1;
|
|
228
229
|
} else if (data === HYDRATION_END) {
|
|
229
230
|
if (depth === 0) {
|
|
@@ -9,7 +9,21 @@ import {
|
|
|
9
9
|
resume_block,
|
|
10
10
|
} from './blocks.js';
|
|
11
11
|
import { TRY_BLOCK } from './constants.js';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
COMMENT_NODE,
|
|
14
|
+
HYDRATION_START,
|
|
15
|
+
HYDRATION_START_PENDING,
|
|
16
|
+
HYDRATION_START_ERRORED,
|
|
17
|
+
STREAM_ERROR_SCRIPT_PREFIX,
|
|
18
|
+
} from '../../../constants.js';
|
|
19
|
+
import {
|
|
20
|
+
hydrate_next,
|
|
21
|
+
hydrate_node,
|
|
22
|
+
hydrating,
|
|
23
|
+
set_hydrate_node,
|
|
24
|
+
set_hydrating,
|
|
25
|
+
skip_to_hydration_end,
|
|
26
|
+
} from './hydration.js';
|
|
13
27
|
import { append } from './template.js';
|
|
14
28
|
import {
|
|
15
29
|
active_block,
|
|
@@ -60,6 +74,14 @@ export function try_block(node, try_fn, catch_fn, pending_fn = null, root_contro
|
|
|
60
74
|
var pending_deferreds = new Map();
|
|
61
75
|
/** @type {Set<Block>} */
|
|
62
76
|
var paused_blocks = new Set();
|
|
77
|
+
/** @type {string | null} */
|
|
78
|
+
var streamed_id = null;
|
|
79
|
+
var streamed_errored = false;
|
|
80
|
+
var streamed_fallback = false;
|
|
81
|
+
/** @type {Comment | null} */
|
|
82
|
+
var slot_open = null;
|
|
83
|
+
/** @type {Comment | null} */
|
|
84
|
+
var slot_close = null;
|
|
63
85
|
|
|
64
86
|
function clear_paused_blocks() {
|
|
65
87
|
paused_blocks.clear();
|
|
@@ -218,6 +240,119 @@ export function try_block(node, try_fn, catch_fn, pending_fn = null, root_contro
|
|
|
218
240
|
destroy_resolved();
|
|
219
241
|
}
|
|
220
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Retires the slot wrapper markers once the slot's fate is decided: the
|
|
245
|
+
* open comment loses its marker data (it may still serve as the boundary
|
|
246
|
+
* anchor, so it must stay in the DOM) and the close comment is dropped —
|
|
247
|
+
* keeping `[`/`]` depth balanced for scans over surrounding slots.
|
|
248
|
+
* @returns {void}
|
|
249
|
+
*/
|
|
250
|
+
function neutralize_slot_markers() {
|
|
251
|
+
/** @type {Comment} */ (slot_open).data = '';
|
|
252
|
+
/** @type {ChildNode} */ (slot_close).remove();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Reads the streamed unit error envelope for this slot and routes the
|
|
257
|
+
* error into this boundary, or the nearest catch boundary above it.
|
|
258
|
+
* @param {string} id
|
|
259
|
+
* @returns {void}
|
|
260
|
+
*/
|
|
261
|
+
function route_streamed_error(id) {
|
|
262
|
+
if (try_block === null || is_destroyed(try_block)) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
neutralize_slot_markers();
|
|
266
|
+
var message = 'An error occurred during server rendering';
|
|
267
|
+
var script = document.getElementById(STREAM_ERROR_SCRIPT_PREFIX + id);
|
|
268
|
+
if (script !== null) {
|
|
269
|
+
try {
|
|
270
|
+
message = JSON.parse(/** @type {string} */ (script.textContent)).message ?? message;
|
|
271
|
+
} catch {
|
|
272
|
+
// keep the generic message
|
|
273
|
+
}
|
|
274
|
+
script.remove();
|
|
275
|
+
}
|
|
276
|
+
var error = new Error(message);
|
|
277
|
+
if (catch_fn !== null) {
|
|
278
|
+
handle_error(error);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
var outer = get_boundary_with_catch(/** @type {Block} */ (try_block));
|
|
282
|
+
if (outer === null) {
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
outer.s.c(error);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Empties the streamed slot: destroys the hydrated fallback branch and
|
|
290
|
+
* sweeps whatever remains between the wrapper comments (leftover marker
|
|
291
|
+
* comments included).
|
|
292
|
+
* @returns {void}
|
|
293
|
+
*/
|
|
294
|
+
function clear_streamed_slot() {
|
|
295
|
+
destroy_pending();
|
|
296
|
+
var node = /** @type {ChildNode} */ (slot_open).nextSibling;
|
|
297
|
+
while (node !== null && node !== slot_close) {
|
|
298
|
+
var next = node.nextSibling;
|
|
299
|
+
/** @type {ChildNode} */ (node).remove();
|
|
300
|
+
node = next;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Called by the inline stream runtime when this slot's chunk arrives after
|
|
306
|
+
* hydration: swaps the fallback for the streamed content and claims the
|
|
307
|
+
* new DOM through a boundary-scoped hydration walk (trackAsync inside the
|
|
308
|
+
* body picks its serialized envelope up from the same chunk).
|
|
309
|
+
* @param {HTMLTemplateElement | null} template
|
|
310
|
+
* @param {number | undefined} [errored]
|
|
311
|
+
* @returns {void}
|
|
312
|
+
*/
|
|
313
|
+
function activate_streamed_chunk(template, errored) {
|
|
314
|
+
if (try_block === null || is_destroyed(try_block)) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
var id = /** @type {string} */ (streamed_id);
|
|
318
|
+
streamed_id = null;
|
|
319
|
+
if (errored) {
|
|
320
|
+
clear_streamed_slot();
|
|
321
|
+
route_streamed_error(id);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
clear_streamed_slot();
|
|
325
|
+
if (template !== null) {
|
|
326
|
+
/** @type {ChildNode} */ (slot_close).before(template.content);
|
|
327
|
+
}
|
|
328
|
+
var first = /** @type {ChildNode} */ (slot_open).nextSibling;
|
|
329
|
+
if (first === null || first === slot_close) {
|
|
330
|
+
neutralize_slot_markers();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
// adopt the streamed body's own <!--[--> as the boundary anchor (the
|
|
334
|
+
// node the buffered-SSR hydration path would have used) and drop the
|
|
335
|
+
// slot wrapper comments, so the resulting DOM matches buffered SSR
|
|
336
|
+
// exactly like the pre-hydration swap path does
|
|
337
|
+
if (anchor === slot_open) {
|
|
338
|
+
anchor = first;
|
|
339
|
+
}
|
|
340
|
+
/** @type {ChildNode} */ (slot_open).remove();
|
|
341
|
+
/** @type {ChildNode} */ (slot_close).remove();
|
|
342
|
+
var previous_hydrating = hydrating;
|
|
343
|
+
var previous_hydrate_node = hydrate_node;
|
|
344
|
+
set_hydrating(true);
|
|
345
|
+
set_hydrate_node(first);
|
|
346
|
+
hydrate_next(); // consume the streamed body's <!--[-->
|
|
347
|
+
try {
|
|
348
|
+
has_resolved = true;
|
|
349
|
+
render_resolved();
|
|
350
|
+
} finally {
|
|
351
|
+
set_hydrating(previous_hydrating);
|
|
352
|
+
set_hydrate_node(previous_hydrate_node, true);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
221
356
|
function begin_request() {
|
|
222
357
|
var request_id = ++request_version;
|
|
223
358
|
active_requests.add(request_id);
|
|
@@ -312,23 +447,78 @@ export function try_block(node, try_fn, catch_fn, pending_fn = null, root_contro
|
|
|
312
447
|
};
|
|
313
448
|
|
|
314
449
|
if (hydrating && (pending_fn !== null || catch_fn !== null)) {
|
|
315
|
-
// Server wraps
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
|
|
320
|
-
has_resolved = true;
|
|
321
|
-
}
|
|
450
|
+
// Server wraps a settled try body with <!--[-->...<!--]--> markers when
|
|
451
|
+
// pending or catch is present, and the boundary hydrates as resolved.
|
|
452
|
+
// Streaming SSR instead emits a slot wrapper around the fallback:
|
|
453
|
+
// <!--[?N--> while unit N's content is still on its way, or
|
|
454
|
+
// <!--[!N--> when the unit errored after its region was already sent.
|
|
322
455
|
if (root_controlled) {
|
|
323
456
|
boundary = /** @type {Node} */ (hydrate_node);
|
|
324
457
|
}
|
|
325
|
-
|
|
458
|
+
|
|
459
|
+
var marker = /** @type {Comment} */ (hydrate_node);
|
|
460
|
+
var data = marker.nodeType === COMMENT_NODE ? marker.data : '';
|
|
461
|
+
|
|
462
|
+
// a slot marker at the boundary's own anchor always belongs to this
|
|
463
|
+
// boundary — the root boundary included: when the root suspends, the
|
|
464
|
+
// server emits its slot as the whole body, so the marker doubles as
|
|
465
|
+
// the hydration anchor
|
|
466
|
+
if (data.startsWith(HYDRATION_START_PENDING) || data.startsWith(HYDRATION_START_ERRORED)) {
|
|
467
|
+
// live streamed slot
|
|
468
|
+
streamed_id = data.slice(HYDRATION_START_PENDING.length);
|
|
469
|
+
streamed_errored = data.startsWith(HYDRATION_START_ERRORED);
|
|
470
|
+
slot_open = marker;
|
|
471
|
+
hydrate_next(); // consume the slot wrapper open
|
|
472
|
+
slot_close = /** @type {Comment} */ (skip_to_hydration_end());
|
|
473
|
+
var fallback_start = /** @type {Comment} */ (hydrate_node);
|
|
474
|
+
streamed_fallback =
|
|
475
|
+
!streamed_errored &&
|
|
476
|
+
pending_fn !== null &&
|
|
477
|
+
fallback_start.nodeType === COMMENT_NODE &&
|
|
478
|
+
fallback_start.data === HYDRATION_START;
|
|
479
|
+
if (streamed_fallback) {
|
|
480
|
+
hydrate_next(); // consume the fallback's <!--[-->
|
|
481
|
+
}
|
|
482
|
+
} else {
|
|
483
|
+
// settled content: mark as already resolved so begin_request's
|
|
484
|
+
// microtask won't transition to pending
|
|
485
|
+
if (pending_fn !== null) {
|
|
486
|
+
has_resolved = true;
|
|
487
|
+
}
|
|
488
|
+
hydrate_next(); // consume <!--[-->
|
|
489
|
+
}
|
|
326
490
|
}
|
|
327
491
|
|
|
328
492
|
try_block = create_try_block(() => {
|
|
493
|
+
if (streamed_id !== null) {
|
|
494
|
+
// the streamed body hasn't arrived yet — hydrate the fallback (if
|
|
495
|
+
// any) as the pending branch and wait for the chunk to activate us
|
|
496
|
+
if (streamed_fallback) {
|
|
497
|
+
mode = 'pending';
|
|
498
|
+
pending_branch = boundary_fn_running_block(() => {
|
|
499
|
+
/** @type {PendingFunction} */ (pending_fn)(anchor);
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
329
504
|
resolved_branch = boundary_fn_running_block(() => try_fn(anchor));
|
|
330
505
|
}, state);
|
|
331
506
|
|
|
507
|
+
if (streamed_id !== null) {
|
|
508
|
+
// continue the outer hydration walk after the slot
|
|
509
|
+
set_hydrate_node(slot_close);
|
|
510
|
+
|
|
511
|
+
var registry = (window.__RIPPLE_B__ ??= {});
|
|
512
|
+
var unit_id = streamed_id;
|
|
513
|
+
if (streamed_errored) {
|
|
514
|
+
// the inline runtime already emptied the slot and marked it errored
|
|
515
|
+
// — route the error once the surrounding hydration has finished
|
|
516
|
+
queue_microtask(() => route_streamed_error(unit_id));
|
|
517
|
+
} else {
|
|
518
|
+
registry[unit_id] = { a: activate_streamed_chunk };
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
332
522
|
if (hydrating && root_controlled) {
|
|
333
523
|
append(/** @type {ChildNode} */ (node), /** @type {Node} */ (boundary));
|
|
334
524
|
}
|
|
@@ -68,7 +68,32 @@ export type RootBoundaryOptions = {
|
|
|
68
68
|
catch?: (anchor: Node, props: { error: unknown; reset: () => void }, block: Block | null) => void;
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Called by the inline stream runtime when a boundary's chunk arrives after
|
|
73
|
+
* hydration: receives the chunk template (null for error-only chunks) and a
|
|
74
|
+
* truthy flag when the unit errored server-side.
|
|
75
|
+
*/
|
|
76
|
+
export type StreamBoundaryActivator = (
|
|
77
|
+
template: HTMLTemplateElement | null,
|
|
78
|
+
errored?: number,
|
|
79
|
+
) => void;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* `window.__RIPPLE_B__` — shared between the inline stream runtime and
|
|
83
|
+
* hydrated try boundaries. A boundary that hydrated a still-pending slot
|
|
84
|
+
* registers `{ a: activator }` under its unit id; the runtime stores `1`
|
|
85
|
+
* once the slot has been swapped or handed over.
|
|
86
|
+
*/
|
|
87
|
+
export type StreamBoundaryRegistry = Record<string | number, 1 | { a: StreamBoundaryActivator }>;
|
|
88
|
+
|
|
71
89
|
declare global {
|
|
90
|
+
interface Window {
|
|
91
|
+
/** streamed-boundary registry, see {@link StreamBoundaryRegistry} */
|
|
92
|
+
__RIPPLE_B__?: StreamBoundaryRegistry;
|
|
93
|
+
/** inline stream runtime entry point: swaps unit `id`'s chunk into its slot */
|
|
94
|
+
__RIPPLE_S__?: (id: number, errored?: number) => void;
|
|
95
|
+
}
|
|
96
|
+
|
|
72
97
|
interface Element {
|
|
73
98
|
__attributes?: {
|
|
74
99
|
checked?: boolean;
|
|
@@ -13,6 +13,7 @@ import { ASYNC_DERIVED_READ_THROWN } from '../client/constants.js';
|
|
|
13
13
|
import {
|
|
14
14
|
TRY_CATCH_BLOCK,
|
|
15
15
|
TRY_PENDING_BLOCK,
|
|
16
|
+
TRY_BLOCK,
|
|
16
17
|
REGULAR_BLOCK,
|
|
17
18
|
COMPONENT_BLOCK,
|
|
18
19
|
ROOT_BLOCK,
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
TrackAsyncRunError,
|
|
28
29
|
create_public_track_async_error,
|
|
29
30
|
serialize_track_async_error,
|
|
31
|
+
begin_stream_unit,
|
|
30
32
|
} from './index.js';
|
|
31
33
|
|
|
32
34
|
/**
|
|
@@ -87,14 +89,21 @@ export function try_block(try_fn, catch_fn = null, pending_fn = null) {
|
|
|
87
89
|
|
|
88
90
|
try {
|
|
89
91
|
try_fn();
|
|
92
|
+
|
|
93
|
+
if (created_block.o.isStreamMode() && created_block.o.hasPendingAsyncOperations()) {
|
|
94
|
+
// the body suspended: turn this boundary into a streaming flush unit —
|
|
95
|
+
// the partially-rendered body is kept (async re-runs fill its holes in
|
|
96
|
+
// place) and the live slot gets the wrapper markers plus the pending
|
|
97
|
+
// fallback (nothing, for catch-only boundaries)
|
|
98
|
+
begin_stream_unit(created_block, pending_fn);
|
|
99
|
+
}
|
|
90
100
|
} catch (error) {
|
|
91
101
|
set_active_block(created_block);
|
|
92
|
-
if (error === ASYNC_DERIVED_READ_THROWN
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return created_block;
|
|
102
|
+
if (error === ASYNC_DERIVED_READ_THROWN) {
|
|
103
|
+
// pending reads inside blocks are swallowed by run_block (they
|
|
104
|
+
// register a re-run); a sentinel escaping to here means a read
|
|
105
|
+
// happened outside any block — let an outer boundary deal with it
|
|
106
|
+
throw error;
|
|
98
107
|
}
|
|
99
108
|
|
|
100
109
|
if (created_block.f & TRY_CATCH_BLOCK) {
|
|
@@ -140,6 +149,26 @@ export function component_block(fn) {
|
|
|
140
149
|
return block(COMPONENT_BLOCK, fn, null, true);
|
|
141
150
|
}
|
|
142
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Nearest try boundary (pending or catch) — async work is accounted on it,
|
|
154
|
+
* and in stream mode it is the boundary that becomes a flush unit.
|
|
155
|
+
* @param {Block} block
|
|
156
|
+
* @returns {TryBlock}
|
|
157
|
+
*/
|
|
158
|
+
export function get_closest_try_block(block) {
|
|
159
|
+
var current = block;
|
|
160
|
+
|
|
161
|
+
while (current !== null) {
|
|
162
|
+
if (current.f & TRY_BLOCK) {
|
|
163
|
+
return /** @type {TryBlock} */ (current);
|
|
164
|
+
}
|
|
165
|
+
// the root block always carries try flags, so we never reach null
|
|
166
|
+
current = /** @type {Block} */ (current.p);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
throw new Error('No try block found');
|
|
170
|
+
}
|
|
171
|
+
|
|
143
172
|
/**
|
|
144
173
|
* @param {Block} block
|
|
145
174
|
* @returns {TryBlockWithCatch}
|
|
@@ -161,15 +190,20 @@ export function get_closest_catch_block(block) {
|
|
|
161
190
|
}
|
|
162
191
|
|
|
163
192
|
/**
|
|
193
|
+
* Cancels outstanding async work below an error boundary. Descendant try
|
|
194
|
+
* boundaries (`is_descendant`) additionally abandon their streaming flush
|
|
195
|
+
* units — their regions get replaced by the boundary's catch content, so
|
|
196
|
+
* they must never stream a stale chunk.
|
|
164
197
|
* @param {Block | null} block
|
|
198
|
+
* @param {boolean} [is_descendant=false]
|
|
165
199
|
* @returns {void}
|
|
166
200
|
*/
|
|
167
|
-
export function cancel_async_operations(block) {
|
|
201
|
+
export function cancel_async_operations(block, is_descendant = false) {
|
|
168
202
|
if (block === null) {
|
|
169
203
|
return;
|
|
170
204
|
}
|
|
171
205
|
|
|
172
|
-
if (block.f &
|
|
206
|
+
if (block.f & TRY_BLOCK) {
|
|
173
207
|
if (block.f & CAUGHT_ERROR) {
|
|
174
208
|
// already handling an error — skip
|
|
175
209
|
return;
|
|
@@ -177,11 +211,15 @@ export function cancel_async_operations(block) {
|
|
|
177
211
|
|
|
178
212
|
block.o.cancelAsyncOperations();
|
|
179
213
|
block.f |= CAUGHT_ERROR;
|
|
214
|
+
|
|
215
|
+
if (is_descendant) {
|
|
216
|
+
block.o.abandonUnit();
|
|
217
|
+
}
|
|
180
218
|
}
|
|
181
219
|
|
|
182
220
|
var child = block.first;
|
|
183
221
|
while (child !== null) {
|
|
184
|
-
cancel_async_operations(child);
|
|
222
|
+
cancel_async_operations(child, true);
|
|
185
223
|
child = child.next;
|
|
186
224
|
}
|
|
187
225
|
}
|