@ima/core
Version:
IMA.js framework for isomorphic javascript application
568 lines (566 loc) • 22.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "AbstractPageManager", {
enumerable: true,
get: function() {
return AbstractPageManager;
}
});
const _task = require("@esmj/task");
const _PageManager = require("./PageManager");
const _CancelError = require("../../error/CancelError");
const _RouterEvents = require("../../router/RouterEvents");
function createDeferred(resolveValue, rejectedValue) {
return (()=>{
let resolve, reject;
const promise = new Promise((res, rej)=>{
resolve = ()=>res(resolveValue);
reject = ()=>rej(rejectedValue);
});
return {
resolve: resolve,
reject: reject,
promise
};
})();
}
class AbstractPageManager extends _PageManager.PageManager {
/**
* Snapshot of the previously managed page before it was replaced with
* a new one
*/ _previousManagedPage = {};
/**
* Factory used by the page manager to create instances of the
* controller for the current route, and decorate the controllers and
* page state managers.
*/ _pageFactory;
/**
* Details of the currently managed page.
*/ _managedPage = {};
/**
* The current renderer of the page.
*/ _pageRenderer;
/**
* The current page state manager.
*/ _pageStateManager;
/**
* A registry that holds a list of pre-manage and post-manage handlers.
*/ _pageHandlerRegistry;
_dispatcher;
/**
* Initializes the page manager.
*
* @param pageFactory Factory used by the page manager to
* create instances of the controller for the current route, and
* decorate the controllers and page state managers.
* @param pageRenderer The current renderer of the page.
* @param pageStateManager The current page state
* manager.
* @param pageHandlerRegistry Instance of HandlerRegistry that
* holds a list of pre-manage and post-manage handlers.
*/ constructor(pageFactory, pageRenderer, pageStateManager, pageHandlerRegistry, dispatcher){
super();
this._pageFactory = pageFactory;
this._pageRenderer = pageRenderer;
this._pageStateManager = pageStateManager;
this._pageHandlerRegistry = pageHandlerRegistry;
this._dispatcher = dispatcher;
}
/**
* @inheritDoc
*/ init() {
this._managedPage = this._getInitialManagedPage();
this._previousManagedPage = this._getInitialManagedPage();
this._pageHandlerRegistry.init();
}
/**
* @inheritDoc
*/ async preManage() {
this._managedPage.state.cancelled = true;
this._previousManagedPage.state.abort?.reject();
return this._managedPage.state.page.promise;
}
/**
* @inheritDoc
*/ async manage({ route, options, params = {}, action = {} }) {
this._storeManagedPageSnapshot();
let controller, view;
const isControllerViewResolved = route.isControllerResolved() && route.isViewResolved();
try {
await (0, _task.autoYield)();
if (!isControllerViewResolved) {
this._dispatcher.fire(_RouterEvents.RouterEvents.BEFORE_LOADING_ASYNC_ROUTE, {
route
});
}
await (0, _task.autoYield)();
const data = await this.getViewController(route);
controller = data.controller;
view = data.view;
} catch (error) {
if (!(error instanceof _CancelError.CancelError)) {
throw error;
}
return {
status: 409
};
} finally{
if (!isControllerViewResolved) {
this._dispatcher.fire(_RouterEvents.RouterEvents.AFTER_LOADING_ASYNC_ROUTE, {
route
});
}
}
if (this._hasOnlyUpdate(controller, view, options) && this._managedPage.state.mounted) {
this._managedPage.params = params;
this._managedPage.state.cancelled = false;
this._managedPage.state.executed = false;
await this._runPreManageHandlers(this._managedPage, action);
const response = await this._updatePageSource();
await this._runPostManageHandlers(this._previousManagedPage, action);
return response;
}
// Construct new managedPage value
const controllerInstance = this._pageFactory.createController(controller, options);
const decoratedController = this._pageFactory.decorateController(controllerInstance);
// @ts-expect-error fixme in the future
const viewInstance = this._pageFactory.createView(view);
const actualManagedPage = this._managedPage;
this._managedPage = this._constructManagedPageValue(controller, view, route, options, params, controllerInstance, decoratedController, viewInstance);
// Run pre-manage handlers before affecting anything
await this._runPreManageHandlers(actualManagedPage, action);
// Deactivate the old instances and clearing state
await this._deactivatePageSource();
await this._destroyPageSource();
this._pageStateManager.clear();
this._clearComponentState(options);
// Initialize controllers and extensions
await this._initPageSource();
const response = await this._loadPageSource();
await this._runPostManageHandlers(this._previousManagedPage, action);
this._previousManagedPage = this._getInitialManagedPage();
return response;
}
postManage() {
setTimeout(()=>{
this._managedPage.state.page.resolve();
}, 0);
}
/**
* @inheritDoc
*/ async destroy() {
this._pageHandlerRegistry.destroy();
this._pageStateManager.onChange = undefined;
await this._deactivatePageSource();
await this._destroyPageSource();
this._pageStateManager.clear();
this._managedPage = this._getInitialManagedPage();
this._previousManagedPage = this._getInitialManagedPage();
}
_constructManagedPageValue(controller, view, route, options, params, controllerInstance, decoratedController, viewInstance) {
return {
controller,
controllerInstance,
decoratedController,
view,
viewInstance,
route,
options,
params,
state: {
activated: false,
initialized: false,
cancelled: false,
executed: false,
mounted: false,
page: createDeferred()
}
};
}
/**
* Creates a cloned version of currently managed page and stores it in
* a helper property.
* Snapshot is used in manager handlers to easily determine differences
* between the current and the previous state.
*/ _storeManagedPageSnapshot() {
this._previousManagedPage = {
...this._managedPage
};
/**
* Create new abort promise used for aborting raced promises
* in canceled handlers.
*/ this._previousManagedPage.state.abort = createDeferred(undefined, new _CancelError.CancelError());
/**
* Reseted managed state promise.
*/ this._managedPage.state.page = createDeferred();
}
/**
* Clear value from managed page.
*/ _getInitialManagedPage() {
return {
controller: undefined,
controllerInstance: undefined,
decoratedController: undefined,
view: undefined,
viewInstance: undefined,
route: undefined,
options: undefined,
params: undefined,
state: {
activated: false,
initialized: false,
cancelled: false,
executed: false,
mounted: false,
page: {
promise: Promise.resolve(),
reject: ()=>undefined,
resolve: ()=>undefined
}
}
};
}
/**
* Removes properties we do not want to propagate outside of the page manager
*
* @param value The managed page object to strip down
*/ _stripManagedPageValueForPublic(value) {
const { controller, view, route, options, params } = value;
return {
controller,
view,
route,
options,
params
};
}
/**
* Set page state manager to extension which has restricted rights to set
* global state.
*/ _setRestrictedPageStateManager(extension, extensionState) {
const stateKeys = Object.keys(extensionState);
const allowedKey = extension.getAllowedStateKeys();
const allAllowedStateKeys = stateKeys.concat(allowedKey);
const pageFactory = this._pageFactory;
const decoratedPageStateManager = pageFactory.decoratePageStateManager(this._pageStateManager, allAllowedStateKeys);
extension.setPageStateManager(decoratedPageStateManager);
}
/**
* For defined extension switches to pageStageManager and clears partial state
* after extension state is loaded.
*/ _switchToPageStateManagerAfterLoaded(extension, extensionState) {
const stateValues = Object.values(extensionState);
Promise.all(stateValues).then(()=>{
extension.switchToStateManager();
extension.clearPartialState();
}).catch(()=>{
extension.clearPartialState();
});
}
/**
* Initialize page source so call init method on controller and his
* extensions.
*/ async _initPageSource() {
try {
await this.#cancelable(this._initController());
await this.#cancelable(this._initExtensions());
this._managedPage.state.initialized = true;
} catch (error) {
if (!(error instanceof _CancelError.CancelError)) {
throw error;
}
}
}
/**
* Initializes managed instance of controller with the provided parameters.
*/ async _initController() {
if (this._managedPage.state.cancelled) {
throw new _CancelError.CancelError();
}
const controller = this._managedPage.controllerInstance;
controller.setRouteParams(this._managedPage.params);
await controller.init();
}
/**
* Initialize extensions for managed instance of controller with the
* provided parameters.
*/ async _initExtensions() {
const controller = this._managedPage.controllerInstance;
for (const extension of controller.getExtensions()){
if (this._managedPage.state.cancelled) {
throw new _CancelError.CancelError();
}
extension.setRouteParams(this._managedPage.params);
await extension.init();
}
}
/**
* Iterates over extensions of current controller and switches each one to
* pageStateManager and clears their partial state.
*/ _switchToPageStateManager() {
const controller = this._managedPage.controllerInstance;
for (const extension of controller.getExtensions()){
extension.switchToStateManager();
extension.clearPartialState();
}
}
/**
* Load page source so call load method on controller and his extensions.
* Merge loaded state and render it.
*/ async _loadPageSource() {
try {
const controllerState = await this.#cancelable(this._getLoadedControllerState());
const extensionsState = await this.#cancelable(this._getLoadedExtensionsState(controllerState));
const loadedPageState = Object.assign({}, extensionsState, controllerState);
if (this._managedPage.state.cancelled) {
throw new _CancelError.CancelError();
}
const response = await this.#cancelable(this._pageRenderer.mount(this._managedPage.decoratedController, this._managedPage.viewInstance, loadedPageState, this._managedPage.options));
this._managedPage.state.mounted = true;
return response;
} catch (error) {
if (error instanceof _CancelError.CancelError) {
return {
status: 409
};
}
throw error;
}
}
/**
* Load controller state from managed instance of controller.
*/ async _getLoadedControllerState() {
if (this._managedPage.state.cancelled) {
throw new _CancelError.CancelError();
}
const controller = this._managedPage.controllerInstance;
const controllerState = await controller.load();
controller.setPageStateManager(this._pageStateManager);
return controllerState;
}
/**
* Load extensions state from managed instance of controller.
*/ async _getLoadedExtensionsState(controllerState) {
const controller = this._managedPage.controllerInstance;
const extensionsState = Object.assign({}, controllerState);
for (const extension of controller.getExtensions()){
if (this._managedPage.state.cancelled) {
throw new _CancelError.CancelError();
}
extension.setPartialState(extensionsState);
extension.switchToPartialState();
const extensionState = await extension.load();
this._switchToPageStateManagerAfterLoaded(extension, extensionState);
this._setRestrictedPageStateManager(extension, extensionState);
Object.assign(extensionsState, extensionState);
}
return extensionsState;
}
/**
* Activate page source so call activate method on controller and his
* extensions.
*/ async _activatePageSource() {
try {
const controller = this._managedPage.controllerInstance;
const isNotActivated = !this._managedPage.state.activated;
if (controller && isNotActivated) {
await this._activateController();
await this._activateExtensions();
this._managedPage.state.activated = true;
}
} catch (error) {
if (!(error instanceof _CancelError.CancelError)) {
throw error;
}
}
}
/**
* Activate managed instance of controller.
*/ async _activateController() {
if (this._managedPage.state.cancelled) {
throw new _CancelError.CancelError();
}
const controller = this._managedPage.controllerInstance;
await (0, _task.autoYield)();
await controller.activate();
}
/**
* Activate extensions for managed instance of controller.
*/ async _activateExtensions() {
const controller = this._managedPage.controllerInstance;
for (const extension of controller.getExtensions()){
if (this._managedPage.state.cancelled) {
throw new _CancelError.CancelError();
}
await (0, _task.autoYield)();
await extension.activate();
}
}
/**
* Update page source so call update method on controller and his
* extensions. Merge updated state and render it.
*/ async _updatePageSource() {
try {
const updatedControllerState = await this.#cancelable(this._getUpdatedControllerState());
const updatedExtensionState = await this.#cancelable(this._getUpdatedExtensionsState(updatedControllerState));
const updatedPageState = Object.assign({}, updatedExtensionState, updatedControllerState);
if (this._managedPage.state.cancelled) {
throw new _CancelError.CancelError();
}
const response = await this.#cancelable(this._pageRenderer.update(this._managedPage.decoratedController, this._managedPage.viewInstance, updatedPageState, this._managedPage.options));
return response;
} catch (error) {
if (error instanceof _CancelError.CancelError) {
return {
status: 409
};
}
throw error;
}
}
/**
* Return updated controller state for current page controller.
*/ _getUpdatedControllerState() {
if (this._managedPage.state.cancelled) {
throw new _CancelError.CancelError();
}
const controller = this._managedPage.controllerInstance;
const lastRouteParams = controller.getRouteParams();
controller.setRouteParams(this._managedPage.params);
return controller.update(lastRouteParams);
}
/**
* Return updated extensions state for current page controller.
*/ async _getUpdatedExtensionsState(controllerState) {
const controller = this._managedPage.controllerInstance;
const extensionsState = Object.assign({}, controllerState);
const extensionsPartialState = Object.assign({}, this._pageStateManager.getState(), controllerState);
for (const extension of controller.getExtensions()){
if (this._managedPage.state.cancelled) {
throw new _CancelError.CancelError();
}
const lastRouteParams = extension.getRouteParams();
extension.setRouteParams(this._managedPage.params);
extension.setPartialState(extensionsPartialState);
extension.switchToPartialState();
const extensionReturnedState = await extension.update(lastRouteParams);
this._switchToPageStateManagerAfterLoaded(extension, extensionReturnedState);
this._setRestrictedPageStateManager(extension, extensionReturnedState);
Object.assign(extensionsState, extensionReturnedState);
Object.assign(extensionsPartialState, extensionReturnedState);
}
return extensionsState;
}
/**
* Deactivate page source so call deactivate method on controller and his
* extensions.
*/ async _deactivatePageSource() {
const controller = this._previousManagedPage.controllerInstance;
const isActivated = this._previousManagedPage.state.activated;
if (controller && isActivated) {
await this._deactivateExtensions();
await this._deactivateController();
}
}
/**
* Deactivate last managed instance of controller only If controller was
* activated.
*/ async _deactivateController() {
const controller = this._previousManagedPage.controllerInstance;
await controller.deactivate();
}
/**
* Deactivate extensions for last managed instance of controller only if
* they were activated.
*/ async _deactivateExtensions() {
const controller = this._previousManagedPage.controllerInstance;
for (const extension of controller.getExtensions()){
await extension.deactivate();
}
}
/**
* Destroy page source so call destroy method on controller and his
* extensions.
*/ async _destroyPageSource() {
const controller = this._previousManagedPage.controllerInstance;
if (controller && this._previousManagedPage.state.initialized) {
await this._destroyExtensions();
await this._destroyController();
}
}
/**
* Destroy last managed instance of controller.
*/ async _destroyController() {
const controller = this._previousManagedPage.controllerInstance;
await controller.destroy();
controller.setPageStateManager();
}
/**
* Destroy extensions for last managed instance of controller.
*
* @protected
* @return {Promise<undefined>}
*/ async _destroyExtensions() {
const controller = this._previousManagedPage.controllerInstance;
for (const extension of controller.getExtensions()){
await extension.destroy();
extension.setPageStateManager();
}
}
/**
* The method clear state on current rendered component to DOM.
*
* @param options The current route options.
*/ _clearComponentState(options) {
const managedOptions = this._previousManagedPage.options;
if (!managedOptions || managedOptions.documentView !== options.documentView || managedOptions.managedRootView !== options.managedRootView || managedOptions.viewAdapter !== options.viewAdapter) {
this._pageRenderer.unmount();
}
}
/**
* Return true if manager has to update last managed controller and view.
*/ _hasOnlyUpdate(controller, view, options) {
if (typeof options.onlyUpdate === 'function') {
return options.onlyUpdate(this._managedPage.controller, this._managedPage.view);
}
return !!(options.onlyUpdate && this._managedPage.controller === controller && this._managedPage.view === view);
}
async _runPreManageHandlers(actualManagedPage, action) {
await (0, _task.autoYield)();
const result = this._pageHandlerRegistry.handlePreManagedState(actualManagedPage.controller ? this._stripManagedPageValueForPublic(actualManagedPage) : null, this._stripManagedPageValueForPublic(this._managedPage) || null, action);
this._managedPage.state.executed = true;
return result;
}
async _runPostManageHandlers(previousManagedPage, action) {
// Has to be called for first managed page too (previous is empty)
if (previousManagedPage.controller && !previousManagedPage.state.executed) {
previousManagedPage.state.executed = false;
return;
}
await (0, _task.autoYield)();
return this._pageHandlerRegistry.handlePostManagedState(this._managedPage.controller ? this._stripManagedPageValueForPublic(this._managedPage) : null, this._stripManagedPageValueForPublic(previousManagedPage) || null, action);
}
async getViewController(route) {
// @ts-expect-error ignore state.abort.promise value
const [controller, view] = await Promise.race([
this._previousManagedPage.state.abort?.promise,
Promise.all([
route.getController(),
route.getView()
])
]);
return {
controller,
view
};
}
#cancelable(promise) {
return (0, _task.autoYield)().then(()=>Promise.race([
this._previousManagedPage.state.abort?.promise,
promise
]));
}
}
//# sourceMappingURL=AbstractPageManager.js.map