/** @jsxImportSource react */ import { Widget, VDOM, getContent } from "./Widget"; import { Instance } from "./Instance"; import { RenderingContext } from "./RenderingContext"; import { debug, appDataFlag } from "../util/Debug"; import { Timing, now, appLoopFlag, vdomRenderFlag } from "../util/Timing"; import { isBatchingUpdates, notifyBatchedUpdateStarting, notifyBatchedUpdateCompleted } from "./batchUpdates"; import { shallowEquals } from "../util/shallowEquals"; import { PureContainer } from "./PureContainer"; import { onIdleCallback } from "../util/onIdleCallback"; import { getCurrentCulture, pushCulture, popCulture, CultureInfo, ResolvedCultureInfo } from "./Culture"; import { View } from "../data/View"; import { Config } from "./Prop"; export interface CxProps { widget?: Config; items?: Config; store?: View; instance?: Instance; parentInstance?: Instance; subscribe?: boolean; immediate?: boolean; deferredUntilIdle?: boolean; idleTimeout?: number; options?: any; onError?: (error: Error, instance: Instance, info: any) => void; params?: any; contentFactory?: (props: { children: any }) => any; cultureInfo?: ResolvedCultureInfo; } export interface CxState { deferToken: number; data?: any; } export class Cx extends VDOM.Component { widget: Widget; store: View; parentInstance?: Instance; instance?: Instance; flags: { preparing?: boolean; dirty?: boolean; rendering?: boolean }; renderCount: number; unsubscribe?: () => void; componentDidCatch?: (error: Error, info: any) => void; forceUpdateCallback: () => void; deferCounter: number; pendingUpdateTimer?: NodeJS.Timeout; unsubscribeIdleRequest?: () => void; constructor(props: CxProps) { super(props); if (props.instance) { this.widget = (props.instance as any).widget; this.store = (props.instance as any).store; } else { this.widget = PureContainer.create({ items: props.widget || props.items }); if (props.parentInstance) { this.parentInstance = props.parentInstance; this.store = props.store || (this.parentInstance as any).store; } else { this.parentInstance = new Instance(this.widget, "0", undefined, props.store); this.store = props.store!; } if (!this.store) throw new Error("Cx component requires a store."); } this.state = { deferToken: 0, }; if (props.subscribe) { this.unsubscribe = this.store.subscribe(this.update.bind(this)); (this.state as any).data = this.store.getData(); } this.flags = {}; this.renderCount = 0; if (props.onError) this.componentDidCatch = this.componentDidCatchHandler.bind(this); this.forceUpdateCallback = this.forceUpdate.bind(this); this.deferCounter = 0; this.waitForIdle(); } UNSAFE_componentWillReceiveProps(props: CxProps): void { let newStore = props.instance ? (props.instance as any).store : props.store ? props.store : (props.parentInstance as any).store; if (newStore != this.store) { this.store = newStore; if (this.unsubscribe) this.unsubscribe(); if (props.subscribe) this.unsubscribe = this.store.subscribe(this.update.bind(this)); } if (props.subscribe) { let data = this.store.getData(); if (data !== this.state.data) { this.waitForIdle(); this.setState({ data }); } } } getInstance(): Instance { if (this.props.instance) return this.props.instance; if (this.instance && (this.instance as any).widget === this.widget) { if ((this.instance as any).parentStore != this.store) (this.instance as any).setParentStore(this.store); return this.instance; } if (this.widget && this.parentInstance) return (this.instance = (this.parentInstance as any).getDetachedChild(this.widget, 0, this.store)); throw new Error("Could not resolve a widget instance in the Cx component."); } render() { if (this.props.deferredUntilIdle && this.state.deferToken < this.deferCounter) return null; let cultureInfo = this.props.cultureInfo ?? getCurrentCulture(); return ( ); } componentDidMount(): void { this.componentDidUpdate(); if (this.props.options && this.props.options.onPipeUpdate) this.props.options.onPipeUpdate(this.update.bind(this)); } componentDidUpdate(): void { if (this.flags.dirty) { this.update(); } } update(): void { let data = this.store.getData(); debug(appDataFlag, data); if (this.flags.preparing) this.flags.dirty = true; else if (isBatchingUpdates() || this.props.immediate) { notifyBatchedUpdateStarting(); this.setState({ data: data }, notifyBatchedUpdateCompleted); } else { //in standard mode sequential store commands are batched if (!this.pendingUpdateTimer) { notifyBatchedUpdateStarting(); this.pendingUpdateTimer = setTimeout(() => { delete this.pendingUpdateTimer; this.setState({ data: data }, notifyBatchedUpdateCompleted); }, 0); } } } waitForIdle(): void { if (!this.props.deferredUntilIdle) return; if (this.unsubscribeIdleRequest) this.unsubscribeIdleRequest(); let token = ++this.deferCounter; this.unsubscribeIdleRequest = onIdleCallback( () => { this.setState({ deferToken: token }); }, { timeout: this.props.idleTimeout || 30000, }, ); } componentWillUnmount(): void { if (this.pendingUpdateTimer) clearTimeout(this.pendingUpdateTimer); if (this.unsubscribeIdleRequest) this.unsubscribeIdleRequest(); if (this.unsubscribe) this.unsubscribe(); if (this.props.options && this.props.options.onPipeUpdate) this.props.options.onPipeUpdate(null); } shouldComponentUpdate(props: CxProps, state: CxState): boolean { if (props.deferredUntilIdle && state.deferToken != this.deferCounter) return false; return ( state !== this.state || !props.params || !shallowEquals(props.params, this.props.params) || props.instance !== this.props.instance || props.widget !== this.props.widget || props.store !== this.props.store || props.parentInstance !== this.props.parentInstance || props.cultureInfo !== this.props.cultureInfo ); } componentDidCatchHandler(error: Error, info: any): void { this.flags.preparing = false; this.props.onError!(error, this.getInstance(), info); } } interface CxContextProps { instance: Instance; flags: { preparing?: boolean; dirty?: boolean; rendering?: boolean }; options?: any; buster: number; contentFactory?: (props: { children: any }) => any; forceUpdate: () => void; cultureInfo?: ResolvedCultureInfo; } class CxContext extends VDOM.Component { renderCount: number; timings: any; content: any; renderingContext?: RenderingContext; constructor(props: CxContextProps) { super(props); this.renderCount = 0; this.UNSAFE_componentWillReceiveProps(props); } UNSAFE_componentWillReceiveProps(props: CxContextProps): void { this.timings = { start: now(), }; let { instance, options, contentFactory } = props; let count = 0, visible, context, forceContinue; //should not be tracked by parents for destroy if (!(instance as any).detached) throw new Error("The instance passed to a Cx component should be detached from its parent."); if (this.props.instance !== instance && (this.props.instance as any).destroyTracked) (this.props.instance as any).destroy(); this.props.flags.preparing = true; if (this.props.cultureInfo) pushCulture(this.props.cultureInfo); try { do { count++; forceContinue = false; context = new RenderingContext(options); (context as any).forceUpdate = this.props.forceUpdate; this.props.flags.dirty = false; (instance as any).assignedRenderList = (context as any).getRootRenderList(); visible = (instance as any).scheduleExploreIfVisible(context); if (visible) { while (!(context as any).exploreStack.empty()) { let inst = (context as any).exploreStack.pop(); //console.log("EXPLORE", inst.widget.constructor.name, inst.widget.tag, inst.widget.widgetId); (inst as any).explore(context); } } else if ((instance as any).destroyTracked) { (instance as any).destroy(); } if (this.props.flags.dirty && count <= 3 && Widget.optimizePrepare && now() - this.timings.start < 8) { forceContinue = true; continue; } if (visible) { this.timings.afterExplore = now(); for (let i = 0; i < (context as any).prepareList.length; i++) (context as any).prepareList[i].prepare(context); this.timings.afterPrepare = now(); } } while ( forceContinue || (this.props.flags.dirty && count <= 3 && Widget.optimizePrepare && now() - this.timings.start < 8) ); if (visible) { //walk in reverse order so children get rendered first let renderList = (context as any).getRootRenderList(); while (renderList) { for (let i = renderList.data.length - 1; i >= 0; i--) { renderList.data[i].render(context); } renderList = renderList.right; } this.content = getContent((instance as any).vdom); if (contentFactory) this.content = contentFactory({ children: this.content }); this.timings.afterRender = now(); for (let i = 0; i < (context as any).cleanupList.length; i++) (context as any).cleanupList[i].cleanup(context); } else { this.content = null; this.timings.afterExplore = this.timings.afterPrepare = this.timings.afterRender = now(); } } finally { if (this.props.cultureInfo) popCulture(this.props.cultureInfo); } this.timings.beforeVDOMRender = now(); this.props.flags.preparing = false; this.props.flags.rendering = true; this.renderingContext = context; } render() { return this.content; } componentDidMount(): void { this.componentDidUpdate(); } componentDidUpdate(): void { this.props.flags.rendering = false; this.timings.afterVDOMRender = now(); //let {instance} = this.props; //instance.cleanup(this.renderingContext); this.timings.afterCleanup = now(); this.renderCount++; if (process.env.NODE_ENV !== "production") { let { start, beforeVDOMRender, afterVDOMRender, afterPrepare, afterExplore, afterRender, afterCleanup } = this.timings; Timing.log( vdomRenderFlag, this.renderCount, "cx", (beforeVDOMRender - start + afterCleanup - afterVDOMRender).toFixed(2) + "ms", "vdom", (afterVDOMRender - beforeVDOMRender).toFixed(2) + "ms", ); Timing.log( appLoopFlag, this.renderCount, (this.renderingContext as any).options.name || "main", "total", (afterCleanup - start).toFixed(1) + "ms", "explore", (afterExplore - start).toFixed(1) + "ms", "prepare", (afterPrepare - afterExplore).toFixed(1), "render", (afterRender - afterPrepare).toFixed(1), "vdom", (afterVDOMRender - beforeVDOMRender).toFixed(1), "cleanup", (afterCleanup - afterVDOMRender).toFixed(1), ); } } componentWillUnmount(): void { let { instance } = this.props; if ((instance as any).destroyTracked) (instance as any).destroy(); } }