ripple 0.3.11 → 0.3.13
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 +43 -0
- package/package.json +8 -2
- package/src/compiler/phases/1-parse/index.js +73 -30
- package/src/compiler/phases/2-analyze/index.js +28 -58
- package/src/compiler/phases/3-transform/client/index.js +127 -164
- package/src/compiler/phases/3-transform/segments.js +4 -8
- package/src/compiler/phases/3-transform/server/index.js +210 -360
- package/src/compiler/types/import.d.ts +0 -12
- package/src/compiler/types/index.d.ts +12 -5
- package/src/compiler/types/parse.d.ts +2 -0
- package/src/compiler/utils.js +39 -44
- package/src/helpers.d.ts +2 -0
- package/src/runtime/index-client.js +15 -13
- package/src/runtime/index-server.js +18 -11
- package/src/runtime/internal/client/blocks.js +19 -23
- package/src/runtime/internal/client/constants.js +20 -9
- package/src/runtime/internal/client/index.js +14 -4
- package/src/runtime/internal/client/runtime.js +435 -173
- package/src/runtime/internal/client/try.js +334 -156
- package/src/runtime/internal/client/types.d.ts +26 -0
- package/src/runtime/internal/server/blocks.js +183 -0
- package/src/runtime/internal/server/constants.js +7 -0
- package/src/runtime/internal/server/index.js +780 -148
- package/src/runtime/internal/server/types.d.ts +35 -0
- package/src/server/index.js +1 -1
- package/src/utils/async.js +35 -0
- package/src/utils/builders.js +3 -1
- package/tests/client/__snapshots__/computed-properties.test.rsrx.snap +49 -0
- package/tests/client/__snapshots__/for.test.rsrx.snap +319 -0
- package/tests/client/__snapshots__/html.test.rsrx.snap +40 -0
- package/tests/client/_etc.test.rsrx +7 -0
- package/tests/client/array/{array.static.test.ripple → array.static.test.rsrx} +18 -20
- package/tests/client/async-suspend.test.rsrx +662 -0
- package/tests/client/basic/__snapshots__/basic.attributes.test.rsrx.snap +60 -0
- package/tests/client/basic/__snapshots__/basic.rendering.test.rsrx.snap +59 -0
- package/tests/client/basic/{basic.errors.test.ripple → basic.errors.test.rsrx} +2 -2
- package/tests/client/compiler/__snapshots__/compiler.assignments.test.rsrx.snap +12 -0
- package/tests/client/compiler/__snapshots__/compiler.typescript.test.rsrx.snap +46 -0
- package/tests/client/compiler/{compiler.try-in-function.test.ripple → compiler.try-in-function.test.rsrx} +8 -6
- package/tests/client/composite/__snapshots__/composite.render.test.rsrx.snap +37 -0
- package/tests/client/{function-overload.test.ripple → function-overload.test.rsrx} +1 -1
- package/tests/client/try.test.rsrx +1702 -0
- package/tests/hydration/build-components.js +5 -3
- package/tests/hydration/compiled/client/head.js +11 -11
- package/tests/hydration/compiled/client/mixed-control-flow.js +55 -70
- package/tests/hydration/compiled/client/nested-control-flow.js +72 -88
- package/tests/hydration/compiled/client/try.js +42 -54
- package/tests/hydration/compiled/server/basic.js +491 -369
- package/tests/hydration/compiled/server/composite.js +153 -128
- package/tests/hydration/compiled/server/events.js +166 -145
- package/tests/hydration/compiled/server/for.js +821 -677
- package/tests/hydration/compiled/server/head.js +200 -165
- package/tests/hydration/compiled/server/hmr.js +62 -54
- package/tests/hydration/compiled/server/html-in-template.js +64 -55
- package/tests/hydration/compiled/server/html.js +1477 -1360
- package/tests/hydration/compiled/server/if-children.js +448 -408
- package/tests/hydration/compiled/server/if.js +204 -171
- package/tests/hydration/compiled/server/mixed-control-flow.js +237 -195
- package/tests/hydration/compiled/server/nested-control-flow.js +533 -467
- package/tests/hydration/compiled/server/portal.js +94 -107
- package/tests/hydration/compiled/server/reactivity.js +87 -64
- package/tests/hydration/compiled/server/return.js +1424 -1174
- package/tests/hydration/compiled/server/switch.js +268 -238
- package/tests/hydration/compiled/server/try.js +98 -87
- package/tests/hydration/components/{mixed-control-flow.ripple → mixed-control-flow.rsrx} +2 -2
- package/tests/hydration/components/{try.ripple → try.rsrx} +4 -2
- package/tests/hydration/mixed-control-flow.test.js +14 -0
- package/tests/hydration/nested-control-flow.test.js +50 -48
- package/tests/hydration/try.test.js +25 -0
- package/tests/server/__snapshots__/compiler.test.ripple.snap +0 -32
- package/tests/server/__snapshots__/compiler.test.rsrx.snap +95 -0
- package/tests/server/{compiler.test.ripple → compiler.test.rsrx} +0 -17
- package/tests/server/{html-nesting-validation.test.ripple → html-nesting-validation.test.rsrx} +3 -3
- package/tests/server/streaming-ssr.test.rsrx +115 -0
- package/tests/server/try.test.rsrx +503 -0
- package/tests/utils/compiler-compat-config.test.js +3 -3
- package/tests/utils/vite-plugin-config.test.js +1 -1
- package/tests/utils/vite-plugin-hmr.test.js +5 -5
- package/tsconfig.json +4 -0
- package/types/index.d.ts +13 -23
- package/types/server.d.ts +43 -16
- package/tests/client/_etc.test.ripple +0 -5
- package/tests/client/async-suspend.test.ripple +0 -94
- package/tests/client/try.test.ripple +0 -196
- package/tests/server/streaming-ssr.test.ripple +0 -68
- package/tests/server/try.test.ripple +0 -82
- /package/tests/client/array/{array.copy-within.test.ripple → array.copy-within.test.rsrx} +0 -0
- /package/tests/client/array/{array.derived.test.ripple → array.derived.test.rsrx} +0 -0
- /package/tests/client/array/{array.iteration.test.ripple → array.iteration.test.rsrx} +0 -0
- /package/tests/client/array/{array.mutations.test.ripple → array.mutations.test.rsrx} +0 -0
- /package/tests/client/array/{array.to-methods.test.ripple → array.to-methods.test.rsrx} +0 -0
- /package/tests/client/basic/{basic.attributes.test.ripple → basic.attributes.test.rsrx} +0 -0
- /package/tests/client/basic/{basic.collections.test.ripple → basic.collections.test.rsrx} +0 -0
- /package/tests/client/basic/{basic.components.test.ripple → basic.components.test.rsrx} +0 -0
- /package/tests/client/basic/{basic.events.test.ripple → basic.events.test.rsrx} +0 -0
- /package/tests/client/basic/{basic.get-set.test.ripple → basic.get-set.test.rsrx} +0 -0
- /package/tests/client/basic/{basic.hmr.test.ripple → basic.hmr.test.rsrx} +0 -0
- /package/tests/client/basic/{basic.reactivity.test.ripple → basic.reactivity.test.rsrx} +0 -0
- /package/tests/client/basic/{basic.rendering.test.ripple → basic.rendering.test.rsrx} +0 -0
- /package/tests/client/basic/{basic.styling.test.ripple → basic.styling.test.rsrx} +0 -0
- /package/tests/client/basic/{basic.utilities.test.ripple → basic.utilities.test.rsrx} +0 -0
- /package/tests/client/{boundaries.test.ripple → boundaries.test.rsrx} +0 -0
- /package/tests/client/compiler/{compiler.assignments.test.ripple → compiler.assignments.test.rsrx} +0 -0
- /package/tests/client/compiler/{compiler.attributes.test.ripple → compiler.attributes.test.rsrx} +0 -0
- /package/tests/client/compiler/{compiler.basic.test.ripple → compiler.basic.test.rsrx} +0 -0
- /package/tests/client/compiler/{compiler.regex.test.ripple → compiler.regex.test.rsrx} +0 -0
- /package/tests/client/compiler/{compiler.tracked-access.test.ripple → compiler.tracked-access.test.rsrx} +0 -0
- /package/tests/client/compiler/{compiler.typescript.test.ripple → compiler.typescript.test.rsrx} +0 -0
- /package/tests/client/composite/{composite.dynamic-components.test.ripple → composite.dynamic-components.test.rsrx} +0 -0
- /package/tests/client/composite/{composite.generics.test.ripple → composite.generics.test.rsrx} +0 -0
- /package/tests/client/composite/{composite.props.test.ripple → composite.props.test.rsrx} +0 -0
- /package/tests/client/composite/{composite.reactivity.test.ripple → composite.reactivity.test.rsrx} +0 -0
- /package/tests/client/composite/{composite.render.test.ripple → composite.render.test.rsrx} +0 -0
- /package/tests/client/{computed-properties.test.ripple → computed-properties.test.rsrx} +0 -0
- /package/tests/client/{context.test.ripple → context.test.rsrx} +0 -0
- /package/tests/client/css/{global-additional-cases.test.ripple → global-additional-cases.test.rsrx} +0 -0
- /package/tests/client/css/{global-advanced-selectors.test.ripple → global-advanced-selectors.test.rsrx} +0 -0
- /package/tests/client/css/{global-at-rules.test.ripple → global-at-rules.test.rsrx} +0 -0
- /package/tests/client/css/{global-basic.test.ripple → global-basic.test.rsrx} +0 -0
- /package/tests/client/css/{global-classes-ids.test.ripple → global-classes-ids.test.rsrx} +0 -0
- /package/tests/client/css/{global-combinators.test.ripple → global-combinators.test.rsrx} +0 -0
- /package/tests/client/css/{global-complex-nesting.test.ripple → global-complex-nesting.test.rsrx} +0 -0
- /package/tests/client/css/{global-edge-cases.test.ripple → global-edge-cases.test.rsrx} +0 -0
- /package/tests/client/css/{global-keyframes.test.ripple → global-keyframes.test.rsrx} +0 -0
- /package/tests/client/css/{global-nested.test.ripple → global-nested.test.rsrx} +0 -0
- /package/tests/client/css/{global-pseudo.test.ripple → global-pseudo.test.rsrx} +0 -0
- /package/tests/client/css/{global-scoping.test.ripple → global-scoping.test.rsrx} +0 -0
- /package/tests/client/css/{style-identifier.test.ripple → style-identifier.test.rsrx} +0 -0
- /package/tests/client/{date.test.ripple → date.test.rsrx} +0 -0
- /package/tests/client/{dynamic-elements.test.ripple → dynamic-elements.test.rsrx} +0 -0
- /package/tests/client/{events.test.ripple → events.test.rsrx} +0 -0
- /package/tests/client/{for.test.ripple → for.test.rsrx} +0 -0
- /package/tests/client/{function-overload-import.ripple → function-overload-import.rsrx} +0 -0
- /package/tests/client/{head.test.ripple → head.test.rsrx} +0 -0
- /package/tests/client/{html.test.ripple → html.test.rsrx} +0 -0
- /package/tests/client/{input-value.test.ripple → input-value.test.rsrx} +0 -0
- /package/tests/client/{lazy-destructuring.test.ripple → lazy-destructuring.test.rsrx} +0 -0
- /package/tests/client/{map.test.ripple → map.test.rsrx} +0 -0
- /package/tests/client/{media-query.test.ripple → media-query.test.rsrx} +0 -0
- /package/tests/client/{object.test.ripple → object.test.rsrx} +0 -0
- /package/tests/client/{portal.test.ripple → portal.test.rsrx} +0 -0
- /package/tests/client/{ref.test.ripple → ref.test.rsrx} +0 -0
- /package/tests/client/{return.test.ripple → return.test.rsrx} +0 -0
- /package/tests/client/{set.test.ripple → set.test.rsrx} +0 -0
- /package/tests/client/{svg.test.ripple → svg.test.rsrx} +0 -0
- /package/tests/client/{switch.test.ripple → switch.test.rsrx} +0 -0
- /package/tests/client/{tsx.test.ripple → tsx.test.rsrx} +0 -0
- /package/tests/client/{typescript-generics.test.ripple → typescript-generics.test.rsrx} +0 -0
- /package/tests/client/url/{url.derived.test.ripple → url.derived.test.rsrx} +0 -0
- /package/tests/client/url/{url.parsing.test.ripple → url.parsing.test.rsrx} +0 -0
- /package/tests/client/url/{url.partial-removal.test.ripple → url.partial-removal.test.rsrx} +0 -0
- /package/tests/client/url/{url.reactivity.test.ripple → url.reactivity.test.rsrx} +0 -0
- /package/tests/client/url/{url.serialization.test.ripple → url.serialization.test.rsrx} +0 -0
- /package/tests/client/url-search-params/{url-search-params.derived.test.ripple → url-search-params.derived.test.rsrx} +0 -0
- /package/tests/client/url-search-params/{url-search-params.initialization.test.ripple → url-search-params.initialization.test.rsrx} +0 -0
- /package/tests/client/url-search-params/{url-search-params.iteration.test.ripple → url-search-params.iteration.test.rsrx} +0 -0
- /package/tests/client/url-search-params/{url-search-params.mutation.test.ripple → url-search-params.mutation.test.rsrx} +0 -0
- /package/tests/client/url-search-params/{url-search-params.retrieval.test.ripple → url-search-params.retrieval.test.rsrx} +0 -0
- /package/tests/client/url-search-params/{url-search-params.serialization.test.ripple → url-search-params.serialization.test.rsrx} +0 -0
- /package/tests/client/url-search-params/{url-search-params.tracked-url.test.ripple → url-search-params.tracked-url.test.rsrx} +0 -0
- /package/tests/hydration/components/{basic.ripple → basic.rsrx} +0 -0
- /package/tests/hydration/components/{composite.ripple → composite.rsrx} +0 -0
- /package/tests/hydration/components/{events.ripple → events.rsrx} +0 -0
- /package/tests/hydration/components/{for.ripple → for.rsrx} +0 -0
- /package/tests/hydration/components/{head.ripple → head.rsrx} +0 -0
- /package/tests/hydration/components/{hmr.ripple → hmr.rsrx} +0 -0
- /package/tests/hydration/components/{html-in-template.ripple → html-in-template.rsrx} +0 -0
- /package/tests/hydration/components/{html.ripple → html.rsrx} +0 -0
- /package/tests/hydration/components/{if-children.ripple → if-children.rsrx} +0 -0
- /package/tests/hydration/components/{if.ripple → if.rsrx} +0 -0
- /package/tests/hydration/components/{nested-control-flow.ripple → nested-control-flow.rsrx} +0 -0
- /package/tests/hydration/components/{portal.ripple → portal.rsrx} +0 -0
- /package/tests/hydration/components/{reactivity.ripple → reactivity.rsrx} +0 -0
- /package/tests/hydration/components/{return.ripple → return.rsrx} +0 -0
- /package/tests/hydration/components/{switch.ripple → switch.rsrx} +0 -0
- /package/tests/server/{await.test.ripple → await.test.rsrx} +0 -0
- /package/tests/server/{basic.attributes.test.ripple → basic.attributes.test.rsrx} +0 -0
- /package/tests/server/{basic.components.test.ripple → basic.components.test.rsrx} +0 -0
- /package/tests/server/{basic.test.ripple → basic.test.rsrx} +0 -0
- /package/tests/server/{composite.props.test.ripple → composite.props.test.rsrx} +0 -0
- /package/tests/server/{composite.test.ripple → composite.test.rsrx} +0 -0
- /package/tests/server/{context.test.ripple → context.test.rsrx} +0 -0
- /package/tests/server/{dynamic-elements.test.ripple → dynamic-elements.test.rsrx} +0 -0
- /package/tests/server/{for.test.ripple → for.test.rsrx} +0 -0
- /package/tests/server/{head.test.ripple → head.test.rsrx} +0 -0
- /package/tests/server/{if.test.ripple → if.test.rsrx} +0 -0
- /package/tests/server/{lazy-destructuring.test.ripple → lazy-destructuring.test.rsrx} +0 -0
- /package/tests/server/{return.test.ripple → return.test.rsrx} +0 -0
- /package/tests/server/{style-identifier.test.ripple → style-identifier.test.rsrx} +0 -0
- /package/tests/server/{switch.test.ripple → switch.test.rsrx} +0 -0
|
@@ -0,0 +1,1702 @@
|
|
|
1
|
+
import type { Tracked } from 'ripple';
|
|
2
|
+
import {
|
|
3
|
+
bindValue,
|
|
4
|
+
effect,
|
|
5
|
+
flushSync,
|
|
6
|
+
track,
|
|
7
|
+
trackAsync,
|
|
8
|
+
peek,
|
|
9
|
+
UNINITIALIZED,
|
|
10
|
+
SUSPENSE_REJECTED,
|
|
11
|
+
SUSPENSE_PENDING,
|
|
12
|
+
} from 'ripple';
|
|
13
|
+
|
|
14
|
+
describe('try block with catch and pending', () => {
|
|
15
|
+
it('catch block works when a child throws synchronously', async () => {
|
|
16
|
+
component App() {
|
|
17
|
+
try {
|
|
18
|
+
<ThrowingChild />
|
|
19
|
+
} pending {
|
|
20
|
+
<p>{'loading...'}</p>
|
|
21
|
+
} catch (err) {
|
|
22
|
+
<p>{'caught error'}</p>
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
component ThrowingChild() {
|
|
27
|
+
throw new Error('sync error');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
render(App);
|
|
31
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
32
|
+
flushSync();
|
|
33
|
+
|
|
34
|
+
expect(container.innerHTML).toContain('caught error');
|
|
35
|
+
expect(container.innerHTML).not.toContain('loading...');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('catch block works when component throws after trackAsync with pending block', async () => {
|
|
39
|
+
component App() {
|
|
40
|
+
try {
|
|
41
|
+
<ThrowingAfterAsync />
|
|
42
|
+
} pending {
|
|
43
|
+
<p>{'loading...'}</p>
|
|
44
|
+
} catch (err) {
|
|
45
|
+
<p>{'caught error'}</p>
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
component ThrowingAfterAsync() {
|
|
50
|
+
let &[data] = trackAsync(() => Promise.resolve('hello'));
|
|
51
|
+
throw new Error('error after await');
|
|
52
|
+
<p>{data}</p>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
render(App);
|
|
56
|
+
|
|
57
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
58
|
+
flushSync();
|
|
59
|
+
|
|
60
|
+
expect(container.innerHTML).toContain('caught error');
|
|
61
|
+
expect(container.innerHTML).not.toContain('loading...');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('catch block works when trackAsync rejects', async () => {
|
|
65
|
+
component App() {
|
|
66
|
+
try {
|
|
67
|
+
let &[data] = trackAsync(() => Promise.reject(new Error('rejected')));
|
|
68
|
+
<p>{data}</p>
|
|
69
|
+
} pending {
|
|
70
|
+
<p>{'loading...'}</p>
|
|
71
|
+
} catch (err) {
|
|
72
|
+
<p>{'caught rejection'}</p>
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
render(App);
|
|
77
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
78
|
+
flushSync();
|
|
79
|
+
|
|
80
|
+
expect(container.innerHTML).toContain('caught rejection');
|
|
81
|
+
expect(container.innerHTML).not.toContain('loading...');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('catch replaces the retained resolved branch when a subsequent request rejects', async () => {
|
|
85
|
+
const requests = new Map<
|
|
86
|
+
number,
|
|
87
|
+
{ resolve: (value: string) => void; reject: (error: Error) => void }
|
|
88
|
+
>();
|
|
89
|
+
|
|
90
|
+
function createRequest(version: number) {
|
|
91
|
+
let resolve_value: (value: string) => void = () => {};
|
|
92
|
+
let reject_value: (error: Error) => void = () => {};
|
|
93
|
+
|
|
94
|
+
const promise = new Promise<string>((resolve, reject) => {
|
|
95
|
+
resolve_value = resolve;
|
|
96
|
+
reject_value = reject;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
requests.set(version, { resolve: resolve_value, reject: reject_value });
|
|
100
|
+
|
|
101
|
+
return promise;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
component App() {
|
|
105
|
+
let &[version] = track(0);
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
let &[data] = trackAsync(() => createRequest(version));
|
|
109
|
+
<p class="resolved">{data}</p>
|
|
110
|
+
} pending {
|
|
111
|
+
<p class="pending">{'loading...'}</p>
|
|
112
|
+
} catch (err) {
|
|
113
|
+
<p class="caught">{(err as Error).message}</p>
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
<button class="retry" onClick={() => version++}>{'Retry'}</button>
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
render(App);
|
|
120
|
+
|
|
121
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
122
|
+
flushSync();
|
|
123
|
+
expect(container.querySelector('.pending')?.textContent).toBe('loading...');
|
|
124
|
+
|
|
125
|
+
(requests.get(0) as { resolve: (value: string) => void }).resolve('resolved value');
|
|
126
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
127
|
+
flushSync();
|
|
128
|
+
|
|
129
|
+
expect(container.querySelector('.resolved')?.textContent).toBe('resolved value');
|
|
130
|
+
expect(container.querySelector('.pending')).toBeNull();
|
|
131
|
+
expect(container.querySelector('.caught')).toBeNull();
|
|
132
|
+
|
|
133
|
+
(container.querySelector('.retry') as HTMLButtonElement).click();
|
|
134
|
+
flushSync();
|
|
135
|
+
|
|
136
|
+
expect(container.querySelector('.resolved')?.textContent).toBe('resolved value');
|
|
137
|
+
expect(container.querySelector('.pending')).toBeNull();
|
|
138
|
+
|
|
139
|
+
(requests.get(1) as { reject: (error: Error) => void }).reject(new Error('failed retry'));
|
|
140
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
141
|
+
flushSync();
|
|
142
|
+
|
|
143
|
+
expect(container.querySelector('.caught')?.textContent).toBe('failed retry');
|
|
144
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
145
|
+
expect(container.querySelector('.pending')).toBeNull();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('retrying from catch shows pending before resolving back to the resolved branch', async () => {
|
|
149
|
+
const requests = new Map<
|
|
150
|
+
number,
|
|
151
|
+
{ resolve: (value: string) => void; reject: (error: Error) => void }
|
|
152
|
+
>();
|
|
153
|
+
|
|
154
|
+
function createRequest(version: number) {
|
|
155
|
+
let resolve_value: (value: string) => void = () => {};
|
|
156
|
+
let reject_value: (error: Error) => void = () => {};
|
|
157
|
+
|
|
158
|
+
const promise = new Promise<string>((resolve, reject) => {
|
|
159
|
+
resolve_value = resolve;
|
|
160
|
+
reject_value = reject;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
requests.set(version, { resolve: resolve_value, reject: reject_value });
|
|
164
|
+
|
|
165
|
+
return promise;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
component App() {
|
|
169
|
+
let &[version] = track(0);
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
let &[data] = trackAsync(() => {
|
|
173
|
+
version;
|
|
174
|
+
return createRequest(version);
|
|
175
|
+
});
|
|
176
|
+
<p class="resolved">{data}</p>
|
|
177
|
+
} pending {
|
|
178
|
+
<p class="pending">{'loading...'}</p>
|
|
179
|
+
} catch (err, reset) {
|
|
180
|
+
<p class="caught">{(err as Error).message}</p>
|
|
181
|
+
<button
|
|
182
|
+
class="retry"
|
|
183
|
+
onClick={() => {
|
|
184
|
+
version++;
|
|
185
|
+
reset();
|
|
186
|
+
}}
|
|
187
|
+
>
|
|
188
|
+
{'Retry'}
|
|
189
|
+
</button>
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
render(App);
|
|
194
|
+
|
|
195
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
196
|
+
flushSync();
|
|
197
|
+
expect(container.querySelector('.pending')?.textContent).toBe('loading...');
|
|
198
|
+
|
|
199
|
+
(requests.get(0) as { reject: (error: Error) => void }).reject(new Error('initial failure'));
|
|
200
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
201
|
+
flushSync();
|
|
202
|
+
|
|
203
|
+
expect(container.querySelector('.caught')?.textContent).toBe('initial failure');
|
|
204
|
+
expect(container.querySelector('.pending')).toBeNull();
|
|
205
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
206
|
+
|
|
207
|
+
(container.querySelector('.retry') as HTMLButtonElement).click();
|
|
208
|
+
flushSync();
|
|
209
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
210
|
+
flushSync();
|
|
211
|
+
|
|
212
|
+
expect(container.querySelector('.pending')?.textContent).toBe('loading...');
|
|
213
|
+
expect(container.querySelector('.caught')).toBeNull();
|
|
214
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
215
|
+
|
|
216
|
+
(requests.get(1) as { resolve: (value: string) => void }).resolve('recovered');
|
|
217
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
218
|
+
flushSync();
|
|
219
|
+
|
|
220
|
+
expect(container.querySelector('.resolved')?.textContent).toBe('recovered');
|
|
221
|
+
expect(container.querySelector('.pending')).toBeNull();
|
|
222
|
+
expect(container.querySelector('.caught')).toBeNull();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('try block', () => {
|
|
227
|
+
it(
|
|
228
|
+
'does not compile ref binds as async callbacks inside try/pending trackAsync branches',
|
|
229
|
+
async () => {
|
|
230
|
+
component App() {
|
|
231
|
+
let &[value, valueTracked] = track(1);
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
let &[loaded] = trackAsync(() => Promise.resolve(value + 1));
|
|
235
|
+
<input type="number" {ref bindValue(valueTracked)} />
|
|
236
|
+
<span>{loaded}</span>
|
|
237
|
+
} pending {
|
|
238
|
+
<p>{'loading...'}</p>
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
render(App);
|
|
243
|
+
|
|
244
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
245
|
+
flushSync();
|
|
246
|
+
|
|
247
|
+
const input = container.querySelector('input') as HTMLInputElement | null;
|
|
248
|
+
expect(input?.value).toBe('1');
|
|
249
|
+
expect(container.innerHTML).toContain('<span>2</span>');
|
|
250
|
+
expect(container.innerHTML).not.toContain('loading...');
|
|
251
|
+
},
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
it('does not crash when trackAsync is used to render a list inside try/pending', async () => {
|
|
255
|
+
component App() {
|
|
256
|
+
try {
|
|
257
|
+
<AsyncChild />
|
|
258
|
+
} pending {
|
|
259
|
+
<p>{'loading...'}</p>
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
component AsyncChild() {
|
|
264
|
+
let &[data] = trackAsync(() => Promise.resolve(['a', 'b', 'c']));
|
|
265
|
+
|
|
266
|
+
<ul>
|
|
267
|
+
for (let item of data) {
|
|
268
|
+
<li>{item}</li>
|
|
269
|
+
}
|
|
270
|
+
</ul>
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
render(App);
|
|
274
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
275
|
+
flushSync();
|
|
276
|
+
|
|
277
|
+
const items = container.querySelectorAll('li');
|
|
278
|
+
expect(items.length).toBe(3);
|
|
279
|
+
expect(items[0].textContent).toBe('a');
|
|
280
|
+
expect(items[1].textContent).toBe('b');
|
|
281
|
+
expect(items[2].textContent).toBe('c');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it(
|
|
285
|
+
'does not crash when async component with tracked state is used inside try/pending',
|
|
286
|
+
async () => {
|
|
287
|
+
component App() {
|
|
288
|
+
let &[query, queryTracked] = track('');
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
<FilteredList {queryTracked} />
|
|
292
|
+
} pending {
|
|
293
|
+
<p>{'loading...'}</p>
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
component FilteredList({ queryTracked }: { queryTracked: Tracked<string> }) {
|
|
298
|
+
let &[items] = trackAsync(() => Promise.resolve(['apple', 'banana', 'cherry']));
|
|
299
|
+
let &[filtered] = track(
|
|
300
|
+
() => items.filter((item: string) => item.includes(queryTracked.value)),
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
<ul>
|
|
304
|
+
for (let item of filtered) {
|
|
305
|
+
<li>{item}</li>
|
|
306
|
+
}
|
|
307
|
+
</ul>
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
render(App);
|
|
311
|
+
|
|
312
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
313
|
+
flushSync();
|
|
314
|
+
|
|
315
|
+
const listItems = container.querySelectorAll('li');
|
|
316
|
+
expect(listItems.length).toBe(3);
|
|
317
|
+
},
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
it('if test condition stays synchronous once trackAsync has resolved', async () => {
|
|
321
|
+
component App() {
|
|
322
|
+
try {
|
|
323
|
+
let &[items] = trackAsync(() => Promise.resolve(['apple', 'banana', 'cherry']));
|
|
324
|
+
|
|
325
|
+
if (items.includes('not-in-list')) {
|
|
326
|
+
<p>{'not-in-list is in the list!'}</p>
|
|
327
|
+
} else {
|
|
328
|
+
<p>{'not-in-list is not in the list.'}</p>
|
|
329
|
+
}
|
|
330
|
+
} pending {
|
|
331
|
+
<p>{'loading...'}</p>
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
render(App);
|
|
336
|
+
|
|
337
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
338
|
+
flushSync();
|
|
339
|
+
|
|
340
|
+
expect(container.innerHTML).toContain('not-in-list is not in the list.');
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('destroying try block while in pending state cleans up pending branch', async () => {
|
|
344
|
+
let pending_effect_teardown_count = 0;
|
|
345
|
+
|
|
346
|
+
component PendingChild() {
|
|
347
|
+
effect(() => {
|
|
348
|
+
return () => {
|
|
349
|
+
pending_effect_teardown_count++;
|
|
350
|
+
};
|
|
351
|
+
});
|
|
352
|
+
<p class="pending">{'loading...'}</p>
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
component App() {
|
|
356
|
+
let &[show] = track(true);
|
|
357
|
+
|
|
358
|
+
if (show) {
|
|
359
|
+
try {
|
|
360
|
+
let &[data] = trackAsync(() => new Promise(() => {}));
|
|
361
|
+
<p class="resolved">{data}</p>
|
|
362
|
+
} pending {
|
|
363
|
+
<PendingChild />
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
<button class="toggle" onClick={() => (show = !show)}>{'toggle'}</button>
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
render(App);
|
|
371
|
+
|
|
372
|
+
// Wait for pending microtask to fire
|
|
373
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
374
|
+
flushSync();
|
|
375
|
+
|
|
376
|
+
expect(container.querySelector('.pending')?.textContent).toBe('loading...');
|
|
377
|
+
expect(pending_effect_teardown_count).toBe(0);
|
|
378
|
+
|
|
379
|
+
// Toggle if condition to false — should destroy try block and pending branch
|
|
380
|
+
(container.querySelector('.toggle') as HTMLButtonElement).click();
|
|
381
|
+
flushSync();
|
|
382
|
+
|
|
383
|
+
expect(container.querySelector('.pending')).toBeNull();
|
|
384
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
385
|
+
// Effect teardown in the pending branch should have run
|
|
386
|
+
expect(pending_effect_teardown_count).toBe(1);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('destroying try block while in resolved state cleans up resolved branch', async () => {
|
|
390
|
+
let resolved_effect_teardown_count = 0;
|
|
391
|
+
|
|
392
|
+
component ResolvedChild(&{ data }: { data: string }) {
|
|
393
|
+
effect(() => {
|
|
394
|
+
return () => {
|
|
395
|
+
resolved_effect_teardown_count++;
|
|
396
|
+
};
|
|
397
|
+
});
|
|
398
|
+
<p class="resolved">{data}</p>
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
component App() {
|
|
402
|
+
let &[show] = track(true);
|
|
403
|
+
|
|
404
|
+
if (show) {
|
|
405
|
+
try {
|
|
406
|
+
let &[data] = trackAsync(() => Promise.resolve('hello'));
|
|
407
|
+
<ResolvedChild {data} />
|
|
408
|
+
} pending {
|
|
409
|
+
<p class="pending">{'loading...'}</p>
|
|
410
|
+
} catch (err) {
|
|
411
|
+
<p class="caught">{(err as Error).message}</p>
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
<button class="toggle" onClick={() => (show = !show)}>{'toggle'}</button>
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
render(App);
|
|
419
|
+
|
|
420
|
+
// Wait for async to resolve
|
|
421
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
422
|
+
flushSync();
|
|
423
|
+
|
|
424
|
+
expect(container.querySelector('.resolved')?.textContent).toBe('hello');
|
|
425
|
+
expect(resolved_effect_teardown_count).toBe(0);
|
|
426
|
+
|
|
427
|
+
// Toggle if condition to false — should destroy try block and resolved branch
|
|
428
|
+
(container.querySelector('.toggle') as HTMLButtonElement).click();
|
|
429
|
+
flushSync();
|
|
430
|
+
|
|
431
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
432
|
+
expect(container.querySelector('.pending')).toBeNull();
|
|
433
|
+
expect(container.querySelector('.caught')).toBeNull();
|
|
434
|
+
// Effect teardown in the resolved branch should have run
|
|
435
|
+
expect(resolved_effect_teardown_count).toBe(1);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('destroying try block while in catch state cleans up catch branch', async () => {
|
|
439
|
+
let catch_effect_teardown_count = 0;
|
|
440
|
+
|
|
441
|
+
component CatchChild({ error }: { error: Error }) {
|
|
442
|
+
effect(() => {
|
|
443
|
+
return () => {
|
|
444
|
+
catch_effect_teardown_count++;
|
|
445
|
+
};
|
|
446
|
+
});
|
|
447
|
+
<p class="caught">{error.message}</p>
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
component App() {
|
|
451
|
+
let &[show] = track(true);
|
|
452
|
+
|
|
453
|
+
if (show) {
|
|
454
|
+
try {
|
|
455
|
+
let &[data] = trackAsync(() => Promise.reject(new Error('fail')));
|
|
456
|
+
<p class="resolved">{data}</p>
|
|
457
|
+
} pending {
|
|
458
|
+
<p class="pending">{'loading...'}</p>
|
|
459
|
+
} catch (err) {
|
|
460
|
+
<CatchChild error={err as Error} />
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
<button class="toggle" onClick={() => (show = !show)}>{'toggle'}</button>
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
render(App);
|
|
468
|
+
|
|
469
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
470
|
+
flushSync();
|
|
471
|
+
|
|
472
|
+
expect(container.querySelector('.caught')?.textContent).toBe('fail');
|
|
473
|
+
expect(catch_effect_teardown_count).toBe(0);
|
|
474
|
+
|
|
475
|
+
// Toggle if condition to false — should destroy try block and catch branch
|
|
476
|
+
(container.querySelector('.toggle') as HTMLButtonElement).click();
|
|
477
|
+
flushSync();
|
|
478
|
+
|
|
479
|
+
expect(container.querySelector('.caught')).toBeNull();
|
|
480
|
+
// Effect teardown in the catch branch should have run
|
|
481
|
+
expect(catch_effect_teardown_count).toBe(1);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('pending block throwing renders catch block from the same try block', async () => {
|
|
485
|
+
component ThrowingPending() {
|
|
486
|
+
throw new Error('pending exploded');
|
|
487
|
+
<p>{'should not render'}</p>
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
component App() {
|
|
491
|
+
try {
|
|
492
|
+
let &[data] = trackAsync(() => new Promise(() => {}));
|
|
493
|
+
<p class="resolved">{data}</p>
|
|
494
|
+
} pending {
|
|
495
|
+
<ThrowingPending />
|
|
496
|
+
} catch (err) {
|
|
497
|
+
<p class="caught">{(err as Error).message}</p>
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
render(App);
|
|
502
|
+
|
|
503
|
+
// Wait for pending microtask to fire and render pending block
|
|
504
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
505
|
+
flushSync();
|
|
506
|
+
|
|
507
|
+
// Pending block threw, so catch should be rendered
|
|
508
|
+
expect(container.querySelector('.caught')?.textContent).toBe('pending exploded');
|
|
509
|
+
expect(container.querySelector('.pending')).toBeNull();
|
|
510
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it('pending block throwing without catch bubbles to parent try/catch', async () => {
|
|
514
|
+
component ThrowingPending() {
|
|
515
|
+
throw new Error('pending exploded');
|
|
516
|
+
<p>{'should not render'}</p>
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
component App() {
|
|
520
|
+
try {
|
|
521
|
+
<Inner />
|
|
522
|
+
} catch (err) {
|
|
523
|
+
<p class="parent-caught">{(err as Error).message}</p>
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
component Inner() {
|
|
528
|
+
try {
|
|
529
|
+
let &[data] = trackAsync(() => new Promise(() => {}));
|
|
530
|
+
<p class="resolved">{data}</p>
|
|
531
|
+
} pending {
|
|
532
|
+
<ThrowingPending />
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
render(App);
|
|
537
|
+
|
|
538
|
+
// Wait for pending microtask to fire and render pending block
|
|
539
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
540
|
+
flushSync();
|
|
541
|
+
|
|
542
|
+
// Inner try has no catch, error should bubble to parent's catch
|
|
543
|
+
expect(container.querySelector('.parent-caught')?.textContent).toBe('pending exploded');
|
|
544
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it('chained trackAsync keeps pending block until all deriveds resolve', async () => {
|
|
548
|
+
let first_resolve: (value: string) => void;
|
|
549
|
+
let second_resolve: (value: number) => void;
|
|
550
|
+
|
|
551
|
+
const first_promise = new Promise<string>((resolve) => {
|
|
552
|
+
first_resolve = resolve;
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
component App() {
|
|
556
|
+
try {
|
|
557
|
+
let &[name] = trackAsync(() => first_promise);
|
|
558
|
+
let &[length] = trackAsync(() => {
|
|
559
|
+
// Read name synchronously — throws ASYNC_DERIVED_READ_THROWN
|
|
560
|
+
// while name is still pending, triggering the deferred mechanism.
|
|
561
|
+
const n = name;
|
|
562
|
+
return new Promise<number>((resolve) => {
|
|
563
|
+
second_resolve = resolve;
|
|
564
|
+
}).then((val) => n.length + val);
|
|
565
|
+
});
|
|
566
|
+
<p class="resolved">
|
|
567
|
+
{name}
|
|
568
|
+
{' has length '}
|
|
569
|
+
{length}
|
|
570
|
+
</p>
|
|
571
|
+
} pending {
|
|
572
|
+
<p class="pending">{'loading...'}</p>
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
render(App);
|
|
577
|
+
|
|
578
|
+
// Wait for pending microtask
|
|
579
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
580
|
+
flushSync();
|
|
581
|
+
|
|
582
|
+
// Both are pending — pending block should show
|
|
583
|
+
expect(container.querySelector('.pending')?.textContent).toBe('loading...');
|
|
584
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
585
|
+
|
|
586
|
+
// Resolve the first trackAsync
|
|
587
|
+
first_resolve!('hello');
|
|
588
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
589
|
+
flushSync();
|
|
590
|
+
|
|
591
|
+
// First resolved, but second still pending — pending block must stay
|
|
592
|
+
expect(container.querySelector('.pending')?.textContent).toBe('loading...');
|
|
593
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
594
|
+
|
|
595
|
+
// Resolve the second trackAsync
|
|
596
|
+
second_resolve!(100);
|
|
597
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
598
|
+
flushSync();
|
|
599
|
+
|
|
600
|
+
// Both resolved — pending removed, resolved content shown with both values
|
|
601
|
+
expect(container.querySelector('.pending')).toBeNull();
|
|
602
|
+
expect(container.querySelector('.resolved')?.textContent).toBe('hello has length 105');
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it(
|
|
606
|
+
'chained trackAsync does not recreate pending block when intermediate derived resolves',
|
|
607
|
+
async () => {
|
|
608
|
+
let first_resolve: (value: string) => void;
|
|
609
|
+
let second_resolve: (value: number) => void;
|
|
610
|
+
let pending_render_count = 0;
|
|
611
|
+
|
|
612
|
+
const first_promise = new Promise<string>((resolve) => {
|
|
613
|
+
first_resolve = resolve;
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
component PendingTracker() {
|
|
617
|
+
pending_render_count++;
|
|
618
|
+
<p class="pending">{'loading...'}</p>
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
component App() {
|
|
622
|
+
try {
|
|
623
|
+
let &[name] = trackAsync(() => first_promise);
|
|
624
|
+
let &[length] = trackAsync(() => {
|
|
625
|
+
const n = name;
|
|
626
|
+
return new Promise<number>((resolve) => {
|
|
627
|
+
second_resolve = resolve;
|
|
628
|
+
}).then((val) => n.length + val);
|
|
629
|
+
});
|
|
630
|
+
<p class="resolved">
|
|
631
|
+
{name}
|
|
632
|
+
{' has length '}
|
|
633
|
+
{length}
|
|
634
|
+
</p>
|
|
635
|
+
} pending {
|
|
636
|
+
<PendingTracker />
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
render(App);
|
|
641
|
+
|
|
642
|
+
// Wait for pending microtask to fire and render pending block
|
|
643
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
644
|
+
flushSync();
|
|
645
|
+
|
|
646
|
+
expect(container.querySelector('.pending')?.textContent).toBe('loading...');
|
|
647
|
+
expect(pending_render_count).toBe(1);
|
|
648
|
+
|
|
649
|
+
// Resolve the first — second starts its own async request
|
|
650
|
+
// Pending block should NOT be torn down and recreated
|
|
651
|
+
first_resolve!('hello');
|
|
652
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
653
|
+
flushSync();
|
|
654
|
+
|
|
655
|
+
expect(container.querySelector('.pending')?.textContent).toBe('loading...');
|
|
656
|
+
expect(pending_render_count).toBe(1); // Still 1 — no flicker
|
|
657
|
+
|
|
658
|
+
// Resolve the second — pending removed, resolved shown
|
|
659
|
+
second_resolve!(100);
|
|
660
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
661
|
+
flushSync();
|
|
662
|
+
|
|
663
|
+
expect(container.querySelector('.pending')).toBeNull();
|
|
664
|
+
expect(container.querySelector('.resolved')?.textContent).toBe('hello has length 105');
|
|
665
|
+
expect(pending_render_count).toBe(1); // Never recreated
|
|
666
|
+
},
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
it(
|
|
670
|
+
'chained trackAsync: first rejects, catch renders and stays after second resolves',
|
|
671
|
+
async () => {
|
|
672
|
+
let first_reject: (error: Error) => void;
|
|
673
|
+
let second_resolve: (value: number) => void;
|
|
674
|
+
|
|
675
|
+
const first_promise = new Promise<string>((_, reject) => {
|
|
676
|
+
first_reject = reject;
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
component App() {
|
|
680
|
+
try {
|
|
681
|
+
let &[name] = trackAsync(() => first_promise);
|
|
682
|
+
let &[length] = trackAsync(() => {
|
|
683
|
+
const n = name;
|
|
684
|
+
return new Promise<number>((resolve) => {
|
|
685
|
+
second_resolve = resolve;
|
|
686
|
+
}).then((val) => n.length + val);
|
|
687
|
+
});
|
|
688
|
+
<p class="resolved">
|
|
689
|
+
{name}
|
|
690
|
+
{' has length '}
|
|
691
|
+
{length}
|
|
692
|
+
</p>
|
|
693
|
+
} pending {
|
|
694
|
+
<p class="pending">{'loading...'}</p>
|
|
695
|
+
} catch (err) {
|
|
696
|
+
<p class="caught">{(err as Error).message}</p>
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
render(App);
|
|
701
|
+
|
|
702
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
703
|
+
flushSync();
|
|
704
|
+
|
|
705
|
+
expect(container.querySelector('.pending')?.textContent).toBe('loading...');
|
|
706
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
707
|
+
expect(container.querySelector('.caught')).toBeNull();
|
|
708
|
+
|
|
709
|
+
// Reject the first trackAsync
|
|
710
|
+
first_reject!(new Error('first failed'));
|
|
711
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
712
|
+
flushSync();
|
|
713
|
+
|
|
714
|
+
// Catch block should render
|
|
715
|
+
expect(container.querySelector('.caught')?.textContent).toBe('first failed');
|
|
716
|
+
expect(container.querySelector('.pending')).toBeNull();
|
|
717
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
718
|
+
|
|
719
|
+
// Resolve the second trackAsync — catch block should stay
|
|
720
|
+
expect(second_resolve!).toBeUndefined();
|
|
721
|
+
},
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
it(
|
|
725
|
+
'chained trackAsync: first succeeds, second rejects, pending stays then catch renders',
|
|
726
|
+
async () => {
|
|
727
|
+
let first_resolve: (value: string) => void;
|
|
728
|
+
let second_reject: (error: Error) => void;
|
|
729
|
+
|
|
730
|
+
const first_promise = new Promise<string>((resolve) => {
|
|
731
|
+
first_resolve = resolve;
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
component App() {
|
|
735
|
+
try {
|
|
736
|
+
let &[name] = trackAsync(() => first_promise);
|
|
737
|
+
let &[length] = trackAsync(() => {
|
|
738
|
+
const n = name;
|
|
739
|
+
return new Promise<number>((_, reject) => {
|
|
740
|
+
second_reject = reject;
|
|
741
|
+
});
|
|
742
|
+
});
|
|
743
|
+
<p class="resolved">
|
|
744
|
+
{name}
|
|
745
|
+
{' has length '}
|
|
746
|
+
{length}
|
|
747
|
+
</p>
|
|
748
|
+
} pending {
|
|
749
|
+
<p class="pending">{'loading...'}</p>
|
|
750
|
+
} catch (err) {
|
|
751
|
+
<p class="caught">{(err as Error).message}</p>
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
render(App);
|
|
756
|
+
|
|
757
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
758
|
+
flushSync();
|
|
759
|
+
|
|
760
|
+
// Both pending — pending block shows
|
|
761
|
+
expect(container.querySelector('.pending')?.textContent).toBe('loading...');
|
|
762
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
763
|
+
expect(container.querySelector('.caught')).toBeNull();
|
|
764
|
+
|
|
765
|
+
// Resolve the first — second still pending, pending block stays
|
|
766
|
+
first_resolve!('hello');
|
|
767
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
768
|
+
flushSync();
|
|
769
|
+
|
|
770
|
+
expect(container.querySelector('.pending')?.textContent).toBe('loading...');
|
|
771
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
772
|
+
expect(container.querySelector('.caught')).toBeNull();
|
|
773
|
+
|
|
774
|
+
// Reject the second — switch to catch
|
|
775
|
+
second_reject!(new Error('second failed'));
|
|
776
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
777
|
+
flushSync();
|
|
778
|
+
|
|
779
|
+
expect(container.querySelector('.caught')?.textContent).toBe('second failed');
|
|
780
|
+
expect(container.querySelector('.pending')).toBeNull();
|
|
781
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
782
|
+
},
|
|
783
|
+
);
|
|
784
|
+
|
|
785
|
+
it('resolved try block without catch bubbles error to parent try/catch', async () => {
|
|
786
|
+
component ThrowingChild() {
|
|
787
|
+
let &[data] = trackAsync(() => Promise.reject(new Error('async failed')));
|
|
788
|
+
<p class="resolved">{data}</p>
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
component App() {
|
|
792
|
+
try {
|
|
793
|
+
<Inner />
|
|
794
|
+
} catch (err) {
|
|
795
|
+
<p class="parent-caught">{(err as Error).message}</p>
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
component Inner() {
|
|
800
|
+
try {
|
|
801
|
+
<ThrowingChild />
|
|
802
|
+
} pending {
|
|
803
|
+
<p class="pending">{'loading...'}</p>
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
render(App);
|
|
808
|
+
|
|
809
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
810
|
+
flushSync();
|
|
811
|
+
|
|
812
|
+
// Inner try has no catch, rejection should bubble to parent's catch
|
|
813
|
+
expect(container.querySelector('.parent-caught')?.textContent).toBe('async failed');
|
|
814
|
+
expect(container.querySelector('.pending')).toBeNull();
|
|
815
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
it(
|
|
819
|
+
'rejection sets derived value to SUSPENSE_REJECTED and promise is properly handled',
|
|
820
|
+
async () => {
|
|
821
|
+
let reject_fn: (error: Error) => void;
|
|
822
|
+
let promise_settled: { type: 'resolved' | 'rejected'; value: any } | null = null;
|
|
823
|
+
|
|
824
|
+
const user_promise = new Promise<string>((_, reject) => {
|
|
825
|
+
reject_fn = reject;
|
|
826
|
+
});
|
|
827
|
+
user_promise.then(
|
|
828
|
+
(v) => {
|
|
829
|
+
promise_settled = { type: 'resolved', value: v };
|
|
830
|
+
},
|
|
831
|
+
(e) => {
|
|
832
|
+
promise_settled = { type: 'rejected', value: e };
|
|
833
|
+
},
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
let data: any;
|
|
837
|
+
|
|
838
|
+
component App() {
|
|
839
|
+
try {
|
|
840
|
+
data = trackAsync(() => user_promise);
|
|
841
|
+
<p class="resolved">{data.value}</p>
|
|
842
|
+
} pending {
|
|
843
|
+
<p class="pending">{'loading...'}</p>
|
|
844
|
+
} catch (err) {
|
|
845
|
+
<p class="caught">{(err as Error).message}</p>
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
render(App);
|
|
850
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
851
|
+
flushSync();
|
|
852
|
+
|
|
853
|
+
expect(container.querySelector('.pending')?.textContent).toBe('loading...');
|
|
854
|
+
|
|
855
|
+
reject_fn!(new Error('test rejection'));
|
|
856
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
857
|
+
flushSync();
|
|
858
|
+
|
|
859
|
+
expect(container.querySelector('.caught')?.textContent).toBe('test rejection');
|
|
860
|
+
expect(container.querySelector('.pending')).toBeNull();
|
|
861
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
862
|
+
|
|
863
|
+
// The user promise was rejected and handled by the runtime
|
|
864
|
+
expect(promise_settled).toEqual({ type: 'rejected', value: expect.any(Error) });
|
|
865
|
+
expect((promise_settled as any).value.message).toBe('test rejection');
|
|
866
|
+
expect(peek(data)).toEqual(SUSPENSE_REJECTED);
|
|
867
|
+
},
|
|
868
|
+
);
|
|
869
|
+
|
|
870
|
+
it(
|
|
871
|
+
'chained trackAsync: first rejects, dependent deferred is rejected and cleaned up',
|
|
872
|
+
async () => {
|
|
873
|
+
let first_reject: (error: Error) => void;
|
|
874
|
+
let first_settled: { type: 'resolved' | 'rejected'; value: any } | null = null;
|
|
875
|
+
|
|
876
|
+
const first_promise = new Promise<string>((_, reject) => {
|
|
877
|
+
first_reject = reject;
|
|
878
|
+
});
|
|
879
|
+
first_promise.then(
|
|
880
|
+
(v) => {
|
|
881
|
+
first_settled = { type: 'resolved', value: v };
|
|
882
|
+
},
|
|
883
|
+
(e) => {
|
|
884
|
+
first_settled = { type: 'rejected', value: e };
|
|
885
|
+
},
|
|
886
|
+
);
|
|
887
|
+
|
|
888
|
+
let name: any;
|
|
889
|
+
let length: any;
|
|
890
|
+
|
|
891
|
+
component App() {
|
|
892
|
+
try {
|
|
893
|
+
name = trackAsync(() => first_promise);
|
|
894
|
+
length = trackAsync(() => {
|
|
895
|
+
const n = name.value;
|
|
896
|
+
return new Promise<number>((resolve) => {
|
|
897
|
+
resolve(n.length);
|
|
898
|
+
});
|
|
899
|
+
});
|
|
900
|
+
<p class="resolved">
|
|
901
|
+
{name.value}
|
|
902
|
+
{' has length '}
|
|
903
|
+
{length.value}
|
|
904
|
+
</p>
|
|
905
|
+
} pending {
|
|
906
|
+
<p class="pending">{'loading...'}</p>
|
|
907
|
+
} catch (err) {
|
|
908
|
+
<p class="caught">{(err as Error).message}</p>
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
render(App);
|
|
913
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
914
|
+
flushSync();
|
|
915
|
+
|
|
916
|
+
expect(container.querySelector('.pending')?.textContent).toBe('loading...');
|
|
917
|
+
|
|
918
|
+
// Reject the first — should route to catch and clean up second's deferred
|
|
919
|
+
first_reject!(new Error('first failed'));
|
|
920
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
921
|
+
flushSync();
|
|
922
|
+
|
|
923
|
+
expect(container.querySelector('.caught')?.textContent).toBe('first failed');
|
|
924
|
+
expect(container.querySelector('.pending')).toBeNull();
|
|
925
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
926
|
+
expect(peek(name)).toEqual(SUSPENSE_REJECTED);
|
|
927
|
+
|
|
928
|
+
// First promise was rejected and handled
|
|
929
|
+
expect(first_settled).toEqual({ type: 'rejected', value: expect.any(Error) });
|
|
930
|
+
|
|
931
|
+
// Verify stability — catch stays, no further state changes
|
|
932
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
933
|
+
flushSync();
|
|
934
|
+
expect(container.querySelector('.caught')?.textContent).toBe('first failed');
|
|
935
|
+
// length never has a chance to run because name access throws and exits the block
|
|
936
|
+
expect(peek(length)).toEqual(SUSPENSE_REJECTED);
|
|
937
|
+
},
|
|
938
|
+
);
|
|
939
|
+
|
|
940
|
+
it('deep chained trackAsync: A rejects, B and C deferreds are cleaned up', async () => {
|
|
941
|
+
let a_reject: (error: Error) => void;
|
|
942
|
+
let a_settled: { type: 'resolved' | 'rejected'; value: any } | null = null;
|
|
943
|
+
|
|
944
|
+
const a_promise = new Promise<string>((_, reject) => {
|
|
945
|
+
a_reject = reject;
|
|
946
|
+
});
|
|
947
|
+
a_promise.then(
|
|
948
|
+
(v) => {
|
|
949
|
+
a_settled = { type: 'resolved', value: v };
|
|
950
|
+
},
|
|
951
|
+
(e) => {
|
|
952
|
+
a_settled = { type: 'rejected', value: e };
|
|
953
|
+
},
|
|
954
|
+
);
|
|
955
|
+
|
|
956
|
+
let a: any;
|
|
957
|
+
let b: any;
|
|
958
|
+
let c: any;
|
|
959
|
+
component App() {
|
|
960
|
+
try {
|
|
961
|
+
a = trackAsync(() => a_promise);
|
|
962
|
+
b = trackAsync(() => {
|
|
963
|
+
const val = a.value;
|
|
964
|
+
return Promise.resolve(val.toUpperCase());
|
|
965
|
+
});
|
|
966
|
+
c = trackAsync(() => {
|
|
967
|
+
const val = b.value;
|
|
968
|
+
return Promise.resolve(val.length);
|
|
969
|
+
});
|
|
970
|
+
<p class="resolved">
|
|
971
|
+
{a.value}
|
|
972
|
+
{' → '}
|
|
973
|
+
{b.value}
|
|
974
|
+
{' → '}
|
|
975
|
+
{c.value}
|
|
976
|
+
</p>
|
|
977
|
+
} pending {
|
|
978
|
+
<p class="pending">{'loading...'}</p>
|
|
979
|
+
} catch (err) {
|
|
980
|
+
<p class="caught">{(err as Error).message}</p>
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
render(App);
|
|
985
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
986
|
+
flushSync();
|
|
987
|
+
|
|
988
|
+
expect(container.querySelector('.pending')?.textContent).toBe('loading...');
|
|
989
|
+
|
|
990
|
+
// Reject A — should cascade to catch, B and C cleaned up
|
|
991
|
+
a_reject!(new Error('chain failed'));
|
|
992
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
993
|
+
flushSync();
|
|
994
|
+
|
|
995
|
+
expect(container.querySelector('.caught')?.textContent).toBe('chain failed');
|
|
996
|
+
expect(container.querySelector('.pending')).toBeNull();
|
|
997
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
998
|
+
|
|
999
|
+
// A promise was rejected and handled
|
|
1000
|
+
expect(a_settled).toEqual({ type: 'rejected', value: expect.any(Error) });
|
|
1001
|
+
expect(peek(a)).toEqual(SUSPENSE_REJECTED);
|
|
1002
|
+
expect(peek(b)).toEqual(SUSPENSE_REJECTED);
|
|
1003
|
+
expect(peek(c)).toEqual(SUSPENSE_REJECTED);
|
|
1004
|
+
|
|
1005
|
+
// Verify stability — no further state changes from B/C deferreds settling
|
|
1006
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1007
|
+
flushSync();
|
|
1008
|
+
expect(container.querySelector('.caught')?.textContent).toBe('chain failed');
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
it('multiple independent trackAsyncs, one rejects, catch shows first error', async () => {
|
|
1012
|
+
let first_reject: (error: Error) => void;
|
|
1013
|
+
let second_resolve: (value: number) => void;
|
|
1014
|
+
let first_settled: { type: 'resolved' | 'rejected'; value: any } | null = null;
|
|
1015
|
+
let second_settled: { type: 'resolved' | 'rejected'; value: any } | null = null;
|
|
1016
|
+
|
|
1017
|
+
const first_promise = new Promise<string>((_, reject) => {
|
|
1018
|
+
first_reject = reject;
|
|
1019
|
+
});
|
|
1020
|
+
first_promise.then(
|
|
1021
|
+
(v) => {
|
|
1022
|
+
first_settled = { type: 'resolved', value: v };
|
|
1023
|
+
},
|
|
1024
|
+
(e) => {
|
|
1025
|
+
first_settled = { type: 'rejected', value: e };
|
|
1026
|
+
},
|
|
1027
|
+
);
|
|
1028
|
+
|
|
1029
|
+
const second_promise = new Promise<number>((resolve) => {
|
|
1030
|
+
second_resolve = resolve;
|
|
1031
|
+
});
|
|
1032
|
+
second_promise.then(
|
|
1033
|
+
(v) => {
|
|
1034
|
+
second_settled = { type: 'resolved', value: v };
|
|
1035
|
+
},
|
|
1036
|
+
(e) => {
|
|
1037
|
+
second_settled = { type: 'rejected', value: e };
|
|
1038
|
+
},
|
|
1039
|
+
);
|
|
1040
|
+
|
|
1041
|
+
let name: any;
|
|
1042
|
+
let count: any;
|
|
1043
|
+
component App() {
|
|
1044
|
+
try {
|
|
1045
|
+
name = trackAsync(() => first_promise);
|
|
1046
|
+
count = trackAsync(() => second_promise);
|
|
1047
|
+
<p class="resolved">
|
|
1048
|
+
{name.value}
|
|
1049
|
+
{' count: '}
|
|
1050
|
+
{count.value}
|
|
1051
|
+
</p>
|
|
1052
|
+
} pending {
|
|
1053
|
+
<p class="pending">{'loading...'}</p>
|
|
1054
|
+
} catch (err) {
|
|
1055
|
+
<p class="caught">{(err as Error).message}</p>
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
render(App);
|
|
1060
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1061
|
+
flushSync();
|
|
1062
|
+
|
|
1063
|
+
expect(container.querySelector('.pending')?.textContent).toBe('loading...');
|
|
1064
|
+
expect(peek(name)).toEqual(SUSPENSE_PENDING);
|
|
1065
|
+
expect(peek(count)).toEqual(SUSPENSE_PENDING);
|
|
1066
|
+
|
|
1067
|
+
// Reject the first — catch should render
|
|
1068
|
+
first_reject!(new Error('name failed'));
|
|
1069
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1070
|
+
flushSync();
|
|
1071
|
+
|
|
1072
|
+
expect(container.querySelector('.caught')?.textContent).toBe('name failed');
|
|
1073
|
+
expect(container.querySelector('.pending')).toBeNull();
|
|
1074
|
+
|
|
1075
|
+
// First promise was rejected
|
|
1076
|
+
expect(first_settled).toEqual({ type: 'rejected', value: expect.any(Error) });
|
|
1077
|
+
expect(peek(name)).toEqual(SUSPENSE_REJECTED);
|
|
1078
|
+
expect(peek(count)).toEqual(SUSPENSE_PENDING);
|
|
1079
|
+
|
|
1080
|
+
// Resolve the second — catch should stay stable
|
|
1081
|
+
second_resolve!(42);
|
|
1082
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1083
|
+
flushSync();
|
|
1084
|
+
|
|
1085
|
+
expect(container.querySelector('.caught')?.textContent).toBe('name failed');
|
|
1086
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
1087
|
+
|
|
1088
|
+
// Second promise was resolved (by the test), but the boundary already caught
|
|
1089
|
+
expect(second_settled).toEqual({ type: 'resolved', value: 42 });
|
|
1090
|
+
expect(peek(name)).toEqual(SUSPENSE_REJECTED);
|
|
1091
|
+
expect(peek(count)).toEqual(42);
|
|
1092
|
+
});
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
describe('sync error while async deriveds are pending', () => {
|
|
1096
|
+
it(
|
|
1097
|
+
'catch stays stable when sync error fires while promise is pending, then promise resolves',
|
|
1098
|
+
async () => {
|
|
1099
|
+
let resolve_fn: ((v: string) => void) | undefined;
|
|
1100
|
+
let user_promise = new Promise<string>((resolve) => {
|
|
1101
|
+
resolve_fn = resolve;
|
|
1102
|
+
});
|
|
1103
|
+
let promise_settled: { type: string; value: any } | null = null;
|
|
1104
|
+
user_promise.then(
|
|
1105
|
+
(v) => (promise_settled = { type: 'resolved', value: v }),
|
|
1106
|
+
(e) => (promise_settled = { type: 'rejected', value: e }),
|
|
1107
|
+
);
|
|
1108
|
+
|
|
1109
|
+
component App() {
|
|
1110
|
+
try {
|
|
1111
|
+
<AsyncChild />
|
|
1112
|
+
<ThrowingChild />
|
|
1113
|
+
} pending {
|
|
1114
|
+
<p class="pending">{'loading...'}</p>
|
|
1115
|
+
} catch (err: Error) {
|
|
1116
|
+
<p class="caught">{err.message}</p>
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
let data: any;
|
|
1121
|
+
component AsyncChild() {
|
|
1122
|
+
data = trackAsync(() => user_promise);
|
|
1123
|
+
<p class="async-resolved">{data.value}</p>
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
component ThrowingChild() {
|
|
1127
|
+
throw new Error('sync boom');
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
render(App);
|
|
1131
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
1132
|
+
flushSync();
|
|
1133
|
+
|
|
1134
|
+
// The sync error from ThrowingChild should trigger catch
|
|
1135
|
+
expect(container.querySelector('.caught')?.textContent).toBe('sync boom');
|
|
1136
|
+
expect(container.querySelector('.pending')).toBeNull();
|
|
1137
|
+
expect(container.querySelector('.async-resolved')).toBeNull();
|
|
1138
|
+
|
|
1139
|
+
expect(peek(data)).toEqual(SUSPENSE_PENDING);
|
|
1140
|
+
|
|
1141
|
+
// Now resolve the promise that was in-flight
|
|
1142
|
+
resolve_fn!('hello');
|
|
1143
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
1144
|
+
flushSync();
|
|
1145
|
+
|
|
1146
|
+
// Catch should stay stable — boundary should NOT switch to resolved
|
|
1147
|
+
expect(container.querySelector('.caught')?.textContent).toBe('sync boom');
|
|
1148
|
+
expect(container.querySelector('.async-resolved')).toBeNull();
|
|
1149
|
+
expect(promise_settled).toEqual({ type: 'resolved', value: 'hello' });
|
|
1150
|
+
|
|
1151
|
+
expect(peek(data)).toEqual('hello');
|
|
1152
|
+
|
|
1153
|
+
// Verify stability after all microtasks drain
|
|
1154
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1155
|
+
flushSync();
|
|
1156
|
+
expect(container.querySelector('.caught')?.textContent).toBe('sync boom');
|
|
1157
|
+
expect(container.querySelector('.async-resolved')).toBeNull();
|
|
1158
|
+
},
|
|
1159
|
+
);
|
|
1160
|
+
|
|
1161
|
+
it(
|
|
1162
|
+
'catch stays stable when sync error fires while promise is pending, then promise rejects',
|
|
1163
|
+
async () => {
|
|
1164
|
+
let reject_fn: ((e: any) => void) | undefined;
|
|
1165
|
+
let user_promise = new Promise<string>((_, reject) => {
|
|
1166
|
+
reject_fn = reject;
|
|
1167
|
+
});
|
|
1168
|
+
let promise_settled: { type: string; value: any } | null = null;
|
|
1169
|
+
user_promise.then(
|
|
1170
|
+
(v) => (promise_settled = { type: 'resolved', value: v }),
|
|
1171
|
+
(e) => (promise_settled = { type: 'rejected', value: e }),
|
|
1172
|
+
);
|
|
1173
|
+
|
|
1174
|
+
component App() {
|
|
1175
|
+
try {
|
|
1176
|
+
<AsyncChild />
|
|
1177
|
+
<ThrowingChild />
|
|
1178
|
+
} pending {
|
|
1179
|
+
<p class="pending">{'loading...'}</p>
|
|
1180
|
+
} catch (err: Error) {
|
|
1181
|
+
<p class="caught">{err.message}</p>
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
let data: any;
|
|
1186
|
+
component AsyncChild() {
|
|
1187
|
+
data = trackAsync(() => user_promise);
|
|
1188
|
+
<p class="async-resolved">{data.value}</p>
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
component ThrowingChild() {
|
|
1192
|
+
throw new Error('sync boom');
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
render(App);
|
|
1196
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
1197
|
+
flushSync();
|
|
1198
|
+
|
|
1199
|
+
// Catch should be showing from sync error
|
|
1200
|
+
expect(container.querySelector('.caught')?.textContent).toBe('sync boom');
|
|
1201
|
+
|
|
1202
|
+
expect(peek(data)).toEqual(SUSPENSE_PENDING);
|
|
1203
|
+
|
|
1204
|
+
// Now reject the in-flight promise
|
|
1205
|
+
reject_fn!(new Error('async failure'));
|
|
1206
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
1207
|
+
flushSync();
|
|
1208
|
+
|
|
1209
|
+
// Catch should stay stable with the original sync error — no double-catch
|
|
1210
|
+
expect(container.querySelector('.caught')?.textContent).toBe('sync boom');
|
|
1211
|
+
expect(promise_settled).toEqual({ type: 'rejected', value: expect.any(Error) });
|
|
1212
|
+
|
|
1213
|
+
expect(peek(data)).toEqual(SUSPENSE_REJECTED);
|
|
1214
|
+
|
|
1215
|
+
// Verify stability after all microtasks drain
|
|
1216
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
1217
|
+
flushSync();
|
|
1218
|
+
expect(container.querySelector('.caught')?.textContent).toBe('sync boom');
|
|
1219
|
+
expect(container.querySelector('.async-resolved')).toBeNull();
|
|
1220
|
+
},
|
|
1221
|
+
);
|
|
1222
|
+
|
|
1223
|
+
it(
|
|
1224
|
+
'catch stays stable when sync error fires with multiple pending trackAsyncs, all later resolve',
|
|
1225
|
+
async () => {
|
|
1226
|
+
let resolve_a: ((v: string) => void) | undefined;
|
|
1227
|
+
let resolve_b: ((v: number) => void) | undefined;
|
|
1228
|
+
let promise_a = new Promise<string>((resolve) => {
|
|
1229
|
+
resolve_a = resolve;
|
|
1230
|
+
});
|
|
1231
|
+
let promise_b = new Promise<number>((resolve) => {
|
|
1232
|
+
resolve_b = resolve;
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
let settled_a: { type: string; value: any } | null = null;
|
|
1236
|
+
let settled_b: { type: string; value: any } | null = null;
|
|
1237
|
+
promise_a.then(
|
|
1238
|
+
(v) => (settled_a = { type: 'resolved', value: v }),
|
|
1239
|
+
(e) => (settled_a = { type: 'rejected', value: e }),
|
|
1240
|
+
);
|
|
1241
|
+
promise_b.then(
|
|
1242
|
+
(v) => (settled_b = { type: 'resolved', value: v }),
|
|
1243
|
+
(e) => (settled_b = { type: 'rejected', value: e }),
|
|
1244
|
+
);
|
|
1245
|
+
|
|
1246
|
+
component App() {
|
|
1247
|
+
try {
|
|
1248
|
+
<ChildA />
|
|
1249
|
+
<ChildB />
|
|
1250
|
+
<ThrowingChild />
|
|
1251
|
+
} pending {
|
|
1252
|
+
<p class="pending">{'loading...'}</p>
|
|
1253
|
+
} catch (err: Error) {
|
|
1254
|
+
<p class="caught">{err.message}</p>
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
let dataA: any;
|
|
1259
|
+
component ChildA() {
|
|
1260
|
+
dataA = trackAsync(() => promise_a);
|
|
1261
|
+
<p class="a">{dataA.value}</p>
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
let dataB: any;
|
|
1265
|
+
component ChildB() {
|
|
1266
|
+
dataB = trackAsync(() => promise_b);
|
|
1267
|
+
<p class="b">{dataB.value}</p>
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
component ThrowingChild() {
|
|
1271
|
+
throw new Error('sync boom');
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
render(App);
|
|
1275
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
1276
|
+
flushSync();
|
|
1277
|
+
|
|
1278
|
+
expect(container.querySelector('.caught')?.textContent).toBe('sync boom');
|
|
1279
|
+
|
|
1280
|
+
expect(peek(dataA)).toEqual(SUSPENSE_PENDING);
|
|
1281
|
+
expect(peek(dataB)).toEqual(SUSPENSE_PENDING);
|
|
1282
|
+
|
|
1283
|
+
// Resolve both promises
|
|
1284
|
+
resolve_a!('hello');
|
|
1285
|
+
resolve_b!(42);
|
|
1286
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
1287
|
+
flushSync();
|
|
1288
|
+
|
|
1289
|
+
// Catch should stay — boundary must NOT switch to resolved
|
|
1290
|
+
expect(container.querySelector('.caught')?.textContent).toBe('sync boom');
|
|
1291
|
+
expect(container.querySelector('.a')).toBeNull();
|
|
1292
|
+
expect(container.querySelector('.b')).toBeNull();
|
|
1293
|
+
expect(settled_a).toEqual({ type: 'resolved', value: 'hello' });
|
|
1294
|
+
expect(settled_b).toEqual({ type: 'resolved', value: 42 });
|
|
1295
|
+
|
|
1296
|
+
expect(peek(dataA)).toEqual('hello');
|
|
1297
|
+
expect(peek(dataB)).toEqual(42);
|
|
1298
|
+
|
|
1299
|
+
// Verify stability after all microtasks drain
|
|
1300
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1301
|
+
flushSync();
|
|
1302
|
+
expect(container.querySelector('.caught')?.textContent).toBe('sync boom');
|
|
1303
|
+
expect(container.querySelector('.a')).toBeNull();
|
|
1304
|
+
expect(container.querySelector('.b')).toBeNull();
|
|
1305
|
+
},
|
|
1306
|
+
);
|
|
1307
|
+
|
|
1308
|
+
it(
|
|
1309
|
+
'catch stays stable when sync error fires with multiple pending trackAsyncs, one resolves and one rejects',
|
|
1310
|
+
async () => {
|
|
1311
|
+
let resolve_a: ((v: string) => void) | undefined;
|
|
1312
|
+
let reject_b: ((e: any) => void) | undefined;
|
|
1313
|
+
let promise_a = new Promise<string>((resolve) => {
|
|
1314
|
+
resolve_a = resolve;
|
|
1315
|
+
});
|
|
1316
|
+
let promise_b = new Promise<number>((_, reject) => {
|
|
1317
|
+
reject_b = reject;
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
let settled_a: { type: string; value: any } | null = null;
|
|
1321
|
+
let settled_b: { type: string; value: any } | null = null;
|
|
1322
|
+
promise_a.then(
|
|
1323
|
+
(v) => (settled_a = { type: 'resolved', value: v }),
|
|
1324
|
+
(e) => (settled_a = { type: 'rejected', value: e }),
|
|
1325
|
+
);
|
|
1326
|
+
promise_b.then(
|
|
1327
|
+
(v) => (settled_b = { type: 'resolved', value: v }),
|
|
1328
|
+
(e) => (settled_b = { type: 'rejected', value: e }),
|
|
1329
|
+
);
|
|
1330
|
+
|
|
1331
|
+
component App() {
|
|
1332
|
+
try {
|
|
1333
|
+
<ChildA />
|
|
1334
|
+
<ChildB />
|
|
1335
|
+
<ThrowingChild />
|
|
1336
|
+
} pending {
|
|
1337
|
+
<p class="pending">{'loading...'}</p>
|
|
1338
|
+
} catch (err: Error) {
|
|
1339
|
+
<p class="caught">{err.message}</p>
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
let dataA: any;
|
|
1344
|
+
component ChildA() {
|
|
1345
|
+
dataA = trackAsync(() => promise_a);
|
|
1346
|
+
<p class="a">{dataA.value}</p>
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
let dataB: any;
|
|
1350
|
+
component ChildB() {
|
|
1351
|
+
dataB = trackAsync(() => promise_b);
|
|
1352
|
+
<p class="b">{dataB.value}</p>
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
component ThrowingChild() {
|
|
1356
|
+
throw new Error('sync boom');
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
render(App);
|
|
1360
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
1361
|
+
flushSync();
|
|
1362
|
+
|
|
1363
|
+
expect(container.querySelector('.caught')?.textContent).toBe('sync boom');
|
|
1364
|
+
|
|
1365
|
+
expect(peek(dataA)).toEqual(SUSPENSE_PENDING);
|
|
1366
|
+
expect(peek(dataB)).toEqual(SUSPENSE_PENDING);
|
|
1367
|
+
|
|
1368
|
+
// Resolve A, reject B
|
|
1369
|
+
resolve_a!('hello');
|
|
1370
|
+
reject_b!(new Error('async failure'));
|
|
1371
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
1372
|
+
flushSync();
|
|
1373
|
+
|
|
1374
|
+
// Catch should stay with original sync error
|
|
1375
|
+
expect(container.querySelector('.caught')?.textContent).toBe('sync boom');
|
|
1376
|
+
expect(container.querySelector('.a')).toBeNull();
|
|
1377
|
+
expect(container.querySelector('.b')).toBeNull();
|
|
1378
|
+
expect(settled_a).toEqual({ type: 'resolved', value: 'hello' });
|
|
1379
|
+
expect(settled_b).toEqual({ type: 'rejected', value: expect.any(Error) });
|
|
1380
|
+
|
|
1381
|
+
expect(peek(dataA)).toEqual('hello');
|
|
1382
|
+
expect(peek(dataB)).toEqual(SUSPENSE_REJECTED);
|
|
1383
|
+
|
|
1384
|
+
// Verify stability after all microtasks drain
|
|
1385
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1386
|
+
flushSync();
|
|
1387
|
+
expect(container.querySelector('.caught')?.textContent).toBe('sync boom');
|
|
1388
|
+
expect(container.querySelector('.a')).toBeNull();
|
|
1389
|
+
expect(container.querySelector('.b')).toBeNull();
|
|
1390
|
+
},
|
|
1391
|
+
);
|
|
1392
|
+
|
|
1393
|
+
it('try block without catch propagates sync error to upstream boundary', async () => {
|
|
1394
|
+
component ThrowingChild() {
|
|
1395
|
+
throw new Error('no catch here');
|
|
1396
|
+
<p>{'should not render'}</p>
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
component App() {
|
|
1400
|
+
try {
|
|
1401
|
+
<Inner />
|
|
1402
|
+
} catch (err) {
|
|
1403
|
+
<p class="upstream-caught">{(err as Error).message}</p>
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
component Inner() {
|
|
1408
|
+
try {
|
|
1409
|
+
<ThrowingChild />
|
|
1410
|
+
} pending {
|
|
1411
|
+
<p class="inner-pending">{'loading...'}</p>
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
render(App);
|
|
1416
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1417
|
+
flushSync();
|
|
1418
|
+
|
|
1419
|
+
expect(container.querySelector('.upstream-caught')?.textContent).toBe('no catch here');
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
it('try block without catch propagates async rejection to upstream boundary', async () => {
|
|
1423
|
+
component App() {
|
|
1424
|
+
try {
|
|
1425
|
+
<Inner />
|
|
1426
|
+
} catch (err) {
|
|
1427
|
+
<p class="upstream-caught">{(err as Error).message}</p>
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
component Inner() {
|
|
1432
|
+
try {
|
|
1433
|
+
let &[data] = trackAsync(() => Promise.reject(new Error('async no catch')));
|
|
1434
|
+
<p class="resolved">{data}</p>
|
|
1435
|
+
} pending {
|
|
1436
|
+
<p class="inner-pending">{'loading...'}</p>
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
render(App);
|
|
1441
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1442
|
+
flushSync();
|
|
1443
|
+
|
|
1444
|
+
expect(container.querySelector('.upstream-caught')?.textContent).toBe('async no catch');
|
|
1445
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
it(
|
|
1449
|
+
'try block without catch and without pending propagates async rejection to upstream boundary',
|
|
1450
|
+
async () => {
|
|
1451
|
+
let reject_fn: (error: Error) => void;
|
|
1452
|
+
const user_promise = new Promise<string>((_, reject) => {
|
|
1453
|
+
reject_fn = reject;
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
component App() {
|
|
1457
|
+
try {
|
|
1458
|
+
<Inner />
|
|
1459
|
+
} catch (err) {
|
|
1460
|
+
<p class="upstream-caught">{(err as Error).message}</p>
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
component Inner() {
|
|
1465
|
+
try {
|
|
1466
|
+
let &[data] = trackAsync(() => user_promise);
|
|
1467
|
+
<p class="resolved">{data}</p>
|
|
1468
|
+
} pending {
|
|
1469
|
+
<p class="inner-pending">{'loading...'}</p>
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
render(App);
|
|
1474
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1475
|
+
flushSync();
|
|
1476
|
+
|
|
1477
|
+
// Inner should show nothing yet (no pending block)
|
|
1478
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
1479
|
+
expect(container.querySelector('.upstream-caught')).toBeNull();
|
|
1480
|
+
|
|
1481
|
+
// Reject the promise
|
|
1482
|
+
reject_fn!(new Error('deferred no catch'));
|
|
1483
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1484
|
+
flushSync();
|
|
1485
|
+
|
|
1486
|
+
expect(container.querySelector('.upstream-caught')?.textContent).toBe('deferred no catch');
|
|
1487
|
+
},
|
|
1488
|
+
);
|
|
1489
|
+
|
|
1490
|
+
it('nested try blocks without catch propagate error through multiple levels', async () => {
|
|
1491
|
+
component ThrowingChild() {
|
|
1492
|
+
throw new Error('deep error');
|
|
1493
|
+
<p>{'should not render'}</p>
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
component App() {
|
|
1497
|
+
try {
|
|
1498
|
+
<Middle />
|
|
1499
|
+
} catch (err) {
|
|
1500
|
+
<p class="top-caught">{(err as Error).message}</p>
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
component Middle() {
|
|
1505
|
+
try {
|
|
1506
|
+
<Inner />
|
|
1507
|
+
} pending {
|
|
1508
|
+
<p class="mid-pending">{'loading...'}</p>
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
component Inner() {
|
|
1513
|
+
try {
|
|
1514
|
+
<ThrowingChild />
|
|
1515
|
+
} pending {
|
|
1516
|
+
<p class="inner-pending">{'loading...'}</p>
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
render(App);
|
|
1521
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1522
|
+
flushSync();
|
|
1523
|
+
|
|
1524
|
+
expect(container.querySelector('.top-caught')?.textContent).toBe('deep error');
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
it('try block without catch throws to global when no upstream boundary exists', async () => {
|
|
1528
|
+
component ThrowingChild() {
|
|
1529
|
+
throw new Error('unhandled error');
|
|
1530
|
+
<p>{'should not render'}</p>
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
component App() {
|
|
1534
|
+
try {
|
|
1535
|
+
<ThrowingChild />
|
|
1536
|
+
} pending {
|
|
1537
|
+
<p class="pending">{'loading...'}</p>
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
expect(() => {
|
|
1542
|
+
render(App);
|
|
1543
|
+
}).toThrow('unhandled error');
|
|
1544
|
+
});
|
|
1545
|
+
|
|
1546
|
+
it(
|
|
1547
|
+
'outer try block with pending and inner try with catch and without pending should propagate rejection to inner boundary',
|
|
1548
|
+
async () => {
|
|
1549
|
+
let reject_fn: (error: Error) => void;
|
|
1550
|
+
const user_promise = new Promise<string>((_, reject) => {
|
|
1551
|
+
reject_fn = reject;
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
component Inner() {
|
|
1555
|
+
try {
|
|
1556
|
+
let &[data] = trackAsync(() => user_promise);
|
|
1557
|
+
<p class="resolved">{data}</p>
|
|
1558
|
+
} catch (err) {
|
|
1559
|
+
<p class="downstream-caught">{(err as Error).message}</p>
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
component App() {
|
|
1564
|
+
try {
|
|
1565
|
+
<Inner />
|
|
1566
|
+
} pending {
|
|
1567
|
+
<p class="outer-pending">{'loading...'}</p>
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
render(App);
|
|
1572
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1573
|
+
flushSync();
|
|
1574
|
+
|
|
1575
|
+
// Inner should show nothing yet (no pending block)
|
|
1576
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
1577
|
+
expect(container.querySelector('.downstream-caught')).toBeNull();
|
|
1578
|
+
expect(container.querySelector('.outer-pending')?.textContent).toBe('loading...');
|
|
1579
|
+
|
|
1580
|
+
// Reject the promise
|
|
1581
|
+
reject_fn!(new Error('deferred no catch'));
|
|
1582
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1583
|
+
flushSync();
|
|
1584
|
+
|
|
1585
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
1586
|
+
expect(container.querySelector('.downstream-caught')?.textContent).toBe('deferred no catch');
|
|
1587
|
+
expect(container.querySelector('.outer-pending')).toBeNull();
|
|
1588
|
+
},
|
|
1589
|
+
);
|
|
1590
|
+
|
|
1591
|
+
it(
|
|
1592
|
+
'outer try block with pending and inner try with synchronous catch and without pending should propagate rejection to inner boundary with promise rejecting',
|
|
1593
|
+
async () => {
|
|
1594
|
+
let reject_fn: (error: Error) => void;
|
|
1595
|
+
let resolve_fn: (value: string) => void;
|
|
1596
|
+
const user_promise = new Promise<string>((resolve, reject) => {
|
|
1597
|
+
resolve_fn = resolve;
|
|
1598
|
+
reject_fn = reject;
|
|
1599
|
+
});
|
|
1600
|
+
|
|
1601
|
+
let user_settled: { type: string; value: any } | null = null;
|
|
1602
|
+
user_promise.then(
|
|
1603
|
+
(v) => (user_settled = { type: 'resolved', value: v }),
|
|
1604
|
+
(e) => (user_settled = { type: 'rejected', value: e }),
|
|
1605
|
+
);
|
|
1606
|
+
|
|
1607
|
+
component Inner() {
|
|
1608
|
+
try {
|
|
1609
|
+
let &[data] = trackAsync(() => user_promise);
|
|
1610
|
+
throw new Error('synchronous error');
|
|
1611
|
+
<p class="resolved">{data}</p>
|
|
1612
|
+
} catch (err) {
|
|
1613
|
+
<p class="downstream-caught">{(err as Error).message}</p>
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
component App() {
|
|
1618
|
+
try {
|
|
1619
|
+
<Inner />
|
|
1620
|
+
} pending {
|
|
1621
|
+
<p class="outer-pending">{'loading...'}</p>
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
render(App);
|
|
1626
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1627
|
+
flushSync();
|
|
1628
|
+
|
|
1629
|
+
// Inner should show nothing yet (no pending block)
|
|
1630
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
1631
|
+
expect(container.querySelector('.outer-pending')).not.toBeNull();
|
|
1632
|
+
expect(container.querySelector('.downstream-caught')).toBeNull();
|
|
1633
|
+
|
|
1634
|
+
// Reject the promise
|
|
1635
|
+
reject_fn!(new Error('whatever'));
|
|
1636
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1637
|
+
flushSync();
|
|
1638
|
+
|
|
1639
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
1640
|
+
expect(container.querySelector('.outer-pending')).toBeNull();
|
|
1641
|
+
expect(container.querySelector('.downstream-caught')?.textContent).toBe('synchronous error');
|
|
1642
|
+
expect(user_settled).toEqual({ type: 'rejected', value: expect.any(Error) });
|
|
1643
|
+
},
|
|
1644
|
+
);
|
|
1645
|
+
|
|
1646
|
+
it(
|
|
1647
|
+
'outer try block with pending and inner try with synchronous catch and without pending should propagate rejection to inner boundary with promise resolving instead of rejecting',
|
|
1648
|
+
async () => {
|
|
1649
|
+
let reject_fn: (error: Error) => void;
|
|
1650
|
+
let resolve_fn: (value: string) => void;
|
|
1651
|
+
const user_promise = new Promise<string>((resolve, reject) => {
|
|
1652
|
+
resolve_fn = resolve;
|
|
1653
|
+
reject_fn = reject;
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
let user_settled: { type: string; value: any } | null = null;
|
|
1657
|
+
user_promise.then(
|
|
1658
|
+
(v) => (user_settled = { type: 'resolved', value: v }),
|
|
1659
|
+
(e) => (user_settled = { type: 'rejected', value: e }),
|
|
1660
|
+
);
|
|
1661
|
+
|
|
1662
|
+
component Inner() {
|
|
1663
|
+
try {
|
|
1664
|
+
let &[data] = trackAsync(() => user_promise);
|
|
1665
|
+
throw new Error('synchronous error');
|
|
1666
|
+
<p class="resolved">{data}</p>
|
|
1667
|
+
} catch (err) {
|
|
1668
|
+
<p class="downstream-caught">{(err as Error).message}</p>
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
component App() {
|
|
1673
|
+
try {
|
|
1674
|
+
<Inner />
|
|
1675
|
+
} pending {
|
|
1676
|
+
<p class="outer-pending">{'loading...'}</p>
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
render(App);
|
|
1681
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1682
|
+
flushSync();
|
|
1683
|
+
|
|
1684
|
+
// Inner should show nothing yet (no pending block)
|
|
1685
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
1686
|
+
expect(container.querySelector('.outer-pending')).not.toBeNull();
|
|
1687
|
+
expect(container.querySelector('.downstream-caught')).toBeNull();
|
|
1688
|
+
|
|
1689
|
+
// Resolve the promise
|
|
1690
|
+
resolve_fn!('hello');
|
|
1691
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1692
|
+
flushSync();
|
|
1693
|
+
|
|
1694
|
+
expect(container.querySelector('.resolved')).toBeNull();
|
|
1695
|
+
expect(container.querySelector('.outer-pending')).toBeNull();
|
|
1696
|
+
// this should render the catch block and make the resolved visible,
|
|
1697
|
+
// and the pending of the parent goes away
|
|
1698
|
+
expect(container.querySelector('.downstream-caught')?.textContent).toBe('synchronous error');
|
|
1699
|
+
expect(user_settled).toEqual({ type: 'resolved', value: 'hello' });
|
|
1700
|
+
},
|
|
1701
|
+
);
|
|
1702
|
+
});
|