UNPKG

@nitrogenbuilder/client-core

Version:

Nitrogen Builder Core Client

212 lines (179 loc) 5.59 kB
import type { BuilderModule, ComponentSettings, Module, } from '@nitrogenbuilder/types'; import { BatchFetchContext, type BatchTransport } from './BatchFetchContext.js'; import { logger } from './utils/logger.js'; import { updateModuleProps } from './utils/updateWithDefaultProps.js'; export class Nitrogen { static instance: Nitrogen; static modules = new Map<string, Module>(); static moduleComponents = new Map<string, any>(); static moduleDataFetchers = new Map<string, (props: any, ctx?: BatchFetchContext) => Promise<any>>(); private static moduleEventQueued = false; private static nitrogenInitializedListenerAdded = false; // The idea hereis that the moduleData map key is a cache key. The first item in the array is always the moduleId. // This logic is not yet implemented. Right now it only works with one array item which is the moduleId. static moduleData = new Map<string, any>(); constructor() { if (Nitrogen.instance) { return Nitrogen.instance; } Nitrogen.instance = this; } getModule(moduleId: string | number) { return Nitrogen.modules.get(`${moduleId}`); } getModuleComponent(moduleId: string | number) { return Nitrogen.moduleComponents.get(`${moduleId}`); } getModuleData(moduleId: string | number) { logger.log( 'getting Nitrogen.moduleData, ' + moduleId, { context: { moduleData: Nitrogen.moduleData.get(`${moduleId}`) } } ); return Nitrogen.moduleData.get(`${moduleId}`); } getModuleDataFetcher(moduleId: string | number) { return Nitrogen.moduleDataFetchers.get(`${moduleId}`); } async getRequestedData({ page, dynamicData, batchTransport, }: { page: BuilderModule[]; dynamicData?: any; batchTransport?: BatchTransport; }) { // Step 1: Walk tree to collect all modules with data fetchers type DataModule = { moduleId: string; moduleName: string; props: any }; const dataModules: DataModule[] = []; function collectDataModules(modules: BuilderModule[]) { for (const module of modules) { if (module.module) { const moduleName = typeof module.module === 'object' ? module.module.name : module.module; if (Nitrogen.moduleDataFetchers.has(moduleName)) { const updatedModule = updateModuleProps(module, dynamicData); dataModules.push({ moduleId: module.id, moduleName, props: updatedModule.props, }); } } if (module.props?.children) { if (Array.isArray(module.props.children)) { collectDataModules(module.props.children); } else if (typeof module.props.children === 'object') { for (const slot in module.props.children) { collectDataModules(module.props.children[slot]); } } } } } collectDataModules(page); // Step 2: Create batch context if transport provided const ctx = batchTransport ? new BatchFetchContext(batchTransport) : undefined; // Step 3: Start all getData calls concurrently const promises = dataModules.map(({ moduleId, moduleName, props }) => { const fetcher = Nitrogen.moduleDataFetchers.get(moduleName)!; return fetcher( { ...props, __dynamicData: dynamicData, }, ctx ).then((data) => ({ moduleId, data, })); }); // Step 4: If batch context exists, flush the single batch request if (ctx) { await ctx.flush(); } // Step 5: Wait for all getData post-processing to complete const results = await Promise.all(promises); // Step 6: Store results for this request and update the shared module cache. const requestedData: Record<string, any> = {}; for (const { moduleId, data } of results) { requestedData[moduleId] = data; Nitrogen.moduleData.set(`${moduleId}`, data); } return requestedData; } registerModuleDataFetcher( moduleId: string | number, getData: (props: any, ctx?: BatchFetchContext) => Promise<any> ) { Nitrogen.moduleDataFetchers.set(`${moduleId}`, getData); } private dispatchModulesEvent() { if (typeof window === 'undefined') return; const dispatch = () => { window.dispatchEvent( new CustomEvent('nitrogen-modules', { detail: { modules: Object.fromEntries(Nitrogen.modules), }, }) ); }; if (window?.nitrogen?.parentInit) { dispatch(); return; } if (Nitrogen.nitrogenInitializedListenerAdded) return; Nitrogen.nitrogenInitializedListenerAdded = true; window.addEventListener( 'nitrogen-initialized', () => { Nitrogen.nitrogenInitializedListenerAdded = false; dispatch(); }, { once: true } ); } private queueModulesEvent() { if (typeof window === 'undefined' || Nitrogen.moduleEventQueued) return; Nitrogen.moduleEventQueued = true; const flush = () => { Nitrogen.moduleEventQueued = false; this.dispatchModulesEvent(); }; if (typeof queueMicrotask === 'function') { queueMicrotask(flush); } else { setTimeout(flush, 0); } } registerModule( moduleId: string | number, Component: any, module: ComponentSettings, getData?: (props: any, ctx?: BatchFetchContext) => Promise<any> ) { Nitrogen.modules.set(`${moduleId}`, { ...module, name: `${moduleId}`, id: moduleId, } as Module); // overwriting the type so we can sneak in the id field Nitrogen.moduleComponents.set(`${moduleId}`, Component); if (getData) { Nitrogen.moduleDataFetchers.set(`${moduleId}`, getData); } logger.log('Nitrogen.modules', { context: { moduleId, moduleCount: Nitrogen.modules.size, windowType: typeof window }, }); this.queueModulesEvent(); } } export const nitrogen = new Nitrogen();