|
4 | 4 | BOUNDARY_EFFECT, |
5 | 5 | BOUNDARY_SUSPENDED, |
6 | 6 | EFFECT_PRESERVED, |
| 7 | + EFFECT_RAN, |
7 | 8 | EFFECT_TRANSPARENT, |
8 | 9 | RENDER_EFFECT |
9 | 10 | } from '../../constants.js'; |
@@ -33,6 +34,8 @@ import { queue_boundary_micro_task } from '../task.js'; |
33 | 34 | import * as e from '../../../shared/errors.js'; |
34 | 35 | import { DEV } from 'esm-env'; |
35 | 36 | import { from_async_derived, set_from_async_derived } from '../../reactivity/deriveds.js'; |
| 37 | +import { raf } from '../../timing.js'; |
| 38 | +import { loop } from '../../loop.js'; |
36 | 39 |
|
37 | 40 | const ASYNC_INCREMENT = Symbol(); |
38 | 41 | const ASYNC_DECREMENT = Symbol(); |
@@ -69,16 +72,20 @@ var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_EFFECT; |
69 | 72 | /** |
70 | 73 | * @param {TemplateNode} node |
71 | 74 | * @param {{ |
72 | | - * onerror?: (error: unknown, reset: () => void) => void, |
73 | | - * failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void |
74 | | - * pending?: (anchor: Node) => void |
| 75 | + * onerror?: (error: unknown, reset: () => void) => void; |
| 76 | + * failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void; |
| 77 | + * pending?: (anchor: Node) => void; |
| 78 | + * showPendingAfter?: number; |
| 79 | + * showPendingFor?: number; |
75 | 80 | * }} props |
76 | 81 | * @param {((anchor: Node) => void)} children |
77 | 82 | * @returns {void} |
78 | 83 | */ |
79 | 84 | export function boundary(node, props, children) { |
80 | 85 | var anchor = node; |
81 | 86 |
|
| 87 | + var parent_boundary = find_boundary(active_effect); |
| 88 | + |
82 | 89 | block(() => { |
83 | 90 | /** @type {Effect | null} */ |
84 | 91 | var main_effect = null; |
@@ -106,6 +113,8 @@ export function boundary(node, props, children) { |
106 | 113 | /** @type {Effect[]} */ |
107 | 114 | var effects = []; |
108 | 115 |
|
| 116 | + var keep_pending_snippet = false; |
| 117 | + |
109 | 118 | /** |
110 | 119 | * @param {() => void} snippet_fn |
111 | 120 | * @returns {Effect | null} |
@@ -145,6 +154,10 @@ export function boundary(node, props, children) { |
145 | 154 | } |
146 | 155 |
|
147 | 156 | function unsuspend() { |
| 157 | + if (keep_pending_snippet || async_count > 0) { |
| 158 | + return; |
| 159 | + } |
| 160 | + |
148 | 161 | if ((boundary.f & BOUNDARY_SUSPENDED) !== 0) { |
149 | 162 | boundary.f ^= BOUNDARY_SUSPENDED; |
150 | 163 | } |
@@ -184,19 +197,70 @@ export function boundary(node, props, children) { |
184 | 197 | } |
185 | 198 | } |
186 | 199 |
|
| 200 | + /** |
| 201 | + * @param {boolean} initial |
| 202 | + */ |
| 203 | + function show_pending_snippet(initial) { |
| 204 | + const pending = props.pending; |
| 205 | + |
| 206 | + if (pending !== undefined) { |
| 207 | + // TODO can this be false? |
| 208 | + if (main_effect !== null) { |
| 209 | + offscreen_fragment = document.createDocumentFragment(); |
| 210 | + move_effect(main_effect, offscreen_fragment); |
| 211 | + } |
| 212 | + |
| 213 | + if (pending_effect === null) { |
| 214 | + pending_effect = branch(() => pending(anchor)); |
| 215 | + } |
| 216 | + |
| 217 | + // TODO do we want to differentiate between initial render and updates here? |
| 218 | + if (!initial) { |
| 219 | + keep_pending_snippet = true; |
| 220 | + |
| 221 | + var end = raf.now() + (props.showPendingFor ?? 300); |
| 222 | + |
| 223 | + loop((now) => { |
| 224 | + if (now >= end) { |
| 225 | + keep_pending_snippet = false; |
| 226 | + unsuspend(); |
| 227 | + return false; |
| 228 | + } |
| 229 | + |
| 230 | + return true; |
| 231 | + }); |
| 232 | + } |
| 233 | + } else if (parent_boundary) { |
| 234 | + throw new Error('TODO show pending snippet on parent'); |
| 235 | + } else { |
| 236 | + throw new Error('no pending snippet to show'); |
| 237 | + } |
| 238 | + } |
| 239 | + |
187 | 240 | // @ts-ignore We re-use the effect's fn property to avoid allocation of an additional field |
188 | 241 | boundary.fn = (/** @type {unknown} */ input, /** @type {any} */ payload) => { |
189 | 242 | if (input === ASYNC_INCREMENT) { |
| 243 | + // post-init, show the pending snippet after a timeout |
| 244 | + if ((boundary.f & BOUNDARY_SUSPENDED) === 0 && (boundary.f & EFFECT_RAN) !== 0) { |
| 245 | + var start = raf.now(); |
| 246 | + var end = start + (props.showPendingAfter ?? 500); |
| 247 | + |
| 248 | + loop((now) => { |
| 249 | + if (async_count === 0) return false; |
| 250 | + if (now < end) return true; |
| 251 | + |
| 252 | + show_pending_snippet(false); |
| 253 | + }); |
| 254 | + } |
| 255 | + |
190 | 256 | boundary.f |= BOUNDARY_SUSPENDED; |
191 | 257 | async_count++; |
192 | 258 |
|
193 | | - // TODO post-init, show the pending snippet after a timeout |
194 | | - |
195 | 259 | return; |
196 | 260 | } |
197 | 261 |
|
198 | 262 | if (input === ASYNC_DECREMENT) { |
199 | | - if (--async_count === 0) { |
| 263 | + if (--async_count === 0 && !keep_pending_snippet) { |
200 | 264 | unsuspend(); |
201 | 265 |
|
202 | 266 | if (main_effect !== null) { |
@@ -307,15 +371,7 @@ export function boundary(node, props, children) { |
307 | 371 |
|
308 | 372 | if (async_count > 0) { |
309 | 373 | boundary.f |= BOUNDARY_SUSPENDED; |
310 | | - |
311 | | - if (pending) { |
312 | | - offscreen_fragment = document.createDocumentFragment(); |
313 | | - move_effect(main_effect, offscreen_fragment); |
314 | | - |
315 | | - pending_effect = branch(() => pending(anchor)); |
316 | | - } else { |
317 | | - // TODO trigger pending boundary on parent |
318 | | - } |
| 374 | + show_pending_snippet(true); |
319 | 375 | } |
320 | 376 | } |
321 | 377 |
|
|
0 commit comments