mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[FIX] component: protect against errors in onWillDestroy
This commit is contained in:
committed by
Sam Degueldre
parent
decf42c742
commit
7fb166bd50
@@ -80,10 +80,7 @@ export function component<P extends object>(
|
|||||||
let isDynamic = typeof name !== "string";
|
let isDynamic = typeof name !== "string";
|
||||||
|
|
||||||
if (node) {
|
if (node) {
|
||||||
if (node.status < STATUS.MOUNTED) {
|
if (node.status === STATUS.DESTROYED) {
|
||||||
node.destroy();
|
|
||||||
node = undefined;
|
|
||||||
} else if (node.status === STATUS.DESTROYED) {
|
|
||||||
node = undefined;
|
node = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,7 +191,7 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
|
|||||||
|
|
||||||
async render(deep: boolean = false) {
|
async render(deep: boolean = false) {
|
||||||
let current = this.fiber;
|
let current = this.fiber;
|
||||||
if (current && current.root!.locked) {
|
if (current && (current.root!.locked || (current as any).bdom === true)) {
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
// situation may have changed after the microtask tick
|
// situation may have changed after the microtask tick
|
||||||
current = this.fiber;
|
current = this.fiber;
|
||||||
@@ -216,6 +213,7 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
|
|||||||
const fiber = makeRootFiber(this);
|
const fiber = makeRootFiber(this);
|
||||||
fiber.deep = deep;
|
fiber.deep = deep;
|
||||||
this.fiber = fiber;
|
this.fiber = fiber;
|
||||||
|
|
||||||
this.app.scheduler.addFiber(fiber);
|
this.app.scheduler.addFiber(fiber);
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
if (this.status === STATUS.DESTROYED) {
|
if (this.status === STATUS.DESTROYED) {
|
||||||
@@ -255,9 +253,15 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
|
|||||||
for (let child of Object.values(this.children)) {
|
for (let child of Object.values(this.children)) {
|
||||||
child._destroy();
|
child._destroy();
|
||||||
}
|
}
|
||||||
|
if (this.willDestroy.length) {
|
||||||
|
try {
|
||||||
for (let cb of this.willDestroy) {
|
for (let cb of this.willDestroy) {
|
||||||
cb.call(component);
|
cb.call(component);
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
handleError({ error: e, node: this });
|
||||||
|
}
|
||||||
|
}
|
||||||
this.status = STATUS.DESTROYED;
|
this.status = STATUS.DESTROYED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { Fiber } from "./fibers";
|
|||||||
export const fibersInError: WeakMap<Fiber, any> = new WeakMap();
|
export const fibersInError: WeakMap<Fiber, any> = new WeakMap();
|
||||||
export const nodeErrorHandlers: WeakMap<ComponentNode, ((error: any) => void)[]> = new WeakMap();
|
export const nodeErrorHandlers: WeakMap<ComponentNode, ((error: any) => void)[]> = new WeakMap();
|
||||||
|
|
||||||
function _handleError(node: ComponentNode | null, error: any, isFirstRound = false): boolean {
|
function _handleError(node: ComponentNode | null, error: any): boolean {
|
||||||
if (!node) {
|
if (!node) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -16,23 +16,19 @@ function _handleError(node: ComponentNode | null, error: any, isFirstRound = fal
|
|||||||
|
|
||||||
const errorHandlers = nodeErrorHandlers.get(node);
|
const errorHandlers = nodeErrorHandlers.get(node);
|
||||||
if (errorHandlers) {
|
if (errorHandlers) {
|
||||||
let stopped = false;
|
let handled = false;
|
||||||
// execute in the opposite order
|
// execute in the opposite order
|
||||||
for (let i = errorHandlers.length - 1; i >= 0; i--) {
|
for (let i = errorHandlers.length - 1; i >= 0; i--) {
|
||||||
try {
|
try {
|
||||||
errorHandlers[i](error);
|
errorHandlers[i](error);
|
||||||
stopped = true;
|
handled = true;
|
||||||
break;
|
break;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e;
|
error = e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stopped) {
|
if (handled) {
|
||||||
if (isFirstRound && fiber && fiber.node.fiber) {
|
|
||||||
const root = fiber.root!;
|
|
||||||
root.setCounter(root.counter - 1);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,7 +51,7 @@ export function handleError(params: ErrorParams) {
|
|||||||
|
|
||||||
fibersInError.set(fiber.root!, error);
|
fibersInError.set(fiber.root!, error);
|
||||||
|
|
||||||
const handled = _handleError(node, error, true);
|
const handled = _handleError(node, error);
|
||||||
if (!handled) {
|
if (!handled) {
|
||||||
console.warn(`[Owl] Unhandled error. Destroying the root component`);
|
console.warn(`[Owl] Unhandled error. Destroying the root component`);
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -124,11 +124,12 @@ export class Fiber {
|
|||||||
const root = this.root;
|
const root = this.root;
|
||||||
if (root) {
|
if (root) {
|
||||||
try {
|
try {
|
||||||
|
(this.bdom as any) = true;
|
||||||
this.bdom = node.renderFn();
|
this.bdom = node.renderFn();
|
||||||
root.setCounter(root.counter - 1);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError({ node, error: e });
|
handleError({ node, error: e });
|
||||||
}
|
}
|
||||||
|
root.setCounter(root.counter - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,64 @@ exports[`basics simple catchError 2`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`can catch errors an error in onWillDestroy 1`] = `
|
||||||
|
"function anonymous(bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
let b2,b3;
|
||||||
|
b2 = text(ctx['state'].value);
|
||||||
|
if (ctx['state'].hasChild) {
|
||||||
|
b3 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
|
||||||
|
}
|
||||||
|
return multi([b2, b3]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`can catch errors an error in onWillDestroy 2`] = `
|
||||||
|
"function anonymous(bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div>abc</div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`can catch errors an error in onWillDestroy, variation 1`] = `
|
||||||
|
"function anonymous(bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
let b2,b3;
|
||||||
|
b2 = text(ctx['state'].value);
|
||||||
|
if (ctx['state'].hasChild) {
|
||||||
|
b3 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
|
||||||
|
}
|
||||||
|
return multi([b2, b3]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`can catch errors an error in onWillDestroy, variation 2`] = `
|
||||||
|
"function anonymous(bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div>abc</div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`can catch errors calling a hook outside setup should crash 1`] = `
|
exports[`can catch errors calling a hook outside setup should crash 1`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -172,6 +172,11 @@ test("destroying/recreating a subcomponent, other scenario", async () => {
|
|||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect([
|
expect([
|
||||||
|
"Parent:willRender",
|
||||||
|
"Child:setup",
|
||||||
|
"Child:willStart",
|
||||||
|
"Parent:rendered",
|
||||||
|
"Child:willDestroy",
|
||||||
"Parent:willRender",
|
"Parent:willRender",
|
||||||
"Child:setup",
|
"Child:setup",
|
||||||
"Child:willStart",
|
"Child:willStart",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, mount } from "../../src";
|
import { Component, mount, onWillDestroy } from "../../src";
|
||||||
import {
|
import {
|
||||||
onError,
|
onError,
|
||||||
onMounted,
|
onMounted,
|
||||||
@@ -1197,4 +1197,126 @@ describe("can catch errors", () => {
|
|||||||
expect(fixture.innerHTML).toBe("<div>Child 2</div>");
|
expect(fixture.innerHTML).toBe("<div>Child 2</div>");
|
||||||
expect(steps).toEqual(["Error Component"]);
|
expect(steps).toEqual(["Error Component"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("an error in onWillDestroy", async () => {
|
||||||
|
class Child extends Component {
|
||||||
|
static template = xml`<div>abc</div>`;
|
||||||
|
setup() {
|
||||||
|
useLogLifecycle();
|
||||||
|
onWillDestroy(() => {
|
||||||
|
throw new Error("boom");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<t t-esc="state.value"/>
|
||||||
|
<t t-if="state.hasChild"><Child/></t>`;
|
||||||
|
static components = { Child };
|
||||||
|
|
||||||
|
state = useState({ value: 1, hasChild: true });
|
||||||
|
setup() {
|
||||||
|
useLogLifecycle();
|
||||||
|
onError(() => {
|
||||||
|
this.state.value++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = await mount(Parent, fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("1<div>abc</div>");
|
||||||
|
expect([
|
||||||
|
"Parent:setup",
|
||||||
|
"Parent:willStart",
|
||||||
|
"Parent:willRender",
|
||||||
|
"Child:setup",
|
||||||
|
"Child:willStart",
|
||||||
|
"Parent:rendered",
|
||||||
|
"Child:willRender",
|
||||||
|
"Child:rendered",
|
||||||
|
"Child:mounted",
|
||||||
|
"Parent:mounted",
|
||||||
|
]).toBeLogged();
|
||||||
|
parent.state.hasChild = false;
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
expect([
|
||||||
|
"Parent:willRender",
|
||||||
|
"Parent:rendered",
|
||||||
|
"Parent:willPatch",
|
||||||
|
"Child:willUnmount",
|
||||||
|
"Child:willDestroy",
|
||||||
|
"Parent:willRender",
|
||||||
|
"Parent:rendered",
|
||||||
|
"Parent:willPatch",
|
||||||
|
"Parent:patched",
|
||||||
|
]).toBeLogged();
|
||||||
|
expect(fixture.innerHTML).toBe("2");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an error in onWillDestroy, variation", async () => {
|
||||||
|
class Child extends Component {
|
||||||
|
static template = xml`<div>abc</div>`;
|
||||||
|
setup() {
|
||||||
|
useLogLifecycle();
|
||||||
|
onWillDestroy(() => {
|
||||||
|
throw new Error("boom");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<t t-esc="state.value"/>
|
||||||
|
<t t-if="state.hasChild"><Child/></t>`;
|
||||||
|
static components = { Child };
|
||||||
|
|
||||||
|
state = useState({ value: 1, hasChild: false });
|
||||||
|
setup() {
|
||||||
|
useLogLifecycle();
|
||||||
|
onError(() => {
|
||||||
|
this.state.value++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = await mount(Parent, fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("1");
|
||||||
|
|
||||||
|
expect([
|
||||||
|
"Parent:setup",
|
||||||
|
"Parent:willStart",
|
||||||
|
"Parent:willRender",
|
||||||
|
"Parent:rendered",
|
||||||
|
"Parent:mounted",
|
||||||
|
]).toBeLogged();
|
||||||
|
|
||||||
|
parent.state.hasChild = true;
|
||||||
|
await nextMicroTick();
|
||||||
|
await nextMicroTick();
|
||||||
|
await nextMicroTick();
|
||||||
|
await nextMicroTick();
|
||||||
|
await nextMicroTick();
|
||||||
|
expect([
|
||||||
|
"Parent:willRender",
|
||||||
|
"Child:setup",
|
||||||
|
"Child:willStart",
|
||||||
|
"Parent:rendered",
|
||||||
|
"Child:willRender",
|
||||||
|
"Child:rendered",
|
||||||
|
]).toBeLogged();
|
||||||
|
parent.state.hasChild = false;
|
||||||
|
await nextTick();
|
||||||
|
expect([
|
||||||
|
"Child:willDestroy",
|
||||||
|
"Parent:willRender",
|
||||||
|
"Parent:rendered",
|
||||||
|
"Parent:willPatch",
|
||||||
|
"Parent:patched",
|
||||||
|
]).toBeLogged();
|
||||||
|
expect(fixture.innerHTML).toBe("2");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user