##// END OF EJS Templates
Added priorities to render tasks, revisited rendering scheduler...
cin -
r146:af4f8424e83d v1.9.0 default
parent child
Show More
@@ -1,60 +1,60
1 1 import { Observable, Sink, Subscribable, observe } from "../observable";
2 2
3 3 const noop = () => { };
4 4
5 5 /** Connects multiple subscribers to the single producer. The producer
6 6 * will be created when the first client subscribes and will be released
7 7 * with the the last client unsubscribed.
8 8 *
9 9 * Use this wrapper to prevent spawning multiple producers.
10 10 *
11 * The emitted values are not cached therefore the new subscriber will not receive
12 * the values emitted before it has been subscribed.
11 * The emitted values are not cached therefore new subscribers will not receive
12 * the values emitted before they had subscribed.
13 13 *
14 14 * @param source The source observable
15 15 * @returns The new observable
16 16 */
17 17 export const subject = <T>(source: Subscribable<T>): Observable<T> => {
18 18 let subscribers: Sink<T>[] = []; // the list of active subscribers
19 19
20 20 let subscription = { unsubscribe: noop }; // current subscription
21 21
22 22 // cleanup method to release resources held by this subscription
23 23 const cleanup = (cb: (item: Sink<T>) => void) => {
24 24 const _subscribers = subscribers;
25 25 subscribers = []; // this will prevent a client cleanup login to run
26 26 _subscribers.forEach(cb);
27 27 // we don't need subscription.unsubscribe(), because cleanup is called
28 28 // from complete or error methods.
29 29 };
30 30
31 31 const sink: Sink<T> = {
32 32 isClosed: () => false,
33 33 complete: () => cleanup(s => s.complete()),
34 34 error: e => cleanup(s => s.error(e)),
35 35 next: v => subscribers.forEach(s => s.next(v))
36 36 };
37 37
38 38 return observe(client => {
39 39 const _subscribers = subscribers;
40 40 subscribers.push(client);
41 41 if (subscribers.length === 1) // this is the first client
42 42 subscription = source.subscribe(sink); // activate the producer
43 43
44 44 return () => {
45 45 // this is a cleanup logic for an individual client
46 46 if (_subscribers === subscribers) {
47 47 // is the current subscription to the producer is active
48 48
49 49 // find this client in the list of subscribers
50 50 const pos = subscribers.indexOf(client);
51 51 if (pos >= 0)
52 52 subscribers.splice(pos, 1);
53 53
54 54 // is this is the last subscriber we need to release the producer
55 55 if (!subscribers.length)
56 56 subscription.unsubscribe();
57 57 }
58 58 };
59 59 });
60 60 };
@@ -1,135 +1,142
1 1 import { djbase, djclass } from "../declare";
2 2 import _WidgetBase = require("dijit/_WidgetBase");
3 3 import _AttachMixin = require("dijit/_AttachMixin");
4 4 import { isNode, isElementNode } from "./traits";
5 5 import registry = require("dijit/registry");
6 6 import on = require("dojo/on");
7 7 import { Scope } from "./Scope";
8 import { render } from "./render";
8 import { queueRenderTask, getPriority, render } from "./render";
9 9 import { isNull } from "@implab/core-amd/safe";
10 10
11 11 // type Handle = dojo.Handle;
12 12
13 13 export interface EventArgs {
14 14 bubbles?: boolean;
15 15
16 16 cancelable?: boolean;
17 17
18 18 composed?: boolean;
19 19 }
20 20
21 21 // eslint-disable-next-line @typescript-eslint/no-unused-vars
22 22 export interface DjxWidgetBase<Attrs = object, Events extends { [name in keyof Events]: Event } = object> extends
23 23 _WidgetBase<Events> {
24 24
25 25 /** This property is declared only for type inference to work, it is never assigned
26 26 * and should not be used.
27 27 */
28 28 readonly _eventMap: Events & GlobalEventHandlersEventMap;
29 29
30 30 /** The list of pairs of event and method names. When the widget is created all methods from
31 31 * this list will be connected to corresponding events.
32 32 *
33 33 * This property is maintained in the prototype
34 34 */
35 35 _eventHandlers: Array<{
36 36 eventName: string,
37 37 handlerMethod: string;
38 38 }>;
39 39 }
40 40
41 41 type _super = {
42 42 startup(): void;
43 43
44 44 destroy(preserveDom?: boolean): void;
45 45 };
46 46
47 47 @djclass
48 48 // eslint-disable-next-line @typescript-eslint/no-unused-vars
49 49 export abstract class DjxWidgetBase<Attrs = object, Events = object> extends djbase<_super, _AttachMixin>(_WidgetBase, _AttachMixin) {
50 50 private readonly _scope = new Scope();
51 51
52 private readonly _priority = getPriority() + 1;
53
52 54 buildRendering() {
53 55 const node = render(this.render(), this._scope);
54 56 if (!isElementNode(node))
55 57 throw new Error("The render method must return a single DOM element");
56 58 this.domNode = node as HTMLElement;
57 59
58 60 super.buildRendering();
59 61
60 62 // now we should get assigned data-dojo-attach-points
61 63 // place the contents of the original srcNode to the containerNode
62 64 const src = this.srcNodeRef;
63 65 const dest = this.containerNode;
64 66
65 67 // the donNode is constructed now we need to connect event handlers
66 68 this._connectEventHandlers();
67 69
68 70 if (src && dest) {
69 71 while (src.firstChild)
70 72 dest.appendChild(src.firstChild);
71 73 }
72 74 }
73 75
76 /** Schedules a new deferred rendition within the scope of the widget */
77 scheduleRender(task: () => void) {
78 return queueRenderTask(task, this._scope, this._priority);
79 }
80
74 81 abstract render(): JSX.Element;
75 82
76 83 private _connectEventHandlers() {
77 84 if (this._eventHandlers)
78 85 this._eventHandlers.forEach(({ eventName, handlerMethod }) => {
79 86 const handler = this[handlerMethod as keyof this];
80 87 if (typeof handler === "function")
81 88 on(this.domNode, eventName, handler.bind(this) as (...args: unknown[]) => unknown);
82 89 });
83 90 }
84 91
85 92 _processTemplateNode<T extends (Element | Node | _WidgetBase)>(
86 93 baseNode: T,
87 94 getAttrFunc: (baseNode: T, attr: string) => string | undefined,
88 95 // tslint:disable-next-line: ban-types
89 96 attachFunc: (node: T, type: string, func?: (...args: unknown[]) => unknown) => dojo.Handle
90 97 ): boolean {
91 98 if (isNode(baseNode)) {
92 99 const w = registry.byNode(baseNode);
93 100 if (w) {
94 101 // from dijit/_WidgetsInTemplateMixin
95 102 this._processTemplateNode(w,
96 103 (n, p) => {
97 104 const v = n.get(p as keyof typeof n);
98 105 return isNull(v) ? undefined : String(v);
99 106 }, // callback to get a property of a widget
100 107 (widget, type, callback) => {
101 108 if (!callback)
102 109 throw new Error("The callback must be specified");
103 110
104 111 // callback to do data-dojo-attach-event to a widget
105 112 if (type in widget) {
106 113 // back-compat, remove for 2.0
107 114 return widget.connect(widget, type, callback as EventListener);
108 115 } else {
109 116 // 1.x may never hit this branch, but it's the default for 2.0
110 117 return widget.on(type as keyof GlobalEventHandlersEventMap, callback);
111 118 }
112 119
113 120 });
114 121 // don't process widgets internals
115 122 return false;
116 123 }
117 124 }
118 125 // eslint-disable-next-line @typescript-eslint/ban-types
119 126 return super._processTemplateNode(baseNode, getAttrFunc as (baseNode: T, attr: string) => string, attachFunc as (node: T, type: string, func?: Function) => dojo.Handle);
120 127 }
121 128
122 129 /** Starts current widget and all its supporting widgets (placed outside
123 130 * `containerNode`) and child widgets (placed inside `containerNode`)
124 131 */
125 132 startup() {
126 133 // startup supporting widgets
127 134 registry.findWidgets(this.domNode, this.containerNode).forEach(w => w.startup());
128 135 super.startup();
129 136 }
130 137
131 138 destroy(preserveDom?: boolean) {
132 139 this._scope.destroy();
133 140 super.destroy(preserveDom);
134 141 }
135 142 }
@@ -1,208 +1,207
1 1 import { id as mid } from "module";
2 2 import { TraceSource } from "@implab/core-amd/log/TraceSource";
3 3 import { argumentNotNull } from "@implab/core-amd/safe";
4 import { getScope, render, scheduleRender } from "./render";
4 import { queueRenderTask, getPriority, getScope, render } from "./render";
5 5 import { RenditionBase } from "./RenditionBase";
6 6 import { Scope } from "./Scope";
7 7 import { Cancellation } from "@implab/core-amd/Cancellation";
8 8 import { collectNodes, destroy as safeDestroy, isDocumentFragmentNode, isElementNode, isMounted, placeAt, startupWidgets } from "./traits";
9 9 import { IDestroyable } from "@implab/core-amd/interfaces";
10 10 import { play } from "../play";
11 11 import * as fx from "dojo/fx";
12 12 import { isSubscribable, Subscribable } from "../observable";
13 13 import { isDjObservableResults, OrderedUpdate } from "../store";
14 14
15 15 const trace = TraceSource.get(mid);
16 16
17 17 interface ItemRendition {
18 18 nodes: Node[];
19 19
20 20 scope: IDestroyable;
21 21
22 22 destroy(): void;
23 23 }
24 24
25 25 interface RenderTask<T> extends OrderedUpdate<T> {
26 26 animate: boolean;
27 27 }
28 28
29 29 export interface AnimationAttrs {
30 30 animate?: boolean;
31 31
32 32 animateIn?: (nodes: Node[]) => Promise<void>;
33 33
34 34 animateOut?: (nodes: Node[]) => Promise<void>;
35 35 }
36 36
37 37 export interface WatchForRenditionAttrs<T> extends AnimationAttrs {
38 38 subject: T[] | Subscribable<OrderedUpdate<T>> | undefined | null;
39 39
40 40 component: (arg: T, index: number) => unknown;
41 41 }
42 42
43 43
44 44 const noop = () => { };
45 45
46 46 const fadeIn = (nodes: Node[]) => Promise.all(nodes
47 47 .filter(isElementNode)
48 48 .map(el => play(fx.fadeIn({ node: el as HTMLElement })))
49 49 ).then(noop);
50 50
51 51 const fadeOut = (nodes: Node[]) => Promise.all(nodes
52 52 .filter(isElementNode)
53 53 .map(el => play(fx.fadeOut({ node: el as HTMLElement })))
54 54 ).then(noop);
55 55
56 56
57 57 export class WatchForRendition<T> extends RenditionBase<Node> {
58 58 private readonly _component: (arg: T, index: number) => unknown;
59 59
60 60 private readonly _node: Node;
61 61
62 62 private readonly _itemRenditions: ItemRendition[] = [];
63 63
64 64 private readonly _subject: T[] | Subscribable<OrderedUpdate<T>>;
65 65
66 66 private readonly _renderTasks: RenderTask<T>[] = [];
67 67
68 68 private readonly _animate: boolean;
69 69
70 70 private readonly _animateIn: (nodes: Node[]) => Promise<void>;
71 71
72 72 private readonly _animateOut: (nodes: Node[]) => Promise<void>;
73 73
74 74 private _ct = Cancellation.none;
75 75
76 private _priority = 0;
77
76 78 constructor({ subject, component, animate, animateIn, animateOut }: WatchForRenditionAttrs<T>) {
77 79 super();
78 80 argumentNotNull(component, "component");
79 81
80 82 this._component = component;
81 83
82 84 this._subject = subject ?? [];
83 85
84 86 this._node = document.createComment("[WatchFor]");
85 87 this._animate = !!animate;
86 88 this._animateIn = animateIn ?? fadeIn;
87 89 this._animateOut = animateOut ?? fadeOut;
88 90 }
89 91
90 92 protected _create() {
93 this._priority = getPriority() + 1;
91 94 const scope = getScope();
92 95 scope.own(() => {
93 96 this._itemRenditions.forEach(safeDestroy);
94 97 safeDestroy(this._node);
95 98 });
96 99
97 100 const result = this._subject;
98 101
99 102 if (result) {
100 103 if (isSubscribable<OrderedUpdate<T>>(result)) {
101 104 let animate = false;
102 105 const subscription = result.subscribe({
103 106 next: ({ item, prevIndex, newIndex }) => this._onItemUpdated({ item, prevIndex, newIndex, animate })
104 107 });
105 108 scope.own(subscription);
106 109 animate = this._animate;
107 110 } else {
108 111 if (isDjObservableResults<T>(result))
109 112 scope.own(result.observe((item, prevIndex, newIndex) => this._onItemUpdated({ item, prevIndex, newIndex, animate: false }), true));
110 113
111 114 for (let i = 0, n = result.length; i < n; i++)
112 115 this._onItemUpdated({ item: result[i], prevIndex: -1, newIndex: i, animate: this._animate });
113 116 }
114 117 }
115 118 this._ct = new Cancellation(cancel => scope.own(cancel));
116 119 }
117 120
118 121 private readonly _onItemUpdated = (item: RenderTask<T>) => {
119 122 if (!this._renderTasks.length) {
120 123 // schedule a new job
121 124 this._renderTasks.push(item);
122 this._render().catch(e => trace.error(e));
125
126 // fork
127 // use dummy scope, because every item will have it's own scope
128 queueRenderTask(this._render, Scope.dummy, this._priority);
123 129 } else {
124 130 // update existing job
125 131 this._renderTasks.push(item);
126 132 }
127 133 };
128 134
129 private async _render() {
130 // fork
131 const beginRender = await scheduleRender();
132 const endRender = beginRender();
133 try {
135 private readonly _render = () => {
134 136 // don't render destroyed rendition
135 137 if (this._ct.isRequested())
136 138 return;
137 139
138 140 this._renderTasks.forEach(this._onRenderItem);
139 141 this._renderTasks.length = 0;
140 } finally {
141 endRender();
142 }
143 }
142 };
144 143
145 144 private readonly _onRenderItem = ({ item, newIndex, prevIndex, animate: _animate }: RenderTask<T>) => {
146 145 const animate = _animate && prevIndex !== newIndex;
147 146
148 147 if (prevIndex > -1) {
149 148 // if we need to delete previous rendition
150 149 const [{ nodes, destroy }] = this._itemRenditions.splice(prevIndex, 1);
151 150 if (animate) {
152 151 this._animateOut(nodes)
153 152 .then(destroy)
154 153 .catch(e => trace.error(e));
155 154 } else {
156 155 destroy();
157 156 }
158 157 }
159 158
160 159 if (newIndex > -1) {
161 160 // if we need to create the new rendition
162 161
163 162 // 1. create a new scope for rendering a content
164 163 const scope = new Scope();
165 164
166 165 // 2. render the content
167 166 const itemNode = render(this._component(item, newIndex), scope);
168 167
169 168 // 3. track nodes
170 169 const nodes = isDocumentFragmentNode(itemNode) ?
171 170 collectNodes(itemNode.childNodes) :
172 171 [itemNode];
173 172
174 173 // 5. insert node at the correct position
175 174
176 175 const { nodes: [beforeNode] } = this._itemRenditions[newIndex] ?? { nodes: [] };
177 176
178 177 if (beforeNode)
179 178 placeAt(itemNode, beforeNode, "before");
180 179 else
181 180 placeAt(itemNode, this._node, "before");
182 181
183 182 // 6. store information about rendition
184 183 this._itemRenditions.splice(newIndex, 0, {
185 184 scope,
186 185 nodes,
187 186 destroy: () => {
188 187 scope.destroy();
189 188 nodes.forEach(safeDestroy);
190 189 }
191 190 });
192 191
193 192 // 7. startup widgets if needed
194 193 if (isMounted(this._node))
195 194 nodes.forEach(n => startupWidgets(n));
196 195
197 196 // 8. optionally play the animation
198 197 if (animate)
199 198 this._animateIn(nodes).catch(e => trace.error(e));
200 199 }
201 200 };
202 201
203 202 protected _getDomNode() {
204 203 if (!this._node)
205 204 throw new Error("The instance of the rendition isn't created");
206 205 return this._node;
207 206 }
208 207 }
@@ -1,98 +1,93
1 import { id as mid } from "module";
2 import { TraceSource } from "@implab/core-amd/log/TraceSource";
3 1 import { argumentNotNull } from "@implab/core-amd/safe";
4 import { getItemDom, getScope, scheduleRender } from "./render";
2 import { queueRenderTask, getItemDom, getPriority, getScope } from "./render";
5 3 import { RenditionBase } from "./RenditionBase";
6 4 import { Scope } from "./Scope";
7 5 import { Subscribable } from "../observable";
8 6 import { Cancellation } from "@implab/core-amd/Cancellation";
9 7 import { collectNodes, destroy, isDocumentFragmentNode, isMounted, placeAt, startupWidgets } from "./traits";
10 8
11 const trace = TraceSource.get(mid);
12
13 9 export class WatchRendition<T> extends RenditionBase<Node> {
14 10 private readonly _component: (arg: T) => unknown;
15 11
16 12 private readonly _node: Node;
17 13
18 14 private readonly _scope = new Scope();
19 15
20 16 private readonly _subject: Subscribable<T>;
21 17
22 18 private _renderJob?: { value: T };
23 19
24 20 private _ct = Cancellation.none;
25 21
22 private _priority = 0;
23
26 24 constructor(component: (arg: T) => unknown, subject: Subscribable<T>) {
27 25 super();
28 26 argumentNotNull(component, "component");
29 27
30 28 this._component = component;
31 29
32 30 this._subject = subject;
33 31
34 32 this._node = document.createComment("[Watch]");
35 33 }
36 34
37 35 protected _create() {
38 36 const scope = getScope();
37 this._priority = getPriority() + 1;
38
39 39 scope.own(() => {
40 40 this._scope.destroy();
41 41 destroy(this._node);
42 42 });
43 43 scope.own(this._subject.subscribe({ next: this._onValue }));
44 44 this._ct = new Cancellation(cancel => scope.own(cancel));
45 45 }
46 46
47 47 private readonly _onValue = (value: T) => {
48 48 if (!this._renderJob) {
49 49 // schedule a new job
50 50 this._renderJob = { value };
51 this._render().catch(e => trace.error(e));
51 queueRenderTask(this._render, this._scope, this._priority);
52 52 } else {
53 53 // update existing job
54 54 this._renderJob = { value };
55 55 }
56 56 };
57 57
58 private async _render() {
59 const beginRender = await scheduleRender(this._scope);
60 const endRender = beginRender();
61 try {
58 private readonly _render = () => {
59
62 60 // don't render destroyed rendition
63 61 if (this._ct.isRequested())
64 62 return;
65 63
66 64 // remove all previous content
67 65 this._scope.clean();
68 66
69 67 // render the new node
70 68 const node = getItemDom(this._renderJob ? this._component(this._renderJob.value) : undefined);
71 69
72 70 // get actual content
73 71 const pending = isDocumentFragmentNode(node) ?
74 72 collectNodes(node.childNodes) :
75 73 [node];
76 74
77 75 placeAt(node, this._node, "after");
78 76
79 77 if (isMounted(this._node))
80 78 pending.forEach(n => startupWidgets(n));
81 79
82 80 if (pending.length)
83 81 this._scope.own(() => pending.forEach(destroy));
84 82
85 83 this._renderJob = undefined;
86 } finally {
87 endRender();
88 }
89 }
84 };
90 85
91 86 protected _getDomNode() {
92 87 if (!this._node)
93 88 throw new Error("The instance of the widget isn't created");
94 89 return this._node;
95 90 }
96 91
97 92
98 93 }
@@ -1,200 +1,334
1 1 import { TraceSource } from "@implab/core-amd/log/TraceSource";
2 import { isPromise } from "@implab/core-amd/safe";
3 2 import { id as mid } from "module";
4 3 import { IScope, Scope } from "./Scope";
5 4 import { isNode, isRendition, isWidget } from "./traits";
6 5
7 6 const trace = TraceSource.get(mid);
8 7
9 8 interface Context {
10 9 readonly scope: IScope;
11 10
11 readonly priority: number;
12
12 13 readonly hooks?: (() => void)[];
13 14 }
14 15
15 let _context: Context = {
16 scope: Scope.dummy
16 type RenderTask = {
17 /**
18 * The priority for this task
19 */
20 readonly priority: number,
21
22 /**
23 * The rendering action performed in this task
24 */
25 readonly render: () => void;
26 };
27
28 type Range = {
29 /** minimum value in this range */
30 readonly min: number;
31 /** maximum value in this range */
32 readonly max: number;
17 33 };
18 34
35 // empty range
36 const emptyPriorities: Range = { min: NaN, max: NaN };
37
38 // holds render tasks
39 let _renderQueue: RenderTask[] = [];
40
41 // holds the minimum and the maximum task priorities in the queue. Used to
42 // optimize rendering process is all tasks are with the same priority.
43 let _renderQueuePriorities: Range = emptyPriorities;
44
45 // current context
46 let _context: Context = {
47 scope: Scope.dummy,
48 priority: 0
49 };
50
51 // started render operations
19 52 let _renderCount = 0;
53
54 // next id for render operations
20 55 let _renderId = 1;
56
57 // hooks for render completion, executed when all render operations has
58 // been completed
21 59 let _renderedHooks: (() => void)[] = [];
22 60
23
24 const guard = (cb: () => unknown) => {
61 const guard = (cb: () => void) => {
25 62 try {
26 const result = cb();
27 if (isPromise(result)) {
28 const warn = (ret: unknown) => trace.error("The callback {0} competed asynchronously. result = {1}", cb, ret);
29 result.then(warn, warn);
30 }
63 cb();
31 64 } catch (e) {
32 65 trace.error(e);
33 66 }
34 67 };
35 68
36 69 /**
70 * Creates a new rendition context with the specified parameters and makes it
71 * an active context.
37 72 *
38 * @param scope
39 * @returns
40 */
41 export const beginRender = (scope = getScope()) => {
42 const prev = _context;
43 _renderCount++;
44 const renderId = _renderId++;
45 trace.debug("beginRender [{0}], pending = {1}", renderId, _renderCount);
46 if (_renderCount === 1)
47 onRendering();
48
49 _context = {
50 scope,
51 hooks: []
52 };
53 return endRender(prev, _context, renderId);
54 };
55
56 /**
57 * Method for a deferred rendering. Returns a promise with `beginRender()` function.
58 * Call to `scheduleRender` will save the current context, and will increment pending
59 * operations counter.
60 *
61 * @example
73 * @see getScope
74 * @see getPriority
62 75 *
63 * const begin = await scheduleRender();
64 * const end = begin();
65 * try {
66 * // do some DOM manipulations
67 * } finally {
68 * end();
69 * }
70 *
71 * @param scope
72 * @returns
76 * @param scope The scope for the current context
77 * @param priority The priority for the current context
78 * @returns The function to restore the previous context and execute pending hooks
73 79 */
74 export const scheduleRender = async (scope = getScope()) => {
75 _renderCount++;
76 const renderId = _renderId ++;
77 trace.debug("scheduleRender [{0}], pending = {1}", renderId, _renderCount);
78 if (_renderCount === 1)
79 onRendering();
80
81 await Promise.resolve();
82
83 return () => {
84 trace.debug("beginRender [{0}], pending = {1}", renderId, _renderCount);
80 const enterContext = (scope: IScope, priority: number) => {
85 81 const prev = _context;
86
87 _context = {
88 scope,
89 hooks: []
90 };
91 return endRender(prev, _context, renderId);
92 };
93 };
94
95 /**
96 * Completes render operation
97 */
98 const endRender = (prev: Context, current: Context, renderId: number) => () => {
99 if (_context !== current)
82 const captured = _context = { scope, priority, hooks: [] };
83 return () => {
84 if (_context !== captured)
100 85 trace.error("endRender mismatched beginRender call");
101 86
102 87 const { hooks } = _context;
103 88 if (hooks)
104 89 hooks.forEach(guard);
105 90
91 _context = prev;
92 };
93 };
94
95 /**
96 * Starts the new render operation. When the operation is started the counter
97 * of running operations is increased. If this is the first render operation
98 * then the `onRendering` event is fired.
99 *
100 * @returns The id of the started rendering operation.
101 */
102 const startRender = () => {
103 _renderCount++;
104 if (_renderCount === 1)
105 onRendering();
106
107 return _renderId++;
108 };
109
110 /**
111 * Completes the rendering operation. When the operation is completed the counter
112 * of running operations is decreased. If there is no more running operations left
113 * then the `onRendered` event is fired.
114 *
115 */
116 const completeRender = () => {
106 117 _renderCount--;
107 _context = prev;
108
109 trace.debug("endRender [{0}], pending = {1}", renderId, _renderCount);
110 118 if (_renderCount === 0)
111 119 onRendered();
112 120 };
113 121
122 /**
123 * Invokes the specified within the rendition context. The rendition context
124 * is created for the task invocation and restored after the task is competed.
125 *
126 * @param scope The cope for the rendition context
127 * @param priority The priority for the rendition context
128 * @returns The result returned by the task
129 */
130 export const renderTask = <T>(task: () => T, scope: IScope, priority: number, renderId: number) => {
131 const restoreContext = enterContext(scope, priority);
132 try {
133 trace.debug("beginRender [{0}], priority = {1}", renderId, priority);
134 return task();
135 } finally {
136 trace.debug("endRender [{0}]", renderId);
137 restoreContext();
138 completeRender();
139 }
140 };
141
142 const processRenderQueue = () => {
143 const q = _renderQueue;
144 const { min, max } = _renderQueuePriorities;
145
146 _renderQueue = [];
147 _renderQueuePriorities = emptyPriorities;
148
149 // all tasks scheduled due queue processing will be queued to the next
150 // processRenderQueue iteration
151 if (min !== max) {
152 // if there are tasks with different priorities in the queue
153 trace.debug("Processing render queue, {0} tasks, priorities=[{1}..{2}] ", q.length, min, max);
154 q.sort(({ priority: a }, { priority: b }) => a - b).forEach(({ render }) => guard(render));
155 } else {
156 // all tasks are have same priority
157 trace.debug("Processing render queue, {0} tasks, priority = {1} ", q.length, min);
158 q.forEach(({ render }) => guard(render));
159 }
160
161 if (_renderQueue.length)
162 trace.debug("Render queue is processed, {0} tasks rescheduled", _renderQueue.length);
163 };
164
165 /**
166 * Adds the specified task to the render queue. The task will be added with the
167 * specified priority.
168 *
169 * Render queue contains a list of render tasks. Each task is executed within
170 * its own rendering context with the specified scope. The priority determines
171 * the order in which tasks will be executed where the tasks with lower priority
172 * numbers are executed first.
173 *
174 * When the queue is empty and the task is added then the render queue will be
175 * scheduled for execution. While the current queue is being executed all
176 * enqueued tasks are added to the new queue and processed after the current
177 * execution has been completed.
178 *
179 * @param task The action to execute. This action will be executed with
180 * {@link renderTask} function in its own rendering context.
181 * @param scope The scope used to create a rendering context for the task.
182 * @param priority The priority
183 */
184 export const queueRenderTask = (task: () => void, scope = Scope.dummy, priority = getPriority()) => {
185 const renderId = startRender();
186 trace.debug("scheduleRender [{0}], priority = {1}", renderId, priority);
187
188 const render = () => renderTask(task, scope, priority, renderId);
189
190 if (!_renderQueue.length) {
191 // this is the first task, schedule next render queue processing
192 Promise.resolve().then(processRenderQueue, e => trace.error(e));
193
194 // initialize priorities
195 _renderQueuePriorities = { min: priority, max: priority };
196 } else {
197
198 // update priorities if needed
199 const { min, max } = _renderQueuePriorities;
200 if (priority < min)
201 _renderQueuePriorities = { min: priority, max };
202 else if (priority > max)
203 _renderQueuePriorities = { min, max: priority };
204 }
205
206 _renderQueue.push({ priority, render });
207 };
208
209 /**
210 * Starts the synchronous rendering process with the specified scope and priority.
211 *
212 * @param scope The scope for the current rendition
213 * @param priority The priority for the current scope
214 * @returns The function to complete the current rendering
215 */
216 export const beginRender = (scope = Scope.dummy, priority = 0) => {
217 const renderId = startRender();
218 const restoreContext = enterContext(scope, priority);
219 trace.debug("beginRender [{0}], priority = {1}", renderId, priority);
220
221 return () => {
222 trace.debug("endRender [{0}]", renderId);
223 restoreContext();
224 completeRender();
225 };
226 };
227
114 228 // called when the first beginRender is called for this iteration
115 229 const onRendering = () => {
116 230 trace.log("Rendering started");
117 231 setTimeout(() => {
118 232 if (_renderCount !== 0)
119 233 trace.error("Rendering tasks aren't finished, currently running = {0}", _renderCount);
120 234 });
121 235 };
122 236
123 237 // called when all render operations are complete
124 238 const onRendered = () => {
125 239 trace.log("Rendering compete");
126 240 _renderedHooks.forEach(guard);
127 241 _renderedHooks = [];
128 242 };
129 243
244 /** Returns promise when the rendering has been completed. */
130 245 export const whenRendered = () => new Promise<void>((resolve) => {
131 246 if (_renderCount)
132 247 _renderedHooks.push(resolve);
133 248 else
134 249 resolve();
135 250 });
136 251
252 /**
253 * Registers hook which is called after the render operation is completed. The
254 * hook will be called once only for the current operation.
255 *
256 * @param hook The hook which should be called when rendering is complete.
257 */
137 258 export const renderHook = (hook: () => void) => {
138 259 const { hooks } = _context;
139 260 if (hooks)
140 261 hooks.push(hook);
141 262 else
142 263 guard(hook);
143 264 };
144 265
266 /**
267 * Registers special hook which will be called with the specified state. The
268 * hook is called once after the rendering is complete. When the rendition is
269 * destroyed the hook is called with the undefined parameter.
270 *
271 * This function is used to register `ref` hooks form a tsx rendition.
272 *
273 * @param value The state which will be supplied as a parameter for the hook
274 * @param ref reference hook
275 */
145 276 export const refHook = <T>(value: T, ref: JSX.Ref<T>) => {
146 277 const { hooks, scope } = _context;
147 278 if (hooks)
148 279 hooks.push(() => ref(value));
149 280 else
150 281 guard(() => ref(value));
151 282
152 283 scope.own(() => ref(undefined));
153 284 };
154 285
155 /** Returns the current scope */
286 /** Returns the current scope. Scope is used to track resources bound to the
287 * current rendering. When the rendering is destroyed the scope is cleaned and
288 * all bound resources are released.
289 */
156 290 export const getScope = () => _context.scope;
157 291
292 /**
293 * Returns the current render task priority. This value is used by some renditions
294 * to schedule asynchronous nested updates with lower priority then themselves.
295 */
296 export const getPriority = () => _context.priority;
297
158 298 /** Schedules the rendition to be rendered to the DOM Node
159 299 * @param rendition The rendition to be rendered
160 300 * @param scope The scope
161 301 */
162 export const render = (rendition: unknown, scope = Scope.dummy) => {
163 const complete = beginRender(scope);
164 try {
165 return getItemDom(rendition);
166 } finally {
167 complete();
168 }
169 };
302 export const render = (rendition: unknown, scope = Scope.dummy) =>
303 renderTask(() => getItemDom(rendition), scope, getPriority(), startRender());
170 304
171 305 const emptyFragment = document.createDocumentFragment();
172 306
173 307 /** Renders DOM element for different types of the argument. */
174 308 export const getItemDom = (v: unknown) => {
175 309 if (typeof v === "string" || typeof v === "number" || v instanceof RegExp || v instanceof Date) {
176 310 // primitive types converted to the text nodes
177 311 return document.createTextNode(v.toString());
178 312 } else if (isNode(v)) {
179 313 // nodes are kept as is
180 314 return v;
181 315 } else if (isRendition(v)) {
182 316 // renditions are instantiated
183 317 return v.getDomNode();
184 318 } else if (isWidget(v)) {
185 319 // widgets are converted to it's markup
186 320 return v.domNode;
187 321 } else if (typeof v === "boolean" || v === null || v === undefined) {
188 322 // null | undefined | boolean are removed
189 323 return emptyFragment;
190 324 } else if (v instanceof Array) {
191 325 // arrays will be translated to document fragments
192 326 const fragment = document.createDocumentFragment();
193 327 v.map(item => getItemDom(item))
194 328 .forEach(node => fragment.appendChild(node));
195 329 return fragment;
196 330 } else {
197 331 // bug: explicit error otherwise
198 332 throw new Error(`Invalid parameter: ${String(v)}`);
199 333 }
200 334 };
@@ -1,88 +1,106
1 1 import Memory = require("dojo/store/Memory");
2 2 import Observable = require("dojo/store/Observable");
3 3 import { Appointment, AppointmentRole, Member } from "./Appointment";
4 4 import { Contact } from "./Contact";
5 5 import { Uuid } from "@implab/core-amd/Uuid";
6 6 import { IDestroyable } from "@implab/core-amd/interfaces";
7 7 import { delay } from "@implab/core-amd/safe";
8 8 import { query } from "@implab/djx/store";
9 9
10 10 type AppointmentRecord = Omit<Appointment, "getMembers"> & { id: string };
11 11
12 12 type ContactRecord = Contact;
13 13
14 14 type MemberRecord = Member & { appointmentId: string; };
15 15
16 16 const item = <T, T2>(map: (x: T) => T2) => <U extends { item: T }>({ item, ...props }: U) => ({ item: map(item), ...props });
17 17
18 18
19 19 export class MainContext implements IDestroyable {
20 20 private readonly _appointments = new Observable(new Memory<AppointmentRecord>());
21 21
22 22 private readonly _contacts = new Observable(new Memory<ContactRecord>());
23 23
24 24 private readonly _members = new Observable(new Memory<MemberRecord>());
25 25
26 26 async createAppointment(title: string, startAt: Date, duration: number, members: Member[]) {
27 27 await delay(1000);
28 28 const id = Uuid();
29 29 this._appointments.add({
30 30 id,
31 31 startAt,
32 32 duration,
33 33 title
34 34 });
35 35
36 36 members.forEach(member =>
37 37 this._members.add({
38 38 appointmentId: id,
39 39 ...member
40 40 }, { id: Uuid() }) as void
41 41 );
42 42 }
43 43
44 async removeAppointment(appointmentId: string) {
45 await delay(10);
46 this._members.query({ appointmentId })
47 .map(m => this._members.getIdentity(m))
48 .forEach(id => this._members.remove(id));
49 this._appointments.remove(appointmentId);
50 }
51
44 52 async load() {
45 await Promise.resolve();
46 for (let i = 0; i < 2; i++) {
53 await delay(10);
54 for (let i = 0; i < 5; i++) {
47 55 const id = Uuid();
48 56 this._appointments.add({
49 57 id,
50 58 startAt: new Date(),
51 59 duration: 30,
52 60 title: `Hello ${i+1}`
53 61 });
62
63 for (let ii = 0; ii < 3; ii++)
64
65 this._members.add({
66 appointmentId: id,
67 email: "some@no.mail",
68 name: `Peter ${ii}`,
69 position: "Manager",
70 role: "participant"
71 }, { id: Uuid() });
54 72 }
55 73 }
56 74
57 75 private readonly _queryAppointmentsRx = query(this._appointments);
58 76
59 77 private readonly _queryMembersRx = query(this._members);
60 78
61 79 queryAppointments({ dateFrom, dateTo }: { dateFrom?: Date; dateTo?: Date; } = {}) {
62 80 return this._queryAppointmentsRx(({ startAt }) =>
63 81 (!dateFrom || dateFrom <= startAt) &&
64 82 (!dateTo || startAt <= dateTo)
65 83 ).map(item(this._mapAppointment));
66 84 }
67 85
68 86 async addMember(appointmentId: string, member: Member) {
69 87 await delay(1000);
70 88 this._members.add({
71 89 appointmentId,
72 90 ...member
73 91 });
74 92 }
75 93
76 94 private readonly _mapAppointment = ({ startAt, title, duration, id }: AppointmentRecord) => ({
77 95 id,
78 96 title,
79 97 startAt,
80 98 duration,
81 99 getMembers: (role?: AppointmentRole) => this._queryMembersRx(role ? { appointmentId: id, role } : { appointmentId: id })
82 100 });
83 101
84 102 destroy() {
85 103
86 104 }
87 105
88 106 }
@@ -1,75 +1,79
1 1 import { id as mid } from "module";
2 2 import { BehaviorSubject, Observer, Unsubscribable } from "rxjs";
3 3 import { IDestroyable } from "@implab/core-amd/interfaces";
4 4 import { Observable } from "@implab/djx/observable";
5 5 import { OrderedUpdate } from "@implab/djx/store";
6 6 import { Appointment, Member } from "./Appointment";
7 7 import { MainContext } from "./MainContext";
8 8 import { LocalDate } from "@js-joda/core";
9 9 import { error } from "../logging";
10 10 import { TraceSource } from "@implab/core-amd/log/TraceSource";
11 11 import { whenRendered } from "@implab/djx/tsx/render";
12 12
13 13 const trace = TraceSource.get(mid);
14 14
15 15 export interface State {
16 16 appointments: Observable<OrderedUpdate<Appointment>>;
17 17
18 18 dateTo: LocalDate;
19 19
20 20 dateFrom: LocalDate;
21 21
22 22 title: string;
23 23 }
24 24
25 25 export default class MainModel implements IDestroyable {
26 26 private readonly _state: BehaviorSubject<State>;
27 27
28 28 private readonly _context = new MainContext();
29 29
30 30 constructor() {
31 31 this._state = new BehaviorSubject<State>({
32 32 dateTo: LocalDate.now(),
33 33 dateFrom: LocalDate.now().minusMonths(1),
34 34 appointments: this._context.queryAppointments(),
35 35 title: "Appointments"
36 36 });
37 37 }
38 38 getState() {
39 39 return this._state.getValue();
40 40 }
41 41
42 42 subscribe(observer: Partial<Observer<State>>): Unsubscribable {
43 43 return this._state.subscribe(observer);
44 44 }
45 45
46 46 protected dispatch(command: Partial<State>) {
47 47 const state = this.getState();
48 48 this._state.next({ ...state, ...command });
49 49 }
50 50
51 51 addMember(appointmentId: string, member: Member) {
52 52 this._context.addMember(appointmentId, member).catch(error(trace));
53 53 }
54 54
55 55 addAppointment(title: string, startAt: Date, duration: number) {
56 56 this._context.createAppointment(title,startAt, duration, [])
57 57 .then(() => {
58 58 trace.debug("addAppointment done");
59 59 return whenRendered();
60 60 })
61 61 .then(() => {
62 62 trace.debug("Render dome");
63 63 })
64 64 .catch(error(trace));
65 65 }
66 66
67 removeAppointment(appointmentId: string) {
68 this._context.removeAppointment(appointmentId).catch(error(trace));
69 }
70
67 71
68 72 load() {
69 73 this._context.load().catch(error(trace));
70 74 }
71 75
72 76 destroy() {
73 77 this._context.destroy();
74 78 }
75 79 } No newline at end of file
@@ -1,77 +1,87
1 import { id as mid } from "module";
1 2 import { djbase, djclass } from "@implab/djx/declare";
2 3 import { DjxWidgetBase } from "@implab/djx/tsx/DjxWidgetBase";
3 4 import { attach, bind, createElement, prop, watch, watchFor } from "@implab/djx/tsx";
4 5 import MainModel from "../model/MainModel";
5 6 import { Observable } from "@implab/djx/observable";
6 7 import { OrderedUpdate } from "@implab/djx/store";
7 8 import { Appointment } from "../model/Appointment";
8 9 import { LocalDate } from "@js-joda/core";
9 10 import Button = require("dijit/form/Button");
10 11 import NewAppointment from "./NewAppointment";
12 import { TraceSource } from "@implab/core-amd/log/TraceSource";
13
14 const trace = TraceSource.get(mid);
11 15
12 16 @djclass
13 17 export default class MainWidget extends djbase(DjxWidgetBase) {
14 18
15 19 appointments?: Observable<OrderedUpdate<Appointment>>;
16 20
17 21 model: MainModel;
18 22
19 23 dateTo?: LocalDate;
20 24
21 25 dateFrom?: LocalDate;
22 26
23 27 toolbarNode?: HTMLDivElement;
24 28
25 29 constructor(opts?: Partial<MainWidget> & ThisType<MainWidget>, srcNode?: string | Node) {
26 30 super(opts, srcNode);
27 31
28 32 const model = this.model = new MainModel();
29 33 this.own(model);
30 34 model.subscribe({ next: x => this.set(x) });
31 35 }
32 36
33 37
34 38 render() {
35 39
36 40 return <div className="tundra">
37 41 <h2 ref={bind("innerHTML", prop(this, "title"))} />
38 42 {watch(prop(this, "appointments"), items => items &&
39 43 <ul>
40 44 {watchFor(items, ({ id, title, getMembers }) =>
41 45 <li>{title}
42 46 <ul>
43 47 {watchFor(getMembers(), ({ role, name, position }) =>
44 48 <li className={role}>{name}({position})</li>
45 49 )}
46 50 </ul>
47 51 <div>
48 52 <Button onClick={() => this._onAddMemberClick(id)}>Add member</Button>
53 <Button onClick={() => this._onRemoveAppointmentClick(id)}>Remove appointment</Button>
49 54 </div>
50 55 </li>
51 56 )}
52 57 </ul>
53 58 )}
54 59 <NewAppointment/>
55 60 <div ref={attach(this, "toolbarNode")}>
56 61 <Button onClick={this._onAddAppointmentClick}>Add new appointment</Button>
57 62 </div>
58 63 </div>;
59 64 }
60 65
61 66 load() {
67 trace.log("Loading data");
62 68 this.model.load();
63 69 }
64 70
65 71 private readonly _onAddMemberClick = (appointmentId: string) => {
66 72 this.model.addMember(appointmentId, {
67 73 email: "some-mail",
68 74 name: "Member Name",
69 75 position: "Member position",
70 76 role: "participant"
71 77 });
72 78 };
73 79
80 private readonly _onRemoveAppointmentClick = (appointmentId: string) => {
81 this.model.removeAppointment(appointmentId);
82 };
83
74 84 private readonly _onAddAppointmentClick = () => {
75 85 this.model.addAppointment("Appointment", new Date, 30);
76 86 };
77 87 }
General Comments 0
You need to be logged in to leave comments. Login now