//@ts-check
import AppChildFragment from "../AppChildFragment";
import AppMainFragment from "../AppMainFragment";
import AppFragmentBuilder from "./AppFragmentBuilder";
/**
* @template {import("AppFragmentBuilder").ForFragment} F
*/
class AsyncAppFragmentBuilder extends AppFragmentBuilder{
/**
* @param {import("AppFragmentBuilder").AppShellAsyncFragmentConstructorArgs<F>} constructorArgs
*/
constructor(constructorArgs){
//@ts-expect-error childFragmentId missing in constructorArgs
super(null, constructorArgs);
if(!constructorArgs.asyncOptions.forFragment){
throw new Error("You must specify forFragment property in AsyncAppFragmentBuilder.");
}
/**
* @type {AppShellAsyncMainFragment<F> | AppShellAsyncChildFragment<F>}
*/
this.fragment = constructorArgs.asyncOptions.forFragment === "main" ? AppShellAsyncMainFragment : AppShellAsyncChildFragment;
}
/**
*
* @param {MainRouter} mainRouterInstance
* @returns
*/
buildFragment(mainRouterInstance){
!this.constructorArgs.mainRouter ? this.constructorArgs.mainRouter = mainRouterInstance : null;
/**
* @type { import("AppFragmentBuilder").AppShellAsyncMainFragmentConstructor | import("AppFragmentBuilder").AppShellAsyncChildFragmentConstructor }
*/
const fragment = this.fragment;
return new fragment({ ...this.constructorArgs, asyncBuilderReference: this });
}
/**
*
* @param {AppMainFragmentConstructor | AppChildFragmentConstructor} fragment
*/
getLoadedFragmentClass(fragment){
this.fragment = fragment;
}
}
if(false){
/**
* @type {import("AppFragmentBuilder").AsyncAppFragmentBuilderConstructor}
*/
const check = AsyncAppFragmentBuilder;
}
/**
* Have an AsyncShellAppFragment implementation here. Pass in loading UI, and error UI.
*
* Automatically forwards calls. So, uninheritable to maintain behavior
*
* Provision for loadingUI, errorUI with retry button labelled "async-reload". Automatically connected
* to retry loading.
*
* So, create shellfragment. Override its builder. Then, do import, show proper uis, and on success, pipe call forward.
* Also, forward pipe the rest so remains shell
*
* Also, not passing fragment but path to it, which will be used for local loading. Have that import() bit done in a callback
* returning it, so can use promise internally, and have webpack correctly resolve the modules
*/
/**
* @template {import("AppFragmentBuilder").ForFragment} F_M
*/
class AppShellAsyncMainFragment extends AppMainFragment {
/**
*
* @param {import("AppFragmentBuilder").AppShellAsyncFragmentConstructorArgs<F_M> & { asyncBuilderReference: import("AppFragmentBuilder").AsyncAppFragmentBuilderInstance }} args
*/
constructor(args){
super(args);
/**
* @type {import("AppFragmentBuilder").AsyncShellLocalPipelineWorker}
*/
//@ts-expect-error //not extending cause no need really. Can make a pass with that
this.localPipelineWorker = new AsyncShellLocalPipelineWorker(this);
this.loadedFragmentConstructorArgs = args;
this.asyncBuilderReference = args.asyncBuilderReference;
this.routeCancelled = false;
}
/**
* Loads the async fragment and pipes control to it
*
* @param {asyncLoadCb} cb
*/
loadAsyncFragment(cb){
globalLoadAsyncFragment.call(this, cb, "main");
}
}
/**
* @template {import("AppFragmentBuilder").ForFragment} F_C
*/
class AppShellAsyncChildFragment extends AppChildFragment {
/**
*
* @param {import("AppFragmentBuilder").AppShellAsyncFragmentConstructorArgs<F_C> & { asyncBuilderReference: import("AppFragmentBuilder").AsyncAppFragmentBuilderInstance }} args
*/
constructor(args){
//@ts-expect-error
super(args);
/**
* @type {import("AppFragmentBuilder").AsyncShellLocalPipelineWorker}
*/
//@ts-expect-error //not extending cause no need really. Can make a pass with that
this.localPipelineWorker = new AsyncShellLocalPipelineWorker(this);
this.loadedFragmentConstructorArgs = args;
//Used to pass new fragment class to builder
this.asyncBuilderReference = args.asyncBuilderReference;
this.routeCancelled = false;
}
/**
* Loads the async fragment and pipes control to it
*
* @typedef {(fragInstance: AppMainFragmentInstance | AppChildFragmentInstance, fragClass: AppMainFragmentConstructor | AppChildFragmentConstructor) => void} asyncLoadCb
*
* @param {asyncLoadCb} cb
*/
loadAsyncFragment(cb){
globalLoadAsyncFragment.call(this, cb, "child");
}
}
/**
* Loads the async fragment and pipes control to it
* @template {import("AppFragmentBuilder").ForFragment} F_fn
* @this {AppShellAsyncMainFragment | AppShellAsyncChildFragment}
* @param {asyncLoadCb} cb
* @param {import("AppFragmentBuilder").AppShellAsyncFragmentConstructorArgs<F_fn>['asyncOptions']['forFragment']} forFragment
*/
async function globalLoadAsyncFragment(cb, forFragment){
let showingLoadingUI = false;
const _Inserted_Error_UI_Id = "async-load-err";
let insertedErrorUI = false;
const showLoadingUI = () => {
if(!this.routeCancelled){
if(!this.isViewInitialized()){ //Loading UI shown ONLY when view had not been initialized, so not server side
this.bindNewUIToDOM(this.loadedFragmentConstructorArgs.asyncOptions.asyncLoadingUI);
showingLoadingUI = true;
}
//Can override bind here to always show ui for testing. But DON'T recommend
}
}
const removeLoadingUI = () => {
if(!this.routeCancelled){
if(showingLoadingUI){
this.detachViewFromDOM();
showingLoadingUI = false;
}
}
}
const showErrorUI = () => {
if(!this.routeCancelled){
if(!showingLoadingUI && !this.isViewInitialized()){ //shows directly as main ui only if view is not initialized. Else, inserts self
this.bindNewUIToDOM(this.loadedFragmentConstructorArgs.asyncOptions.asyncErrorUI);
} else {
//if view was server-side rendered
//Thinking of a notification pop-up? Or inserting ui afterbegin? Yeap.
//Combine with a notif. Or, since user can scroll, do a notif card.
//Pass notif options for this. For now, it's afterbegin
/**
* @type {Element}
*/
const node = forFragment === "main" ? this.getMainFragmentComponent() :
//@ts-expect-error
this.getChildFragmentComponent();
node.insertAdjacentHTML("afterbegin", `<div id="${_Inserted_Error_UI_Id}">${this.loadedFragmentConstructorArgs.asyncOptions.asyncErrorUI}</div>`);
insertedErrorUI = true;
}
}
}
const removeErrorUI = () => {
if(!this.routeCancelled){
if(insertedErrorUI){
/**
* @type {Element}
*/
const node = forFragment === "main" ? this.getMainFragmentComponent() :
//@ts-expect-error
this.getChildFragmentComponent();
const ui = node.querySelector(`#${_Inserted_Error_UI_Id}`);
node.removeChild(ui);
insertedErrorUI = false;
} else {
this.detachViewFromDOM();
}
}
}
try {
//Show loading UI
//do only if view not intialized
showLoadingUI();
const loadedFragment = (await this.loadedFragmentConstructorArgs.asyncOptions.importCb()).default;
//Remove loading UI
removeLoadingUI();
//putting in own try-catch to avoid catching errors involving main frag now
try{
// Can comment here to test uis. But don't recommend. Just place in p.hbs file, then copy to actual module once satisfied
cb(new loadedFragment(this.loadedFragmentConstructorArgs), loadedFragment);
} catch(err){
console.error(err);
console.warn("Probably an error in loaded fragment. Check");
}
} catch(err){
console.error(err);
//Remove loading UI
removeLoadingUI();
//Show error UI
showErrorUI();
//Bind retry button
const retryBtn = this.getMainFragmentComponent().getElementsByClassName("async-reload")[0];
if(!retryBtn){
console.warn("Retry button not bound. Cannot retry to load async fragment. Possibly route was cancelled? -> " + this.routeCancelled);
} else {
retryBtn.addEventListener("click", (e) => {
removeErrorUI();
this.loadAsyncFragment(cb);
});
}
}
}
class AsyncShellLocalPipelineWorker{
/**
*
* @param {AppShellAsyncMainFragment | AppShellAsyncChildFragment} host
*/
constructor(host){
this.host = host;
/**
* @type {AppMainFragmentInstance | AppChildFragmentInstance}
*/
this.loadedFragment = null;
}
/**
* Called by MainRoutingPipeline to build the fragment route
*
* Uses localBuildingState to see if route already built and thus only fire param changes check and move on
*
* FORWARD PIPING AFTER LOAD
*
* @param {RouteParams} routeParams
* @param {SavedFragmentState} savedState
* @param {{}} data
* @param {genericFunction} cb
*/
buildFragmentRoute(routeParams, savedState, data, cb){
//for cases where after cancel, this was still part of valid ones - SEE WHY WE WORK WITH PIPELINES. DAMMIT
//Can work with one here? Probably
this.host.routeCancelled = false;
const forwardPipeCall = () => {
this.loadedFragment.localPipelineWorker.buildFragmentRoute(routeParams, savedState, data, cb);
}
//Have to do this for async loaded then param query changed. Was causing multiple constructor firings
//will no longer do this once the referenced appFragment is not the shell we used to async loading on page/fragment load
if(this.loadedFragment){
forwardPipeCall();
} else {
//Load in the actual frag, then pipe the call forward
this.host.loadAsyncFragment((loadedFrag, fragClass) => {
this.loadedFragment = loadedFrag;
//Now, ensure builder always has correct constructor
this.host.asyncBuilderReference.getLoadedFragmentClass(fragClass);
if(!this.host.routeCancelled){
forwardPipeCall();
}
});
}
}
/**
* ONLY extra forward pipe that will be needed, cause this happens while building
* Consent is ONLY after successful full build
* @param {genericFunction} cb
*/
cancelFragmentRoute(cb){
//Tell host that route is being cancelled
this.host.routeCancelled = true;
if(this.loadedFragment){
this.loadedFragment.localPipelineWorker.cancelFragmentRoute(cb)
} else {
//Async fragment yet to load
//remove any uis if attached - solves lingering ui bug (remember, builder inserts, doesn't override. That's why it lingered)
if(this.host.isViewInitialized()){
this.host.detachViewFromDOM();
}
//Async fragment might have failed to load. So, just approve
cb();
}
}
/**
* GOING DOWN, only needed once. For first async loaded. Else, direct loaded after pass down of correct one
* CAN SOLVE ALL THIS by having fixed instance of these you know. Ha!
* So, not sending new. Already loaded. But that will affect logic for fragments. Yikes!
* SO, NO
* Called by MainRoutingPipeline to get the destroy consent of the fragment hosting this local pipeline worker
* @param {routingPipelineConsentCb} cb
*/
getRouteChangeConsent(cb, newRouteInfo){
if(this.loadedFragment){
this.loadedFragment.localPipelineWorker.getRouteChangeConsent(cb, newRouteInfo);
} else {
//Async fragment might have failed to load. So, just approve
cb({ consent: true, savedState: {} });
}
}
/**
* Called by MainRoutingPipeline
*
* Inform a previously consented fragment that the route has been maintained by a parent
* OR
* It was consenting to a route where it is not being destroyed, and the consent was either approved or not. Regardless, running state valid
* Therefore, transition the child's state back to running
*
* RATIONALE FOR THIS STRUCTURE
*
* Imagine a route /newBlog/:blogId/addMedia
* The addMedia fragment is the last node, but there's the :blogId fragment that probably is
* rendering and managing the view of WYSIWYG editor. If all consenting powers rest on the last node,
* the user's unsaved work will get lost. Thus, all in destruction stack MUST consent to destruction
* This algorithm allows for more flexible architectures
*/
routeMaintained(){
this.loadedFragment.localPipelineWorker.routeMaintained();
}
/**
* Called by MainRoutingPipeline to destroy the fragment
* @param {fragmentDestroyCb} cb
*/
destroyFragment(cb){
this.loadedFragment.localPipelineWorker.destroyFragment(cb);
}
}
export default AsyncAppFragmentBuilder;