From 20c6cacb4eabd4b3ecf25b899911825be24a6db3 Mon Sep 17 00:00:00 2001
From: Romeo Fragomeli <rfr@odoo.com>
Date: Mon, 30 Sep 2024 10:49:39 +0200
Subject: [PATCH] [REL] v2.4.0

# v2.4.0

 - [IMP] owl: add basic support for sub roots
 - [IMP] make set of timeout-able hooks (and their timeouts) clearer by using a const map
 - [IMP] devtools: add support for file urls on chrome
---
 docs/owl.js       | 107 +++++++++++++++++++++++++++++++---------------
 package-lock.json |   2 +-
 package.json      |   2 +-
 src/version.ts    |   2 +-
 4 files changed, 75 insertions(+), 38 deletions(-)

diff --git a/docs/owl.js b/docs/owl.js
index 3e667837c..738aa71bd 100644
--- a/docs/owl.js
+++ b/docs/owl.js
@@ -2598,42 +2598,47 @@ class ComponentNode {
 }
 
 const TIMEOUT = Symbol("timeout");
+const HOOK_TIMEOUT = {
+    onWillStart: 3000,
+    onWillUpdateProps: 3000,
+};
 function wrapError(fn, hookName) {
-    const error = new OwlError(`The following error occurred in ${hookName}: `);
-    const timeoutError = new OwlError(`${hookName}'s promise hasn't resolved after 3 seconds`);
+    const error = new OwlError();
+    const timeoutError = new OwlError();
     const node = getCurrent();
     return (...args) => {
         const onError = (cause) => {
             error.cause = cause;
-            if (cause instanceof Error) {
-                error.message += `"${cause.message}"`;
-            }
-            else {
-                error.message = `Something that is not an Error was thrown in ${hookName} (see this Error's "cause" property)`;
-            }
+            error.message =
+                cause instanceof Error
+                    ? `The following error occurred in ${hookName}: "${cause.message}"`
+                    : `Something that is not an Error was thrown in ${hookName} (see this Error's "cause" property)`;
             throw error;
         };
+        let result;
         try {
-            const result = fn(...args);
-            if (result instanceof Promise) {
-                if (hookName === "onWillStart" || hookName === "onWillUpdateProps") {
-                    const fiber = node.fiber;
-                    Promise.race([
-                        result.catch(() => { }),
-                        new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
-                    ]).then((res) => {
-                        if (res === TIMEOUT && node.fiber === fiber && node.status <= 2) {
-                            console.log(timeoutError);
-                        }
-                    });
-                }
-                return result.catch(onError);
-            }
-            return result;
+            result = fn(...args);
         }
         catch (cause) {
             onError(cause);
         }
+        if (!(result instanceof Promise)) {
+            return result;
+        }
+        const timeout = HOOK_TIMEOUT[hookName];
+        if (timeout) {
+            const fiber = node.fiber;
+            Promise.race([
+                result.catch(() => { }),
+                new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), timeout)),
+            ]).then((res) => {
+                if (res === TIMEOUT && node.fiber === fiber && node.status <= 2) {
+                    timeoutError.message = `${hookName}'s promise hasn't resolved after ${timeout / 1000} seconds`;
+                    console.log(timeoutError);
+                }
+            });
+        }
+        return result.catch(onError);
     };
 }
 // -----------------------------------------------------------------------------
@@ -5552,7 +5557,7 @@ function compile(template, options = {}) {
 }
 
 // do not modify manually. This file is generated by the release script.
-const version = "2.3.1";
+const version = "2.4.0";
 
 // -----------------------------------------------------------------------------
 //  Scheduler
@@ -5647,6 +5652,7 @@ class App extends TemplateSet {
     constructor(Root, config = {}) {
         super(config);
         this.scheduler = new Scheduler();
+        this.subRoots = new Set();
         this.root = null;
         this.name = config.name || "";
         this.Root = Root;
@@ -5665,14 +5671,42 @@ class App extends TemplateSet {
         this.props = config.props || {};
     }
     mount(target, options) {
-        App.validateTarget(target);
-        if (this.dev) {
-            validateProps(this.Root, this.props, { __owl__: { app: this } });
-        }
-        const node = this.makeNode(this.Root, this.props);
-        const prom = this.mountNode(node, target, options);
-        this.root = node;
-        return prom;
+        const root = this.createRoot(this.Root, { props: this.props });
+        this.root = root.node;
+        this.subRoots.delete(root.node);
+        return root.mount(target, options);
+    }
+    createRoot(Root, config = {}) {
+        const props = config.props || {};
+        // hack to make sure the sub root get the sub env if necessary. for owl 3,
+        // would be nice to rethink the initialization process to make sure that
+        // we can create a ComponentNode and give it explicitely the env, instead
+        // of looking it up in the app
+        const env = this.env;
+        if (config.env) {
+            this.env = config.env;
+        }
+        const node = this.makeNode(Root, props);
+        if (config.env) {
+            this.env = env;
+        }
+        this.subRoots.add(node);
+        return {
+            node,
+            mount: (target, options) => {
+                App.validateTarget(target);
+                if (this.dev) {
+                    validateProps(Root, props, { __owl__: { app: this } });
+                }
+                const prom = this.mountNode(node, target, options);
+                return prom;
+            },
+            destroy: () => {
+                this.subRoots.delete(node);
+                node.destroy();
+                this.scheduler.processTasks();
+            },
+        };
     }
     makeNode(Component, props) {
         return new ComponentNode(Component, props, this, null, null);
@@ -5704,6 +5738,9 @@ class App extends TemplateSet {
     }
     destroy() {
         if (this.root) {
+            for (let subroot of this.subRoots) {
+                subroot.destroy();
+            }
             this.root.destroy();
             this.scheduler.processTasks();
         }
@@ -5981,6 +6018,6 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(name, templat
 export { App, Component, EventBus, OwlError, __info__, batched, blockDom, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml };
 
 
-__info__.date = '2024-08-14T14:25:23.038Z';
-__info__.hash = '9c2d957';
+__info__.date = '2024-09-30T08:49:29.420Z';
+__info__.hash = 'eb2b32a';
 __info__.url = 'https://github.com/odoo/owl';
diff --git a/package-lock.json b/package-lock.json
index d53033129..da89315f7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "@odoo/owl",
-  "version": "2.3.1",
+  "version": "2.4.0",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
diff --git a/package.json b/package.json
index 686ccdcf7..3a4bc45f4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@odoo/owl",
-  "version": "2.3.1",
+  "version": "2.4.0",
   "description": "Odoo Web Library (OWL)",
   "main": "dist/owl.cjs.js",
   "module": "dist/owl.es.js",
diff --git a/src/version.ts b/src/version.ts
index c11a9375c..3b1ce5fe2 100644
--- a/src/version.ts
+++ b/src/version.ts
@@ -1,2 +1,2 @@
 // do not modify manually. This file is generated by the release script.
-export const version = "2.3.1";
+export const version = "2.4.0";