207 lines
6.7 KiB
JavaScript
207 lines
6.7 KiB
JavaScript
/** @odoo-module **/
|
|
|
|
import { registry } from "./core/registry";
|
|
import { templates } from "./core/assets";
|
|
import { App, EventBus } from "@odoo/owl";
|
|
import { _t } from "@web/core/l10n/translation";
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Types
|
|
// -----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* @typedef {Object} OdooEnv
|
|
* @property {import("services").Services} services
|
|
* @property {EventBus} bus
|
|
* @property {string} debug
|
|
* @property {(str: string) => string} _t
|
|
* @property {boolean} [isSmall]
|
|
*/
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// makeEnv
|
|
// -----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Return a value Odoo Env object
|
|
*
|
|
* @returns {OdooEnv}
|
|
*/
|
|
export function makeEnv() {
|
|
return {
|
|
bus: new EventBus(),
|
|
services: {},
|
|
debug: odoo.debug,
|
|
get isSmall() {
|
|
throw new Error("UI service not initialized!");
|
|
},
|
|
};
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Service Launcher
|
|
// -----------------------------------------------------------------------------
|
|
|
|
const serviceRegistry = registry.category("services");
|
|
|
|
export const SERVICES_METADATA = {};
|
|
let startServicesPromise = null;
|
|
|
|
/**
|
|
* Start all services registered in the service registry, while making sure
|
|
* each service dependencies are properly fulfilled.
|
|
*
|
|
* @param {OdooEnv} env
|
|
* @returns {Promise<void>}
|
|
*/
|
|
export async function startServices(env) {
|
|
// Wait for all synchronous code so that if new services that depend on
|
|
// one another are added to the registry, they're all present before we
|
|
// start them regardless of the order they're added to the registry.
|
|
await Promise.resolve();
|
|
|
|
const toStart = new Set();
|
|
serviceRegistry.addEventListener("UPDATE", async (ev) => {
|
|
// Wait for all synchronous code so that if new services that depend on
|
|
// one another are added to the registry, they're all present before we
|
|
// start them regardless of the order they're added to the registry.
|
|
await Promise.resolve();
|
|
const { operation, key: name, value: service } = ev.detail;
|
|
if (operation === "delete") {
|
|
// We hardly see why it would be usefull to remove a service.
|
|
// Furthermore we could encounter problems with dependencies.
|
|
// Keep it simple!
|
|
return;
|
|
}
|
|
if (toStart.size) {
|
|
const namedService = Object.assign(Object.create(service), { name });
|
|
toStart.add(namedService);
|
|
} else {
|
|
await _startServices(env, toStart);
|
|
}
|
|
});
|
|
await _startServices(env, toStart);
|
|
}
|
|
|
|
async function _startServices(env, toStart) {
|
|
if (startServicesPromise) {
|
|
return startServicesPromise.then(() => _startServices(env, toStart));
|
|
}
|
|
const services = env.services;
|
|
for (const [name, service] of serviceRegistry.getEntries()) {
|
|
if (!(name in services)) {
|
|
const namedService = Object.assign(Object.create(service), { name });
|
|
toStart.add(namedService);
|
|
}
|
|
}
|
|
|
|
// start as many services in parallel as possible
|
|
async function start() {
|
|
let service = null;
|
|
const proms = [];
|
|
while ((service = findNext())) {
|
|
const name = service.name;
|
|
toStart.delete(service);
|
|
const entries = (service.dependencies || []).map((dep) => [dep, services[dep]]);
|
|
const dependencies = Object.fromEntries(entries);
|
|
let value;
|
|
try {
|
|
value = service.start(env, dependencies);
|
|
} catch (e) {
|
|
value = e;
|
|
console.error(e);
|
|
}
|
|
if ("async" in service) {
|
|
SERVICES_METADATA[name] = service.async;
|
|
}
|
|
if (value instanceof Promise) {
|
|
proms.push(
|
|
new Promise((resolve) => {
|
|
value
|
|
.then((val) => {
|
|
services[name] = val || null;
|
|
})
|
|
.catch((error) => {
|
|
services[name] = error;
|
|
console.error("Can't load service '" + name + "' because:", error);
|
|
})
|
|
.finally(resolve);
|
|
})
|
|
);
|
|
} else {
|
|
services[service.name] = value || null;
|
|
}
|
|
}
|
|
await Promise.all(proms);
|
|
if (proms.length) {
|
|
return start();
|
|
}
|
|
}
|
|
startServicesPromise = start();
|
|
await startServicesPromise;
|
|
startServicesPromise = null;
|
|
if (toStart.size) {
|
|
const names = [...toStart].map((s) => s.name);
|
|
const missingDeps = new Set();
|
|
[...toStart].forEach((s) =>
|
|
s.dependencies.forEach((dep) => {
|
|
if (!(dep in services) && !names.includes(dep)) {
|
|
missingDeps.add(dep);
|
|
}
|
|
})
|
|
);
|
|
const depNames = [...missingDeps].join(", ");
|
|
throw new Error(
|
|
`Some services could not be started: ${names}. Missing dependencies: ${depNames}`
|
|
);
|
|
}
|
|
|
|
function findNext() {
|
|
for (const s of toStart) {
|
|
if (s.dependencies) {
|
|
if (s.dependencies.every((d) => d in services)) {
|
|
return s;
|
|
}
|
|
} else {
|
|
return s;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create an application with a given component as root and mount it. If no env
|
|
* is provided, the application will be treated as a "root": an env will be
|
|
* created and the services will be started, it will also be set as the root
|
|
* in `__WOWL_DEBUG__`
|
|
*
|
|
* @param {import("@odoo/owl").Component} component the component to mount
|
|
* @param {HTMLElement} target the HTML element in which to mount the app
|
|
* @param {Partial<ConstructorParameters<typeof App>[1]>} [appConfig] object
|
|
* containing a (partial) config for the app.
|
|
*/
|
|
export async function mountComponent(component, target, appConfig = {}) {
|
|
let { env } = appConfig;
|
|
const isRoot = !env;
|
|
if (isRoot) {
|
|
env = await makeEnv();
|
|
await startServices(env);
|
|
}
|
|
const app = new App(component, {
|
|
env,
|
|
templates,
|
|
dev: env.debug,
|
|
warnIfNoStaticProps: true,
|
|
name: component.constructor.name,
|
|
translatableAttributes: ["data-tooltip"],
|
|
translateFn: _t,
|
|
...appConfig,
|
|
});
|
|
const root = await app.mount(target);
|
|
if (isRoot) {
|
|
odoo.__WOWL_DEBUG__ = { root };
|
|
}
|
|
return app;
|
|
}
|