|
|
import { TraceSource } from "@implab/core-amd/log/TraceSource";
|
|
|
import { id as mid } from "module";
|
|
|
import { IScope, Scope } from "./Scope";
|
|
|
import { isNode, isRendition, isWidget } from "./traits";
|
|
|
|
|
|
const trace = TraceSource.get(mid);
|
|
|
|
|
|
interface Context {
|
|
|
readonly scope: IScope;
|
|
|
|
|
|
readonly priority: number;
|
|
|
|
|
|
readonly hooks?: (() => void)[];
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
};
|
|
|
|
|
|
// 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
|
|
|
let _renderCount = 0;
|
|
|
|
|
|
// next id for render operations
|
|
|
let _renderId = 1;
|
|
|
|
|
|
// hooks for render completion, executed when all render operations has
|
|
|
// been completed
|
|
|
let _renderedHooks: (() => void)[] = [];
|
|
|
|
|
|
const guard = (cb: () => void) => {
|
|
|
try {
|
|
|
cb();
|
|
|
} catch (e) {
|
|
|
trace.error(e);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Creates a new rendition context with the specified parameters and makes it
|
|
|
* an active context.
|
|
|
*
|
|
|
* @see getScope
|
|
|
* @see getPriority
|
|
|
*
|
|
|
* @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
|
|
|
*/
|
|
|
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");
|
|
|
|
|
|
const { hooks } = _context;
|
|
|
if (hooks)
|
|
|
hooks.forEach(guard);
|
|
|
|
|
|
_context = prev;
|
|
|
};
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 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.
|
|
|
*/
|
|
|
const startRender = () => {
|
|
|
_renderCount++;
|
|
|
if (_renderCount === 1)
|
|
|
onRendering();
|
|
|
|
|
|
return _renderId++;
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 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 = () => {
|
|
|
_renderCount--;
|
|
|
if (_renderCount === 0)
|
|
|
onRendered();
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 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();
|
|
|
};
|
|
|
};
|
|
|
|
|
|
// called when the first beginRender is called for this iteration
|
|
|
const onRendering = () => {
|
|
|
trace.log("Rendering started");
|
|
|
setTimeout(() => {
|
|
|
if (_renderCount !== 0)
|
|
|
trace.error("Rendering tasks aren't finished, currently running = {0}", _renderCount);
|
|
|
});
|
|
|
};
|
|
|
|
|
|
// called when all render operations are complete
|
|
|
const onRendered = () => {
|
|
|
trace.log("Rendering compete");
|
|
|
_renderedHooks.forEach(guard);
|
|
|
_renderedHooks = [];
|
|
|
};
|
|
|
|
|
|
/** Returns promise when the rendering has been completed. */
|
|
|
export const whenRendered = () => new Promise<void>((resolve) => {
|
|
|
if (_renderCount)
|
|
|
_renderedHooks.push(resolve);
|
|
|
else
|
|
|
resolve();
|
|
|
});
|
|
|
|
|
|
/**
|
|
|
* 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.
|
|
|
*/
|
|
|
export const renderHook = (hook: () => void) => {
|
|
|
const { hooks } = _context;
|
|
|
if (hooks)
|
|
|
hooks.push(hook);
|
|
|
else
|
|
|
guard(hook);
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 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
|
|
|
*/
|
|
|
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));
|
|
|
};
|
|
|
|
|
|
/** 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.
|
|
|
*/
|
|
|
export const getScope = () => _context.scope;
|
|
|
|
|
|
/**
|
|
|
* 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;
|
|
|
|
|
|
/** Schedules the rendition to be rendered to the DOM Node
|
|
|
* @param rendition The rendition to be rendered
|
|
|
* @param scope The scope
|
|
|
*/
|
|
|
export const render = (rendition: unknown, scope = Scope.dummy) =>
|
|
|
renderTask(() => getItemDom(rendition), scope, getPriority(), startRender());
|
|
|
|
|
|
const emptyFragment = document.createDocumentFragment();
|
|
|
|
|
|
/** Renders DOM element for different types of the argument. */
|
|
|
export const getItemDom = (v: unknown) => {
|
|
|
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
|
|
|
return v.getDomNode();
|
|
|
} else if (isWidget(v)) {
|
|
|
// widgets are converted to it's markup
|
|
|
return v.domNode;
|
|
|
} else if (typeof v === "boolean" || v === null || v === undefined) {
|
|
|
// null | undefined | boolean are removed
|
|
|
return emptyFragment;
|
|
|
} else if (v instanceof Array) {
|
|
|
// arrays will be translated to document fragments
|
|
|
const fragment = document.createDocumentFragment();
|
|
|
v.map(item => getItemDom(item))
|
|
|
.forEach(node => fragment.appendChild(node));
|
|
|
return fragment;
|
|
|
} else {
|
|
|
// bug: explicit error otherwise
|
|
|
throw new Error(`Invalid parameter: ${String(v)}`);
|
|
|
}
|
|
|
};
|
|
|
|