mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
7538aeae0e
Before this commit, most of the time, components are destroyed when the
virtual dom is patched and a component node is removed. However, since
Owl is asynchronous and a component may take some time to get ready
(with onWillStart), it can happen that a component is created, then
before it is ready, it is recreated. In that case, the initial instance
has to be destroyed.
Before this commit, the destroy operation was done immediately, when we
cancel the current fibers. However, this means that we cannot have a
guarantee between micro task ticks that a component has not been
destroyed in the meantime.
For example, in Odoo, it is common to use the rpc service, which will
throw an error if called by a destroyed component. But because of the
possible destruction of a component at any time, the following code is
unsafe:
async loadSomeData() {
// guaranteed to be called when component is alive
await Promise.resolve();
// however here, component may have been destroyed
this.rpc(...)
}
So, to prevent this issue, we can slightly delay the destroy operation.
It is not entirely trivial, since we need to find a way to neutralize
the component in the meantime. But it seems like performing all that
kind of operation at the "commit" phase (so, the request animation frame
callback) makes sense to me.
So, this commit modifies the code to add a new component status
(cancelled) and use it to cancel components that are waiting to be
destroyed. These components will then be destroyed as soon as the
requestanimation frame starts, before all other dom operations.
79 lines
2.2 KiB
TypeScript
79 lines
2.2 KiB
TypeScript
import type { ComponentNode } from "./component_node";
|
|
import type { Fiber } from "./fibers";
|
|
|
|
// Custom error class that wraps error that happen in the owl lifecycle
|
|
export class OwlError extends Error {
|
|
cause?: any;
|
|
}
|
|
|
|
// Maps fibers to thrown errors
|
|
export const fibersInError: WeakMap<Fiber, any> = new WeakMap();
|
|
export const nodeErrorHandlers: WeakMap<ComponentNode, ((error: any) => void)[]> = new WeakMap();
|
|
|
|
function _handleError(node: ComponentNode | null, error: any): boolean {
|
|
if (!node) {
|
|
return false;
|
|
}
|
|
const fiber = node.fiber;
|
|
if (fiber) {
|
|
fibersInError.set(fiber, error);
|
|
}
|
|
|
|
const errorHandlers = nodeErrorHandlers.get(node);
|
|
if (errorHandlers) {
|
|
let handled = false;
|
|
// execute in the opposite order
|
|
for (let i = errorHandlers.length - 1; i >= 0; i--) {
|
|
try {
|
|
errorHandlers[i](error);
|
|
handled = true;
|
|
break;
|
|
} catch (e) {
|
|
error = e;
|
|
}
|
|
}
|
|
|
|
if (handled) {
|
|
return true;
|
|
}
|
|
}
|
|
return _handleError(node.parent, error);
|
|
}
|
|
|
|
type ErrorParams = { error: any } & ({ node: ComponentNode } | { fiber: Fiber });
|
|
export function handleError(params: ErrorParams) {
|
|
let { error } = params;
|
|
// Wrap error if it wasn't wrapped by wrapError (ie when not in dev mode)
|
|
if (!(error instanceof OwlError)) {
|
|
error = Object.assign(
|
|
new OwlError(`An error occured in the owl lifecycle (see this Error's "cause" property)`),
|
|
{ cause: error }
|
|
);
|
|
}
|
|
const node = "node" in params ? params.node : params.fiber.node;
|
|
const fiber = "fiber" in params ? params.fiber : node.fiber;
|
|
|
|
if (fiber) {
|
|
// resets the fibers on components if possible. This is important so that
|
|
// new renderings can be properly included in the initial one, if any.
|
|
let current: Fiber | null = fiber;
|
|
do {
|
|
current.node.fiber = current;
|
|
current = current.parent;
|
|
} while (current);
|
|
|
|
fibersInError.set(fiber.root!, error);
|
|
}
|
|
|
|
const handled = _handleError(node, error);
|
|
if (!handled) {
|
|
console.warn(`[Owl] Unhandled error. Destroying the root component`);
|
|
try {
|
|
node.app.destroy();
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|