Skip to content

Commit 71fe085

Browse files
committed
refactor: add error-boundary support to server
1 parent 9df995f commit 71fe085

File tree

13 files changed

+180
-41
lines changed

13 files changed

+180
-41
lines changed

packages/core/src/awaiter/awaiter.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { REJECTED_STATUS } from '../constants';
21
import { type Fiber, type Hook } from '../fiber';
2+
import { REJECTED_STATUS } from '../constants';
3+
import { createError } from '../utils';
34

45
class Awaiter {
56
store = new Map<Hook, [Hook, Hook, Set<Promise<unknown>>]>();
67

78
add(suspense: Fiber, boundary: Fiber, promise: Promise<unknown>) {
8-
const key = suspense.hook || boundary.hook;
9+
const key = suspense?.hook || boundary?.hook;
910
!this.store.has(key) && this.store.set(key, [null, null, new Set()]);
1011
const data = this.store.get(key);
1112

@@ -30,16 +31,24 @@ class Awaiter {
3031
}
3132

3233
Promise.allSettled(promises).then(res => {
33-
const rejected = res.find(x => x.status === REJECTED_STATUS);
34+
const hook =
35+
boundaryHook && suspenseHook
36+
? boundaryHook.owner.id < suspenseHook.owner.id
37+
? boundaryHook
38+
: suspenseHook
39+
: boundaryHook || suspenseHook;
3440

35-
if (rejected && boundaryHook) {
36-
boundaryHook.owner.setError(new Error(rejected.reason));
41+
if (boundaryHook) {
42+
const rejected = res.find(x => x.status === REJECTED_STATUS) as PromiseRejectedResult;
43+
44+
rejected && boundaryHook.owner.setError(createError(rejected.reason));
3745
}
3846

3947
if (suspenseHook && pendings === suspenseHook.getPendings()) {
4048
suspenseHook.setIsPeinding(false);
41-
suspenseHook.update();
4249
}
50+
51+
hook.update();
4352
});
4453
}
4554
}

packages/core/src/boundary/boundary.tsx

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,76 @@
1-
import type { DarkElement, SlotProps, Callback } from '../shared';
1+
import type { DarkElement, SlotProps, Callback, AppResource } from '../shared';
2+
import { detectIsFunction, createError } from '../utils';
23
import { __useCursor as useCursor } from '../internal';
3-
import { detectIsFunction } from '../utils';
4+
import { useUpdate } from '../use-update';
45
import { useEffect } from '../use-effect';
56
import { component } from '../component';
67
import { useState } from '../use-state';
78
import { useEvent } from '../use-event';
9+
import { useMemo } from '../use-memo';
10+
import { $$scope } from '../scope';
811

9-
function useError(): [Error | null, Callback] {
12+
function useError(__id?: number): [Error | null, Callback] {
1013
const cursor = useCursor();
11-
const [error, setError] = useState<Error>(null);
12-
const reset = useEvent(() => setError(null));
14+
const $scope = $$scope();
15+
const inBoundary = cursor.parent?.hook?.getIsBoundary();
16+
const res = inBoundary ? $scope.getResource(__id) : null;
17+
const [error, setError] = useState<Error>(() => (inBoundary ? init(res) : null));
18+
const reset = useEvent(() => {
19+
inBoundary && $scope.setResource(__id, null);
20+
setError(null);
21+
});
1322

14-
cursor.hook.setCatch(setError);
23+
if (inBoundary) {
24+
cursor.parent.hook.setCatch(setError);
25+
} else {
26+
cursor.hook.setCatch(setError);
27+
}
1528

1629
return [error, reset];
1730
}
1831

32+
const init = (res: AppResource) => (res?.[1] ? createError(res?.[1]) : null);
33+
1934
type ErrorBoundaryProps = {
2035
fallback?: DarkElement;
2136
renderFallback?: (x: RenderFallbackOptions) => DarkElement;
2237
onError?: (e: Error) => void;
2338
} & Required<SlotProps>;
2439

2540
const ErrorBoundary = component<ErrorBoundaryProps>(
26-
({ fallback = null, renderFallback, onError, slot }) => {
27-
const [error, reset] = useError();
41+
props => {
42+
const cursor = useCursor();
43+
const update = useUpdate();
44+
const $scope = $$scope();
45+
const id = useMemo(() => $scope.getNextResourceId(), []);
46+
47+
cursor.hook.setIsBoundary(true);
48+
cursor.hook.setUpdate(update);
49+
cursor.hook.setLevel($scope.getMountLevel());
50+
cursor.hook.setResId(id);
51+
52+
return <ErrorBoundaryInternal {...props} id={id} />;
53+
},
54+
{
55+
displayName: 'ErrorBoundary',
56+
},
57+
);
58+
59+
type ErrorBoundaryInternalProps = {
60+
id: number;
61+
} & ErrorBoundaryProps;
62+
63+
const ErrorBoundaryInternal = component<ErrorBoundaryInternalProps>(
64+
({ id, fallback = null, renderFallback, onError, slot }) => {
65+
const [error, reset] = useError(id);
2866

2967
useEffect(() => {
3068
detectIsFunction(onError) && onError(error);
3169
}, [error]);
3270

3371
return error ? (detectIsFunction(renderFallback) ? renderFallback({ error, reset }) : fallback) : slot;
3472
},
35-
{ displayName: 'ErrorBoundary' },
73+
{ displayName: 'ErrorBoundaryInternal' },
3674
);
3775

3876
type RenderFallbackOptions = {

packages/core/src/constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ export const MOVE_MASK = 32;
1818
export const IS_WIP_HOOK_MASK = 1;
1919
export const IS_PORTAL_HOOK_MASK = 2;
2020
export const IS_SUSPENSE_HOOK_MASK = 4;
21-
export const IS_PENDING_HOOK_MASK = 8;
21+
export const IS_BOUNDARY_HOOK_MASK = 8;
22+
export const IS_PENDING_HOOK_MASK = 16;
2223
export const HOOK_DELIMETER = ':';
2324
export const YIELD_INTERVAL = 6;
2425
export const STATE_SCRIPT_TYPE = 'text/dark-state';

packages/core/src/fiber/fiber.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { IS_WIP_HOOK_MASK, IS_PORTAL_HOOK_MASK, IS_SUSPENSE_HOOK_MASK, IS_PENDING_HOOK_MASK } from '../constants';
1+
import {
2+
IS_WIP_HOOK_MASK,
3+
IS_PORTAL_HOOK_MASK,
4+
IS_SUSPENSE_HOOK_MASK,
5+
IS_BOUNDARY_HOOK_MASK,
6+
IS_PENDING_HOOK_MASK,
7+
} from '../constants';
28
import { detectIsFunction, detectIsUndefined, logError } from '../utils';
39
import { type Instance, type Callback, type TimerId } from '../shared';
410
import { detectAreSameComponentTypesWithSameKeys } from '../view';
@@ -116,6 +122,14 @@ class Hook<T = unknown> {
116122
this.__mark(IS_SUSPENSE_HOOK_MASK, x);
117123
}
118124

125+
getIsBoundary() {
126+
return this.__getMask(IS_BOUNDARY_HOOK_MASK);
127+
}
128+
129+
setIsBoundary(x: boolean) {
130+
this.__mark(IS_BOUNDARY_HOOK_MASK, x);
131+
}
132+
119133
getIsPending() {
120134
return this.__getMask(IS_PENDING_HOOK_MASK);
121135
}
@@ -173,6 +187,24 @@ class Hook<T = unknown> {
173187
getPendings() {
174188
return this.box?.pendings;
175189
}
190+
191+
setResId(x: number) {
192+
this.__box();
193+
this.box.resId = x;
194+
}
195+
196+
getResId() {
197+
return this.box?.resId;
198+
}
199+
200+
setLevel(x: number) {
201+
this.__box();
202+
this.box.level = x;
203+
}
204+
205+
getLevel() {
206+
return this.box?.level;
207+
}
176208
}
177209

178210
function getHook(alt: Fiber, prevInst: Instance, nextInst: Instance): Hook | null {
@@ -188,6 +220,8 @@ type Box = {
188220
catch?: Catch;
189221
pendings?: number;
190222
update?: Callback;
223+
resId?: number;
224+
level?: number;
191225
};
192226

193227
type Batch = {

packages/core/src/scheduler/scheduler.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import { getTime, detectIsPromise, detectIsFunction, dummyFn } from '../utils';
12
import { HOOK_DELIMETER, YIELD_INTERVAL, TaskPriority } from '../constants';
2-
import { getTime, detectIsPromise, detectIsFunction } from '../utils';
33
import { type WorkLoop, workLoop, detectIsBusy } from '../workloop';
44
import { type Callback } from '../shared';
55
import { EventEmitter } from '../emitter';
@@ -148,7 +148,7 @@ class Scheduler {
148148
task.getForceAsync() ? this.requestCallbackAsync(workLoop) : this.requestCallback(workLoop);
149149
} catch (something) {
150150
if (detectIsPromise(something)) {
151-
something.finally(() => {
151+
something.catch(dummyFn).finally(() => {
152152
this.run(task);
153153
});
154154
} else {
@@ -180,7 +180,7 @@ class Scheduler {
180180
const something = callback(false);
181181

182182
if (detectIsPromise(something)) {
183-
something.finally(() => {
183+
something.catch(dummyFn).finally(() => {
184184
this.requestCallback(callback);
185185
});
186186
} else {
@@ -195,7 +195,7 @@ class Scheduler {
195195
const something = this.scheduledCallback(true);
196196

197197
if (detectIsPromise(something)) {
198-
something.finally(() => {
198+
something.catch(dummyFn).finally(() => {
199199
this.port.postMessage(null);
200200
});
201201
} else if (something) {

packages/core/src/scope/scope.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,11 @@ class Scope {
156156
this.mountLevel = this.mountLevel - 1;
157157
}
158158

159+
setMount(level: number) {
160+
this.mountLevel = level;
161+
this.mountNav[this.mountLevel] = 0;
162+
}
163+
159164
navToPrev() {
160165
const idx = this.getMountIndex();
161166

@@ -168,6 +173,10 @@ class Scope {
168173
}
169174
}
170175

176+
getMountLevel() {
177+
return this.mountLevel;
178+
}
179+
171180
getMountIndex() {
172181
return this.mountNav[this.mountLevel];
173182
}
@@ -405,14 +414,6 @@ class Scope {
405414
return this.resources;
406415
}
407416

408-
getResourceId() {
409-
return this.resourceId;
410-
}
411-
412-
setResourceId(id: number) {
413-
this.resourceId = id;
414-
}
415-
416417
getNextResourceId() {
417418
return ++this.resourceId;
418419
}

packages/core/src/utils/utils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ const createIndexKey = (idx: number) => `${INDEX_KEY}:${idx}`;
9999

100100
const mapRecord = <T extends object>(record: T) => keys(record).map(x => record[x]);
101101

102+
function createError(reason: unknown) {
103+
return reason instanceof Error ? reason : new Error(String(reason).replace('Error: ', ''));
104+
}
105+
102106
export {
103107
detectIsFunction,
104108
detectIsUndefined,
@@ -128,4 +132,5 @@ export {
128132
nextTick,
129133
createIndexKey,
130134
mapRecord,
135+
createError,
131136
};

packages/core/src/walk/walk.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ function resolveBoundary(fiber: Fiber): Fiber {
9494
let boundary = fiber;
9595

9696
while (boundary) {
97-
if (boundary.hook?.hasCatch()) return boundary;
97+
if (boundary.hook?.getIsBoundary()) return boundary;
9898
boundary = boundary.parent;
9999
}
100100

packages/core/src/workloop/workloop.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -398,21 +398,21 @@ function mount(fiber: Fiber, prev: Fiber, $scope: Scope) {
398398

399399
if (detectIsPromise(err)) {
400400
const promise = err;
401-
const reset = createResetClosure(fiber, prev, $scope);
401+
const reset = createReset(fiber, prev, $scope);
402+
const boundary = resolveBoundary(fiber);
402403

403404
if (!isSSR) {
404405
const suspense = resolveSuspense(fiber);
405-
const boundary = resolveBoundary(fiber);
406406

407407
if (suspense || boundary) {
408408
$scope.getAwaiter().add(suspense, boundary, promise);
409409
} else {
410410
reset();
411-
throw err;
411+
throw promise;
412412
}
413413
} else {
414-
reset();
415-
throw err;
414+
handleAsync(promise, boundary, reset, $scope);
415+
throw promise;
416416
}
417417
} else {
418418
component.children = [];
@@ -432,7 +432,20 @@ function mount(fiber: Fiber, prev: Fiber, $scope: Scope) {
432432
return inst;
433433
}
434434

435-
const createResetClosure = (fiber: Fiber, prev: Fiber, $scope: Scope) => () => {
435+
async function handleAsync(promise: Promise<unknown>, boundary: Fiber, reset: Callback, $scope: Scope) {
436+
let isRejected = false;
437+
438+
try {
439+
await promise;
440+
} catch (reason) {
441+
isRejected = true;
442+
boundary && restartFromBoundary(boundary, reason, $scope);
443+
} finally {
444+
(!isRejected || !boundary) && reset();
445+
}
446+
}
447+
448+
const createReset = (fiber: Fiber, prev: Fiber, $scope: Scope) => () => {
436449
if (prev) {
437450
fiber.hook.owner = null;
438451
fiber.hook.idx = 0;
@@ -445,6 +458,17 @@ const createResetClosure = (fiber: Fiber, prev: Fiber, $scope: Scope) => () => {
445458
}
446459
};
447460

461+
function restartFromBoundary(fiber: Fiber, reason: unknown, $scope: Scope) {
462+
const resId = fiber.hook.getResId();
463+
464+
fiber.child = null;
465+
fiber.cec = 0;
466+
Fiber.setNextId(fiber.id);
467+
$scope.setMount(fiber.hook.getLevel());
468+
$scope.setNextUnitOfWork(fiber);
469+
$scope.setResource(resId, [null, reason instanceof Error ? reason.stack : (reason as string)]);
470+
}
471+
448472
function extractKeys(alt: Fiber, children: Array<Instance>) {
449473
let nextFiber = alt;
450474
let idx = 0;

packages/data/src/use-query/use-query.spec.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type DarkElement, component, Suspense, useState, STATE_SCRIPT_TYPE } from '@dark-engine/core';
1+
import { type DarkElement, component, Suspense, ErrorBoundary, useState, STATE_SCRIPT_TYPE } from '@dark-engine/core';
22
import {
33
createBrowserEnv,
44
createBrowserHydrateEnv,
@@ -290,4 +290,27 @@ describe('@data/use-query', () => {
290290
expect(spy).toHaveBeenCalledWith([false, 20, null]);
291291
spy.mockClear();
292292
});
293+
294+
// test.only('xxx', async () => {
295+
// const DataLoader = component(() => {
296+
// const { data } = useQuery(Key.GET_DATA, ({ id }) => api.getData(id, true), {
297+
// variables: { id: 2 },
298+
// });
299+
300+
// return <div>{data}</div>;
301+
// });
302+
// const App = component(() => {
303+
// return (
304+
// <ErrorBoundary fallback={<div>ERROR!</div>}>
305+
// <DataLoader />
306+
// </ErrorBoundary>
307+
// );
308+
// });
309+
// const { renderToString } = createServerEnv();
310+
// const result = await renderToString(withProvider(<App />));
311+
312+
// expect(result).toMatchInlineSnapshot(
313+
// `"<div>ERROR!</div><script type="text/dark-state">"eyIxIjpbbnVsbCwiRXJyb3I6IG9vcHMhIl19"</script>"`,
314+
// );
315+
// });
293316
});

0 commit comments

Comments
 (0)