Files
owl/src/runtime/fibers.ts
T
Julien Carion (juca) 9b1ec5dc0e [FIX] *: move OwlError to common
This commit moves OwlError to its own file in the new common folder
such that it is no longer associated with the runtime.
2023-07-26 15:15:20 +02:00

266 lines
7.6 KiB
TypeScript

import { BDom, mount } from "./blockdom";
import type { ComponentNode } from "./component_node";
import { fibersInError } from "./error_handling";
import { OwlError } from "../common/owl_error";
import { STATUS } from "./status";
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
let current = node.fiber;
if (current) {
cancelFibers(current.children);
current.root = null;
}
return new Fiber(node, parent);
}
export function makeRootFiber(node: ComponentNode): Fiber {
let current = node.fiber;
if (current) {
let root = current.root!;
// lock root fiber because canceling children fibers may destroy components,
// which means any arbitrary code can be run in onWillDestroy, which may
// trigger new renderings
root.locked = true;
root.setCounter(root.counter + 1 - cancelFibers(current.children));
root.locked = false;
current.children = [];
current.childrenMap = {};
current.bdom = null;
if (fibersInError.has(current)) {
fibersInError.delete(current);
fibersInError.delete(root);
current.appliedToDom = false;
}
return current;
}
const fiber = new RootFiber(node, null);
if (node.willPatch.length) {
fiber.willPatch.push(fiber);
}
if (node.patched.length) {
fiber.patched.push(fiber);
}
return fiber;
}
function throwOnRender() {
throw new OwlError("Attempted to render cancelled fiber");
}
/**
* @returns number of not-yet rendered fibers cancelled
*/
function cancelFibers(fibers: Fiber[]): number {
let result = 0;
for (let fiber of fibers) {
let node = fiber.node;
fiber.render = throwOnRender;
if (node.status === STATUS.NEW) {
node.cancel();
}
node.fiber = null;
if (fiber.bdom) {
// if fiber has been rendered, this means that the component props have
// been updated. however, this fiber will not be patched to the dom, so
// it could happen that the next render compare the current props with
// the same props, and skip the render completely. With the next line,
// we kindly request the component code to force a render, so it works as
// expected.
node.forceNextRender = true;
} else {
result++;
}
result += cancelFibers(fiber.children);
}
return result;
}
export class Fiber {
node: ComponentNode;
bdom: BDom | null = null;
root: RootFiber | null; // A Fiber that has been replaced by another has no root
parent: Fiber | null;
children: Fiber[] = [];
appliedToDom = false;
deep: boolean = false;
childrenMap: ComponentNode["children"] = {};
constructor(node: ComponentNode, parent: Fiber | null) {
this.node = node;
this.parent = parent;
if (parent) {
this.deep = parent.deep;
const root = parent.root!;
root.setCounter(root.counter + 1);
this.root = root;
parent.children.push(this);
} else {
this.root = this as any;
}
}
render() {
// if some parent has a fiber => register in followup
let prev = this.root!.node;
let scheduler = prev.app.scheduler;
let current = prev.parent;
while (current) {
if (current.fiber) {
let root = current.fiber.root!;
if (root.counter === 0 && prev.parentKey! in current.fiber.childrenMap) {
current = root.node;
} else {
scheduler.delayedRenders.push(this);
return;
}
}
prev = current;
current = current.parent;
}
// there are no current rendering from above => we can render
this._render();
}
_render() {
const node = this.node;
const root = this.root;
if (root) {
try {
(this.bdom as any) = true;
this.bdom = node.renderFn();
} catch (e) {
node.app.handleError({ node, error: e });
}
root.setCounter(root.counter - 1);
}
}
}
export class RootFiber extends Fiber {
counter: number = 1;
// only add stuff in this if they have registered some hooks
willPatch: Fiber[] = [];
patched: Fiber[] = [];
mounted: Fiber[] = [];
// A fiber is typically locked when it is completing and the patch has not, or is being applied.
// i.e.: render triggered in onWillUnmount or in willPatch will be delayed
locked: boolean = false;
complete() {
const node = this.node;
this.locked = true;
let current: Fiber | undefined = undefined;
try {
// Step 1: calling all willPatch lifecycle hooks
for (current of this.willPatch) {
// because of the asynchronous nature of the rendering, some parts of the
// UI may have been rendered, then deleted in a followup rendering, and we
// do not want to call onWillPatch in that case.
let node = current.node;
if (node.fiber === current) {
const component = node.component;
for (let cb of node.willPatch) {
cb.call(component);
}
}
}
current = undefined;
// Step 2: patching the dom
node._patch();
this.locked = false;
// Step 4: calling all mounted lifecycle hooks
let mountedFibers = this.mounted;
while ((current = mountedFibers.pop())) {
current = current;
if (current.appliedToDom) {
for (let cb of current.node.mounted) {
cb();
}
}
}
// Step 5: calling all patched hooks
let patchedFibers = this.patched;
while ((current = patchedFibers.pop())) {
current = current;
if (current.appliedToDom) {
for (let cb of current.node.patched) {
cb();
}
}
}
} catch (e) {
this.locked = false;
node.app.handleError({ fiber: current || this, error: e });
}
}
setCounter(newValue: number) {
this.counter = newValue;
if (newValue === 0) {
this.node.app.scheduler.flush();
}
}
}
type Position = "first-child" | "last-child";
export interface MountOptions {
position?: Position;
}
export class MountFiber extends RootFiber {
target: HTMLElement;
position: Position;
constructor(node: ComponentNode, target: HTMLElement, options: MountOptions = {}) {
super(node, null);
this.target = target;
this.position = options.position || "last-child";
}
complete() {
let current: Fiber | undefined = this;
try {
const node = this.node;
node.children = this.childrenMap;
(node.app.constructor as any).validateTarget(this.target);
if (node.bdom) {
// this is a complicated situation: if we mount a fiber with an existing
// bdom, this means that this same fiber was already completed, mounted,
// but a crash occurred in some mounted hook. Then, it was handled and
// the new rendering is being applied.
node.updateDom();
} else {
node.bdom = this.bdom;
if (this.position === "last-child" || this.target.childNodes.length === 0) {
mount(node.bdom!, this.target);
} else {
const firstChild = this.target.childNodes[0];
mount(node.bdom!, this.target, firstChild);
}
}
// unregistering the fiber before mounted since it can do another render
// and that the current rendering is obviously completed
node.fiber = null;
node.status = STATUS.MOUNTED;
this.appliedToDom = true;
let mountedFibers = this.mounted;
while ((current = mountedFibers.pop())) {
if (current.appliedToDom) {
for (let cb of current.node.mounted) {
cb();
}
}
}
} catch (e) {
this.node.app.handleError({ fiber: current as Fiber, error: e });
}
}
}