##// END OF EJS Templates
bump dependencies, bump ts to 5.2...
bump dependencies, bump ts to 5.2 moved observables logic to ObservableImpl fixed while/until bug with complete handler multiple call

File last commit:

r146:af4f8424e83d v1.9.0 default
r155:6acbe6efbe20 v1.10.2 default
Show More
render.ts
334 lines | 10.5 KiB | video/mp2t | TypeScriptLexer
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)}`);
}
};