render.ts
334 lines
| 10.5 KiB
| video/mp2t
|
TypeScriptLexer
cin
|
r101 | import { TraceSource } from "@implab/core-amd/log/TraceSource"; | ||
import { id as mid } from "module"; | ||||
cin
|
r102 | import { IScope, Scope } from "./Scope"; | ||
import { isNode, isRendition, isWidget } from "./traits"; | ||||
cin
|
r101 | |||
const trace = TraceSource.get(mid); | ||||
cin
|
r98 | |||
cin
|
r102 | interface Context { | ||
cin
|
r118 | readonly scope: IScope; | ||
cin
|
r98 | |||
cin
|
r146 | readonly priority: number; | ||
cin
|
r118 | readonly hooks?: (() => void)[]; | ||
cin
|
r102 | } | ||
cin
|
r101 | |||
cin
|
r146 | type RenderTask = { | ||
/** | ||||
* The priority for this task | ||||
*/ | ||||
readonly priority: number, | ||||
/** | ||||
* The rendering action performed in this task | ||||
*/ | ||||
readonly render: () => void; | ||||
}; | ||||
type Range = { | ||||
/** minimum value in this range */ | ||||
readonly min: number; | ||||
/** maximum value in this range */ | ||||
readonly max: number; | ||||
cin
|
r109 | }; | ||
cin
|
r101 | |||
cin
|
r146 | // empty range | ||
const emptyPriorities: Range = { min: NaN, max: NaN }; | ||||
// holds render tasks | ||||
let _renderQueue: RenderTask[] = []; | ||||
// holds the minimum and the maximum task priorities in the queue. Used to | ||||
// optimize rendering process is all tasks are with the same priority. | ||||
let _renderQueuePriorities: Range = emptyPriorities; | ||||
// current context | ||||
let _context: Context = { | ||||
scope: Scope.dummy, | ||||
priority: 0 | ||||
}; | ||||
// started render operations | ||||
cin
|
r118 | let _renderCount = 0; | ||
cin
|
r146 | |||
// next id for render operations | ||||
cin
|
r118 | let _renderId = 1; | ||
cin
|
r146 | |||
// hooks for render completion, executed when all render operations has | ||||
// been completed | ||||
cin
|
r118 | let _renderedHooks: (() => void)[] = []; | ||
cin
|
r146 | const guard = (cb: () => void) => { | ||
cin
|
r101 | try { | ||
cin
|
r146 | cb(); | ||
cin
|
r101 | } catch (e) { | ||
trace.error(e); | ||||
} | ||||
cin
|
r109 | }; | ||
cin
|
r98 | |||
cin
|
r118 | /** | ||
cin
|
r146 | * Creates a new rendition context with the specified parameters and makes it | ||
* an active context. | ||||
cin
|
r118 | * | ||
cin
|
r146 | * @see getScope | ||
* @see getPriority | ||||
cin
|
r118 | * | ||
cin
|
r146 | * @param scope The scope for the current context | ||
* @param priority The priority for the current context | ||||
* @returns The function to restore the previous context and execute pending hooks | ||||
cin
|
r118 | */ | ||
cin
|
r146 | const enterContext = (scope: IScope, priority: number) => { | ||
const prev = _context; | ||||
const captured = _context = { scope, priority, hooks: [] }; | ||||
return () => { | ||||
if (_context !== captured) | ||||
trace.error("endRender mismatched beginRender call"); | ||||
cin
|
r118 | |||
cin
|
r146 | const { hooks } = _context; | ||
if (hooks) | ||||
hooks.forEach(guard); | ||||
cin
|
r122 | |||
cin
|
r146 | _context = prev; | ||
cin
|
r118 | }; | ||
cin
|
r109 | }; | ||
cin
|
r101 | |||
/** | ||||
cin
|
r146 | * Starts the new render operation. When the operation is started the counter | ||
* of running operations is increased. If this is the first render operation | ||||
* then the `onRendering` event is fired. | ||||
* | ||||
* @returns The id of the started rendering operation. | ||||
cin
|
r101 | */ | ||
cin
|
r146 | const startRender = () => { | ||
_renderCount++; | ||||
if (_renderCount === 1) | ||||
onRendering(); | ||||
cin
|
r118 | |||
cin
|
r146 | return _renderId++; | ||
}; | ||||
cin
|
r102 | |||
cin
|
r146 | /** | ||
* Completes the rendering operation. When the operation is completed the counter | ||||
* of running operations is decreased. If there is no more running operations left | ||||
* then the `onRendered` event is fired. | ||||
* | ||||
*/ | ||||
const completeRender = () => { | ||||
cin
|
r118 | _renderCount--; | ||
if (_renderCount === 0) | ||||
onRendered(); | ||||
cin
|
r109 | }; | ||
cin
|
r101 | |||
cin
|
r146 | /** | ||
* Invokes the specified within the rendition context. The rendition context | ||||
* is created for the task invocation and restored after the task is competed. | ||||
* | ||||
* @param scope The cope for the rendition context | ||||
* @param priority The priority for the rendition context | ||||
* @returns The result returned by the task | ||||
*/ | ||||
export const renderTask = <T>(task: () => T, scope: IScope, priority: number, renderId: number) => { | ||||
const restoreContext = enterContext(scope, priority); | ||||
try { | ||||
trace.debug("beginRender [{0}], priority = {1}", renderId, priority); | ||||
return task(); | ||||
} finally { | ||||
trace.debug("endRender [{0}]", renderId); | ||||
restoreContext(); | ||||
completeRender(); | ||||
} | ||||
}; | ||||
const processRenderQueue = () => { | ||||
const q = _renderQueue; | ||||
const { min, max } = _renderQueuePriorities; | ||||
_renderQueue = []; | ||||
_renderQueuePriorities = emptyPriorities; | ||||
// all tasks scheduled due queue processing will be queued to the next | ||||
// processRenderQueue iteration | ||||
if (min !== max) { | ||||
// if there are tasks with different priorities in the queue | ||||
trace.debug("Processing render queue, {0} tasks, priorities=[{1}..{2}] ", q.length, min, max); | ||||
q.sort(({ priority: a }, { priority: b }) => a - b).forEach(({ render }) => guard(render)); | ||||
} else { | ||||
// all tasks are have same priority | ||||
trace.debug("Processing render queue, {0} tasks, priority = {1} ", q.length, min); | ||||
q.forEach(({ render }) => guard(render)); | ||||
} | ||||
if (_renderQueue.length) | ||||
trace.debug("Render queue is processed, {0} tasks rescheduled", _renderQueue.length); | ||||
}; | ||||
/** | ||||
* Adds the specified task to the render queue. The task will be added with the | ||||
* specified priority. | ||||
* | ||||
* Render queue contains a list of render tasks. Each task is executed within | ||||
* its own rendering context with the specified scope. The priority determines | ||||
* the order in which tasks will be executed where the tasks with lower priority | ||||
* numbers are executed first. | ||||
* | ||||
* When the queue is empty and the task is added then the render queue will be | ||||
* scheduled for execution. While the current queue is being executed all | ||||
* enqueued tasks are added to the new queue and processed after the current | ||||
* execution has been completed. | ||||
* | ||||
* @param task The action to execute. This action will be executed with | ||||
* {@link renderTask} function in its own rendering context. | ||||
* @param scope The scope used to create a rendering context for the task. | ||||
* @param priority The priority | ||||
*/ | ||||
export const queueRenderTask = (task: () => void, scope = Scope.dummy, priority = getPriority()) => { | ||||
const renderId = startRender(); | ||||
trace.debug("scheduleRender [{0}], priority = {1}", renderId, priority); | ||||
const render = () => renderTask(task, scope, priority, renderId); | ||||
if (!_renderQueue.length) { | ||||
// this is the first task, schedule next render queue processing | ||||
Promise.resolve().then(processRenderQueue, e => trace.error(e)); | ||||
// initialize priorities | ||||
_renderQueuePriorities = { min: priority, max: priority }; | ||||
} else { | ||||
// update priorities if needed | ||||
const { min, max } = _renderQueuePriorities; | ||||
if (priority < min) | ||||
_renderQueuePriorities = { min: priority, max }; | ||||
else if (priority > max) | ||||
_renderQueuePriorities = { min, max: priority }; | ||||
} | ||||
_renderQueue.push({ priority, render }); | ||||
}; | ||||
/** | ||||
* Starts the synchronous rendering process with the specified scope and priority. | ||||
* | ||||
* @param scope The scope for the current rendition | ||||
* @param priority The priority for the current scope | ||||
* @returns The function to complete the current rendering | ||||
*/ | ||||
export const beginRender = (scope = Scope.dummy, priority = 0) => { | ||||
const renderId = startRender(); | ||||
const restoreContext = enterContext(scope, priority); | ||||
trace.debug("beginRender [{0}], priority = {1}", renderId, priority); | ||||
return () => { | ||||
trace.debug("endRender [{0}]", renderId); | ||||
restoreContext(); | ||||
completeRender(); | ||||
}; | ||||
}; | ||||
cin
|
r118 | // called when the first beginRender is called for this iteration | ||
const onRendering = () => { | ||||
cin
|
r133 | trace.log("Rendering started"); | ||
cin
|
r118 | setTimeout(() => { | ||
if (_renderCount !== 0) | ||||
trace.error("Rendering tasks aren't finished, currently running = {0}", _renderCount); | ||||
}); | ||||
}; | ||||
// called when all render operations are complete | ||||
const onRendered = () => { | ||||
cin
|
r133 | trace.log("Rendering compete"); | ||
cin
|
r118 | _renderedHooks.forEach(guard); | ||
_renderedHooks = []; | ||||
}; | ||||
cin
|
r146 | /** Returns promise when the rendering has been completed. */ | ||
cin
|
r118 | export const whenRendered = () => new Promise<void>((resolve) => { | ||
if (_renderCount) | ||||
_renderedHooks.push(resolve); | ||||
else | ||||
resolve(); | ||||
}); | ||||
cin
|
r146 | /** | ||
* Registers hook which is called after the render operation is completed. The | ||||
* hook will be called once only for the current operation. | ||||
* | ||||
* @param hook The hook which should be called when rendering is complete. | ||||
*/ | ||||
cin
|
r101 | export const renderHook = (hook: () => void) => { | ||
cin
|
r102 | const { hooks } = _context; | ||
if (hooks) | ||||
cin
|
r101 | hooks.push(hook); | ||
else | ||||
guard(hook); | ||||
cin
|
r109 | }; | ||
cin
|
r98 | |||
cin
|
r146 | /** | ||
* Registers special hook which will be called with the specified state. The | ||||
* hook is called once after the rendering is complete. When the rendition is | ||||
* destroyed the hook is called with the undefined parameter. | ||||
* | ||||
* This function is used to register `ref` hooks form a tsx rendition. | ||||
* | ||||
* @param value The state which will be supplied as a parameter for the hook | ||||
* @param ref reference hook | ||||
*/ | ||||
cin
|
r102 | export const refHook = <T>(value: T, ref: JSX.Ref<T>) => { | ||
const { hooks, scope } = _context; | ||||
if (hooks) | ||||
hooks.push(() => ref(value)); | ||||
else | ||||
guard(() => ref(value)); | ||||
scope.own(() => ref(undefined)); | ||||
cin
|
r109 | }; | ||
cin
|
r102 | |||
cin
|
r146 | /** Returns the current scope. Scope is used to track resources bound to the | ||
* current rendering. When the rendering is destroyed the scope is cleaned and | ||||
* all bound resources are released. | ||||
*/ | ||||
cin
|
r102 | export const getScope = () => _context.scope; | ||
cin
|
r98 | |||
cin
|
r146 | /** | ||
* Returns the current render task priority. This value is used by some renditions | ||||
* to schedule asynchronous nested updates with lower priority then themselves. | ||||
*/ | ||||
export const getPriority = () => _context.priority; | ||||
cin
|
r98 | /** Schedules the rendition to be rendered to the DOM Node | ||
* @param rendition The rendition to be rendered | ||||
* @param scope The scope | ||||
*/ | ||||
cin
|
r146 | export const render = (rendition: unknown, scope = Scope.dummy) => | ||
renderTask(() => getItemDom(rendition), scope, getPriority(), startRender()); | ||||
cin
|
r98 | |||
cin
|
r120 | const emptyFragment = document.createDocumentFragment(); | ||
cin
|
r98 | /** Renders DOM element for different types of the argument. */ | ||
cin
|
r101 | export const getItemDom = (v: unknown) => { | ||
cin
|
r98 | if (typeof v === "string" || typeof v === "number" || v instanceof RegExp || v instanceof Date) { | ||
// primitive types converted to the text nodes | ||||
return document.createTextNode(v.toString()); | ||||
} else if (isNode(v)) { | ||||
// nodes are kept as is | ||||
return v; | ||||
} else if (isRendition(v)) { | ||||
// renditions are instantiated | ||||
cin
|
r101 | return v.getDomNode(); | ||
cin
|
r98 | } else if (isWidget(v)) { | ||
// widgets are converted to it's markup | ||||
return v.domNode; | ||||
} else if (typeof v === "boolean" || v === null || v === undefined) { | ||||
cin
|
r102 | // null | undefined | boolean are removed | ||
cin
|
r120 | return emptyFragment; | ||
cin
|
r98 | } else if (v instanceof Array) { | ||
// arrays will be translated to document fragments | ||||
const fragment = document.createDocumentFragment(); | ||||
cin
|
r101 | v.map(item => getItemDom(item)) | ||
cin
|
r98 | .forEach(node => fragment.appendChild(node)); | ||
return fragment; | ||||
} else { | ||||
// bug: explicit error otherwise | ||||
cin
|
r109 | throw new Error(`Invalid parameter: ${String(v)}`); | ||
cin
|
r98 | } | ||
cin
|
r109 | }; | ||