Skip to content

Commit 0dcc250

Browse files
committed
chore: refactor task microtask dispatching + boundary scheduling
1 parent a9d1f46 commit 0dcc250

File tree

16 files changed

+225
-79
lines changed

16 files changed

+225
-79
lines changed

.changeset/eleven-weeks-dance.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
chore: refactor task microtask dispatching + boundary scheduling

packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
/** @import { Context } from '../types' */
33
import * as e from '../../../errors.js';
44

5-
const valid = ['onerror', 'failed'];
5+
const valid = ['onerror', 'failed', 'pending'];
66

77
/**
88
* @param {AST.SvelteBoundary} node

packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ export function SvelteBoundary(node, context) {
3939

4040
// Capture the `failed` implicit snippet prop
4141
for (const child of node.fragment.nodes) {
42-
if (child.type === 'SnippetBlock' && child.expression.name === 'failed') {
42+
if (
43+
child.type === 'SnippetBlock' &&
44+
(child.expression.name === 'failed' || child.expression.name === 'pending')
45+
) {
4346
// we need to delay the visit of the snippets in case they access a ConstTag that is declared
4447
// after the snippets so that the visitor for the const tag can be updated
4548
snippets_visits.push(() => {

packages/svelte/src/internal/client/dom/blocks/await.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
set_dev_current_component_function
1414
} from '../../runtime.js';
1515
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
16-
import { queue_micro_task } from '../task.js';
16+
import { queue_post_micro_task } from '../task.js';
1717
import { UNINITIALIZED } from '../../../../constants.js';
1818

1919
const PENDING = 0;
@@ -148,7 +148,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
148148
} else {
149149
// Wait a microtask before checking if we should show the pending state as
150150
// the promise might have resolved by the next microtask.
151-
queue_micro_task(() => {
151+
queue_post_micro_task(() => {
152152
if (!resolved) update(PENDING, true);
153153
});
154154
}

packages/svelte/src/internal/client/dom/blocks/boundary.js

+132-23
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
/** @import { Effect, TemplateNode, } from '#client' */
22

33
import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '../../constants.js';
4-
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
4+
import {
5+
block,
6+
branch,
7+
destroy_effect,
8+
pause_effect,
9+
resume_effect
10+
} from '../../reactivity/effects.js';
511
import {
612
active_effect,
713
active_reaction,
@@ -20,7 +26,11 @@ import {
2026
remove_nodes,
2127
set_hydrate_node
2228
} from '../hydration.js';
23-
import { queue_micro_task } from '../task.js';
29+
import { get_next_sibling } from '../operations.js';
30+
import { queue_boundary_micro_task } from '../task.js';
31+
32+
const ASYNC_INCREMENT = Symbol();
33+
const ASYNC_DECREMENT = Symbol();
2434

2535
/**
2636
* @param {Effect} boundary
@@ -49,6 +59,7 @@ function with_boundary(boundary, fn) {
4959
* @param {{
5060
* onerror?: (error: unknown, reset: () => void) => void,
5161
* failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void
62+
* pending?: (anchor: Node) => void
5263
* }} props
5364
* @param {((anchor: Node) => void)} boundary_fn
5465
* @returns {void}
@@ -58,14 +69,106 @@ export function boundary(node, props, boundary_fn) {
5869

5970
/** @type {Effect} */
6071
var boundary_effect;
72+
/** @type {Effect | null} */
73+
var async_effect = null;
74+
/** @type {DocumentFragment | null} */
75+
var async_fragment = null;
76+
var async_count = 0;
6177

6278
block(() => {
6379
var boundary = /** @type {Effect} */ (active_effect);
6480
var hydrate_open = hydrate_node;
6581
var is_creating_fallback = false;
6682

67-
// We re-use the effect's fn property to avoid allocation of an additional field
68-
boundary.fn = (/** @type {unknown}} */ error) => {
83+
const render_snippet = (/** @type { () => void } */ snippet_fn) => {
84+
with_boundary(boundary, () => {
85+
is_creating_fallback = true;
86+
87+
try {
88+
boundary_effect = branch(() => {
89+
snippet_fn();
90+
});
91+
} catch (error) {
92+
handle_error(error, boundary, null, boundary.ctx);
93+
}
94+
95+
reset_is_throwing_error();
96+
is_creating_fallback = false;
97+
});
98+
};
99+
100+
// @ts-ignore We re-use the effect's fn property to avoid allocation of an additional field
101+
boundary.fn = (/** @type {unknown} */ input) => {
102+
let pending = props.pending;
103+
104+
if (input === ASYNC_INCREMENT) {
105+
if (!pending) {
106+
return false;
107+
}
108+
109+
if (async_count++ === 0) {
110+
queue_boundary_micro_task(() => {
111+
if (async_effect || !boundary_effect) {
112+
return;
113+
}
114+
115+
var effect = boundary_effect;
116+
async_effect = boundary_effect;
117+
118+
pause_effect(
119+
async_effect,
120+
() => {
121+
/** @type {TemplateNode | null} */
122+
var node = effect.nodes_start;
123+
var end = effect.nodes_end;
124+
async_fragment = document.createDocumentFragment();
125+
126+
while (node !== null) {
127+
/** @type {TemplateNode | null} */
128+
var sibling =
129+
node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node));
130+
131+
node.remove();
132+
async_fragment.append(node);
133+
node = sibling;
134+
}
135+
},
136+
false
137+
);
138+
139+
render_snippet(() => {
140+
pending(anchor);
141+
});
142+
});
143+
}
144+
145+
return true;
146+
}
147+
148+
if (input === ASYNC_DECREMENT) {
149+
if (!pending) {
150+
return false;
151+
}
152+
153+
if (--async_count === 0) {
154+
queue_boundary_micro_task(() => {
155+
if (!async_effect) {
156+
return;
157+
}
158+
if (boundary_effect) {
159+
destroy_effect(boundary_effect);
160+
}
161+
boundary_effect = async_effect;
162+
async_effect = null;
163+
anchor.before(/** @type {DocumentFragment} */ (async_fragment));
164+
resume_effect(boundary_effect);
165+
});
166+
}
167+
168+
return true;
169+
}
170+
171+
var error = input;
69172
var onerror = props.onerror;
70173
let failed = props.failed;
71174

@@ -96,25 +199,13 @@ export function boundary(node, props, boundary_fn) {
96199
}
97200

98201
if (failed) {
99-
// Render the `failed` snippet in a microtask
100-
queue_micro_task(() => {
101-
with_boundary(boundary, () => {
102-
is_creating_fallback = true;
103-
104-
try {
105-
boundary_effect = branch(() => {
106-
failed(
107-
anchor,
108-
() => error,
109-
() => reset
110-
);
111-
});
112-
} catch (error) {
113-
handle_error(error, boundary, null, boundary.ctx);
114-
}
115-
116-
reset_is_throwing_error();
117-
is_creating_fallback = false;
202+
queue_boundary_micro_task(() => {
203+
render_snippet(() => {
204+
failed(
205+
anchor,
206+
() => error,
207+
() => reset
208+
);
118209
});
119210
});
120211
}
@@ -132,3 +223,21 @@ export function boundary(node, props, boundary_fn) {
132223
anchor = hydrate_node;
133224
}
134225
}
226+
227+
/**
228+
* @param {Effect | null} effect
229+
* @param {typeof ASYNC_INCREMENT | typeof ASYNC_DECREMENT} trigger
230+
*/
231+
export function trigger_async_boundary(effect, trigger) {
232+
var current = effect;
233+
234+
while (current !== null) {
235+
if ((current.f & BOUNDARY_EFFECT) !== 0) {
236+
// @ts-ignore
237+
if (current.fn(trigger)) {
238+
return;
239+
}
240+
}
241+
current = current.parent;
242+
}
243+
}

packages/svelte/src/internal/client/dom/blocks/each.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import {
3434
import { source, mutable_source, internal_set } from '../../reactivity/sources.js';
3535
import { array_from, is_array } from '../../../shared/utils.js';
3636
import { INERT } from '../../constants.js';
37-
import { queue_micro_task } from '../task.js';
37+
import { queue_post_micro_task } from '../task.js';
3838
import { active_effect, active_reaction, get } from '../../runtime.js';
3939
import { DEV } from 'esm-env';
4040
import { derived_safe_equal } from '../../reactivity/deriveds.js';
@@ -470,7 +470,7 @@ function reconcile(array, state, anchor, render_fn, flags, is_inert, get_key, ge
470470
}
471471

472472
if (is_animated) {
473-
queue_micro_task(() => {
473+
queue_post_micro_task(() => {
474474
if (to_animate === undefined) return;
475475
for (item of to_animate) {
476476
item.a?.apply();

packages/svelte/src/internal/client/dom/css.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { DEV } from 'esm-env';
2-
import { queue_micro_task } from './task.js';
2+
import { queue_post_micro_task } from './task.js';
33
import { register_style } from '../dev/css.js';
44

55
/**
66
* @param {Node} anchor
77
* @param {{ hash: string, code: string }} css
88
*/
99
export function append_styles(anchor, css) {
10-
// Use `queue_micro_task` to ensure `anchor` is in the DOM, otherwise getRootNode() will yield wrong results
11-
queue_micro_task(() => {
10+
// Use `queue_post_micro_task` to ensure `anchor` is in the DOM, otherwise getRootNode() will yield wrong results
11+
queue_post_micro_task(() => {
1212
var root = anchor.getRootNode();
1313

1414
var target = /** @type {ShadowRoot} */ (root).host

packages/svelte/src/internal/client/dom/elements/bindings/input.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { render_effect, teardown } from '../../../reactivity/effects.js';
33
import { listen_to_event_and_reset_event } from './shared.js';
44
import * as e from '../../../errors.js';
55
import { is } from '../../../proxy.js';
6-
import { queue_micro_task } from '../../task.js';
6+
import { queue_post_micro_task } from '../../task.js';
77
import { hydrating } from '../../hydration.js';
88
import { is_runes, untrack } from '../../../runtime.js';
99

@@ -158,14 +158,14 @@ export function bind_group(inputs, group_index, input, get, set = get) {
158158
if (!pending.has(binding_group)) {
159159
pending.add(binding_group);
160160

161-
queue_micro_task(() => {
161+
queue_post_micro_task(() => {
162162
// necessary to maintain binding group order in all insertion scenarios
163163
binding_group.sort((a, b) => (a.compareDocumentPosition(b) === 4 ? -1 : 1));
164164
pending.delete(binding_group);
165165
});
166166
}
167167

168-
queue_micro_task(() => {
168+
queue_post_micro_task(() => {
169169
if (hydration_mismatch) {
170170
var value;
171171

packages/svelte/src/internal/client/dom/elements/bindings/this.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { STATE_SYMBOL } from '../../../constants.js';
22
import { effect, render_effect } from '../../../reactivity/effects.js';
33
import { untrack } from '../../../runtime.js';
4-
import { queue_micro_task } from '../../task.js';
4+
import { queue_post_micro_task } from '../../task.js';
55

66
/**
77
* @param {any} bound_value
@@ -49,7 +49,7 @@ export function bind_this(element_or_component = {}, update, get_value, get_part
4949

5050
return () => {
5151
// We cannot use effects in the teardown phase, we we use a microtask instead.
52-
queue_micro_task(() => {
52+
queue_post_micro_task(() => {
5353
if (parts && is_bound_this(get_value(...parts), element_or_component)) {
5454
update(null, ...parts);
5555
}

packages/svelte/src/internal/client/dom/elements/events.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { teardown } from '../../reactivity/effects.js';
33
import { define_property, is_array } from '../../../shared/utils.js';
44
import { hydrating } from '../hydration.js';
5-
import { queue_micro_task } from '../task.js';
5+
import { queue_post_micro_task } from '../task.js';
66
import { FILENAME } from '../../../../constants.js';
77
import * as w from '../../warnings.js';
88
import {
@@ -77,7 +77,7 @@ export function create_event(event_name, dom, handler, options) {
7777
event_name.startsWith('touch') ||
7878
event_name === 'wheel'
7979
) {
80-
queue_micro_task(() => {
80+
queue_post_micro_task(() => {
8181
dom.addEventListener(event_name, target_handler, options);
8282
});
8383
} else {

packages/svelte/src/internal/client/dom/elements/misc.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { hydrating } from '../hydration.js';
22
import { clear_text_content, get_first_child } from '../operations.js';
3-
import { queue_micro_task } from '../task.js';
3+
import { queue_post_micro_task } from '../task.js';
44

55
/**
66
* @param {HTMLElement} dom
@@ -12,7 +12,7 @@ export function autofocus(dom, value) {
1212
const body = document.body;
1313
dom.autofocus = true;
1414

15-
queue_micro_task(() => {
15+
queue_post_micro_task(() => {
1616
if (document.activeElement === body) {
1717
dom.focus();
1818
}

packages/svelte/src/internal/client/dom/elements/transitions.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { should_intro } from '../../render.js';
1313
import { current_each_item } from '../blocks/each.js';
1414
import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js';
1515
import { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '../../constants.js';
16-
import { queue_micro_task } from '../task.js';
16+
import { queue_post_micro_task } from '../task.js';
1717

1818
/**
1919
* @param {Element} element
@@ -326,7 +326,7 @@ function animate(element, options, counterpart, t2, on_finish) {
326326
var a;
327327
var aborted = false;
328328

329-
queue_micro_task(() => {
329+
queue_post_micro_task(() => {
330330
if (aborted) return;
331331
var o = options({ direction: is_intro ? 'in' : 'out' });
332332
a = animate(element, o, counterpart, t2, on_finish);

0 commit comments

Comments
 (0)