##// END OF EJS Templates
corrected tear down logic handling in observables. Added support for observable query results
cin -
r110:1a190b3a757d v1.4.0 default
parent child
Show More
@@ -0,0 +1,11
1 import { TraceSource } from "@implab/core-amd/log/TraceSource";
2
3 const delegate = <T extends { [p in K]: (...args: unknown[]) => unknown }, K extends string>(target: T, key: K): OmitThisParameter<T[K]> => target[key].bind(target) as OmitThisParameter<T[K]>;
4
5 export const log = (trace: TraceSource) => delegate(trace, "log");
6
7 export const debug = (trace: TraceSource) => delegate(trace, "debug");
8
9 export const warn = (trace: TraceSource) => delegate(trace, "warn");
10
11 export const error = (trace: TraceSource) => delegate(trace, "error");
@@ -25,6 +25,7
25 "eslint-plugin-promise": "^6.0.0",
25 "eslint-plugin-promise": "^6.0.0",
26 "eslint-plugin-react": "^7.29.4",
26 "eslint-plugin-react": "^7.29.4",
27 "requirejs": "2.3.6",
27 "requirejs": "2.3.6",
28 "rxjs": "7.5.6",
28 "tap": "16.3.0",
29 "tap": "16.3.0",
29 "typescript": "4.8.3",
30 "typescript": "4.8.3",
30 "yaml": "~1.7.2"
31 "yaml": "~1.7.2"
@@ -3994,6 +3995,21
3994 "queue-microtask": "^1.2.2"
3995 "queue-microtask": "^1.2.2"
3995 }
3996 }
3996 },
3997 },
3998 "node_modules/rxjs": {
3999 "version": "7.5.6",
4000 "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz",
4001 "integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==",
4002 "dev": true,
4003 "dependencies": {
4004 "tslib": "^2.1.0"
4005 }
4006 },
4007 "node_modules/rxjs/node_modules/tslib": {
4008 "version": "2.4.0",
4009 "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
4010 "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
4011 "dev": true
4012 },
3997 "node_modules/safe-buffer": {
4013 "node_modules/safe-buffer": {
3998 "version": "5.1.2",
4014 "version": "5.1.2",
3999 "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
4015 "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -9572,6 +9588,23
9572 "queue-microtask": "^1.2.2"
9588 "queue-microtask": "^1.2.2"
9573 }
9589 }
9574 },
9590 },
9591 "rxjs": {
9592 "version": "7.5.6",
9593 "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz",
9594 "integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==",
9595 "dev": true,
9596 "requires": {
9597 "tslib": "^2.1.0"
9598 },
9599 "dependencies": {
9600 "tslib": {
9601 "version": "2.4.0",
9602 "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
9603 "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
9604 "dev": true
9605 }
9606 }
9607 },
9575 "safe-buffer": {
9608 "safe-buffer": {
9576 "version": "5.1.2",
9609 "version": "5.1.2",
9577 "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
9610 "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -24,6 +24,7
24 "@types/requirejs": "2.1.31",
24 "@types/requirejs": "2.1.31",
25 "@types/yaml": "1.2.0",
25 "@types/yaml": "1.2.0",
26 "@types/tap": "15.0.7",
26 "@types/tap": "15.0.7",
27 "rxjs": "7.5.6",
27 "dojo": "1.16.0",
28 "dojo": "1.16.0",
28 "@implab/dojo-typings": "1.0.3",
29 "@implab/dojo-typings": "1.0.3",
29 "@typescript-eslint/eslint-plugin": "^5.23.0",
30 "@typescript-eslint/eslint-plugin": "^5.23.0",
@@ -1,3 +1,6
1 import { PromiseOrValue } from "@implab/core-amd/interfaces";
2 import { isPromise } from "@implab/core-amd/safe";
3
1 /**
4 /**
2 * The interface for the consumer of an observable sequence
5 * The interface for the consumer of an observable sequence
3 */
6 */
@@ -19,11 +22,30 export interface Observer<T> {
19 }
22 }
20
23
21 /**
24 /**
22 * The group of functions to feed an observable. This methods are provided to
25 * The group of functions to feed an observable. These methods are provided to
23 * the producer to generate a stream of events.
26 * the producer to generate a stream of events.
24 */
27 */
25 export type Sink<T> = {
28 export type Sink<T> = {
26 [k in keyof Observer<T>]: (this: void, ...args: Parameters<Observer<T>[k]>) => void;
29 /**
30 * Call to send the next element in the sequence
31 */
32 next: (value: T) => void;
33
34 /**
35 * Call to notify about the error occurred in the sequence.
36 */
37 error: (e: unknown) => void;
38
39 /**
40 * Call to signal the end of the sequence.
41 */
42 complete: () => void;
43
44 /**
45 * Checks whether the sink is accepting new elements. It's safe to
46 * send elements to the closed sink.
47 */
48 isClosed: () => boolean;
27 };
49 };
28
50
29 export type Producer<T> = (sink: Sink<T>) => (void | (() => void));
51 export type Producer<T> = (sink: Sink<T>) => (void | (() => void));
@@ -64,6 +86,8 export interface Observable<T> extends S
64 * @param initial
86 * @param initial
65 */
87 */
66 scan<A>(accumulator: (acc: A, value: T) => A, initial: A): Observable<A>;
88 scan<A>(accumulator: (acc: A, value: T) => A, initial: A): Observable<A>;
89
90 cat(...seq: Subscribable<T>[]): Observable<T>;
67 }
91 }
68
92
69 const noop = () => { };
93 const noop = () => { };
@@ -73,75 +97,133 const sink = <T>(consumer: Partial<Obser
73 return {
97 return {
74 next: next ? next.bind(consumer) : noop,
98 next: next ? next.bind(consumer) : noop,
75 error: error ? error.bind(consumer) : noop,
99 error: error ? error.bind(consumer) : noop,
76 complete: complete ? complete.bind(consumer) : noop
100 complete: complete ? complete.bind(consumer) : noop,
101 isClosed: () => false
77 };
102 };
78 };
103 };
79
104
80 const fuse = <T>({ next, error, complete }: Sink<T>) => {
105 /** Wraps the producer to handle tear down logic and subscription management
106 *
107 * @param producer The producer to wrap
108 * @returns The wrapper producer
109 */
110 const fuse = <T>(producer: Producer<T>) => ({ next, error, complete }: Sink<T>) => {
81 let done = false;
111 let done = false;
82 return {
112 let cleanup = noop;
113
114 const _fin = <A extends unknown[]>(fn: (...args: A) => void) =>
115 (...args: A) => done ?
116 void (0) :
117 (done = true, cleanup(), fn(...args));
118
119 const safeSink = {
83 next: (value: T) => { !done && next(value); },
120 next: (value: T) => { !done && next(value); },
84 error: (e: unknown) => { !done && (done = true, error(e)); },
121 error: _fin(error),
85 complete: () => { !done && (done = true, complete()); }
122 complete: _fin(complete),
123 isClosed: () => done
86 };
124 };
125 cleanup = producer(safeSink) ?? noop;
126 return done ?
127 (cleanup(), noop) :
128 _fin(noop);
87 };
129 };
88
130
89 const _observe = <T>(producer: Producer<T>): Observable<T> => ({
131 const _observe = <T>(producer: Producer<T>): Observable<T> => ({
90 subscribe: (consumer: Partial<Observer<T>>) => ({
132 subscribe: (consumer: Partial<Observer<T>>) => ({
91 unsubscribe: producer(sink(consumer)) ?? noop
133 unsubscribe: producer(sink(consumer)) ?? noop
92 }),
134 }),
93 map: (mapper) => _observe(({ next, error, complete }) =>
135 map: (mapper) => _observe(({ next, ...rest }) =>
94 producer({
136 producer({
95 next: next !== noop ? (v: T) => next(mapper(v)) : noop,
137 next: next !== noop ? (v: T) => next(mapper(v)) : noop,
96 error,
138 ...rest
97 complete
139 })
140 ),
141 filter: (predicate) => _observe(({ next, ...rest }) =>
142 producer({
143 next: next !== noop ? (v: T) => predicate(v) ? next(v) : void (0) : noop,
144 ...rest
98 })
145 })
99 ),
146 ),
100 filter: (predicate) => _observe(({ next, error, complete }) =>
147 scan: (accumulator, initial) => _observe(({ next, ...rest }) => {
101 producer({
102 next: next !== noop ?
103 (v: T) => predicate(v) ? next(v) : void (0) : noop,
104 error,
105 complete
106 })
107 ),
108 scan: (accumulator, initial) => _observe(({ next, error, complete }) => {
109 let _acc = initial;
148 let _acc = initial;
110 return producer({
149 return producer({
111 next: next !== noop ?
150 next: next !== noop ? (v: T) => next(_acc = accumulator(_acc, v)) : noop,
112 (v: T) => next(_acc = accumulator(_acc, v)) : noop,
151 ...rest
113 error,
114 complete
115 });
152 });
153 }),
154
155 cat: (...seq) => _observe(({ next, complete: final, ...rest }) => {
156 let cleanup: () => void;
157 const complete = () => {
158 const continuation = seq.shift();
159 if (continuation) {
160 // if we have a next sequence, subscribe to it
161 const subscription = continuation.subscribe({ next, complete, ...rest });
162 cleanup = subscription.unsubscribe.bind(subscription);
163 } else {
164 // otherwise notify the consumer about completion
165 final();
166 }
167 };
168
169 cleanup = producer({ next, complete, ...rest }) ?? noop;
170
171 return () => cleanup();
116 })
172 })
117 });
173 });
118
174
119 export const observe = <T>(producer: Producer<T>): Observable<T> => ({
175 export interface OrderUpdate<T> {
120 subscribe: (consumer: Partial<Observer<T>>) => ({
176 /** The item is being updated */
121 unsubscribe: producer(fuse(sink(consumer))) ?? noop
177 item: T;
122 }),
178
123 map: (mapper) => _observe(({ next, error, complete }) =>
179 /** The previous index of the item, -1 in case it is inserted */
124 producer(fuse({
180 prevIndex: number;
125 next: next !== noop ?
181
126 (v: T) => next(mapper(v)) : noop,
182 /** The new index of the item, -1 in case it is deleted */
127 error,
183 newIndex: number;
128 complete
184 }
129 }))
185
130 ),
186 interface ObservableResults<T> {
131 filter: (predicate) => _observe(({ next, error, complete }) =>
187 /**
132 producer(fuse({
188 * Allows observation of results
133 next: next !== noop ?
189 */
134 (v: T) => predicate(v) ? next(v) : void (0) : noop,
190 observe(listener: (object: T, previousIndex: number, newIndex: number) => void, includeUpdates?: boolean): {
135 error,
191 remove(): void;
136 complete
192 };
137 }))
193 }
138 ),
194
139 scan: (accumulator, initial) => observe(({ next, error, complete }) => {
195 interface Queryable<T, A extends unknown[]> {
140 let _acc = initial;
196 query(...args: A): PromiseOrValue<T[]>;
141 return producer(fuse({
197 }
142 next: next !== noop ? (v: T) => next(_acc = accumulator(_acc, v)) : noop,
198
143 error,
199 export const isObservableResults = <T>(v: object): v is ObservableResults<T> =>
144 complete
200 v && (typeof (v as { observe?: unknown; }).observe === "function");
145 }));
201
146 })
202 export const observe = <T>(producer: Producer<T>) => _observe(fuse(producer));
203
204 export const empty = observe<never>(({ complete }) => complete());
205
206 export const query = <T, A extends unknown[]>(store: Queryable<T, A>) =>
207 (...args: A) => {
208 return observe<OrderUpdate<T>>(({ next, complete, error }) => {
209 try {
210 const results = store.query(...args);
211 if (isPromise(results)) {
212 results.then(items => items.forEach((item, newIndex) => next({ item, newIndex, prevIndex: -1 })))
213 .then(undefined, error);
214 } else {
215 results.forEach((item, newIndex) => next({ item, newIndex, prevIndex: -1 }));
216 }
217
218 if (isObservableResults<T>(results)) {
219 const h = results.observe((item, prevIndex, newIndex) => next({ item, prevIndex, newIndex }));
220 return () => h.remove();
221 } else {
222 complete();
223 }
224 } catch (err) {
225 error(err);
226 }
147 });
227 });
228
229 };
@@ -7,7 +7,7 import Stateful = require("dojo/Stateful
7 import _WidgetBase = require("dijit/_WidgetBase");
7 import _WidgetBase = require("dijit/_WidgetBase");
8 import { DjxWidgetBase } from "./tsx/DjxWidgetBase";
8 import { DjxWidgetBase } from "./tsx/DjxWidgetBase";
9 import { WatchRendition } from "./tsx/WatchRendition";
9 import { WatchRendition } from "./tsx/WatchRendition";
10 import { Observable, observe, Subscribable } from "./observable";
10 import { Observable, observe, OrderUpdate, Subscribable } from "./observable";
11 import djAttr = require("dojo/dom-attr");
11 import djAttr = require("dojo/dom-attr");
12 import djClass = require("dojo/dom-class");
12 import djClass = require("dojo/dom-class");
13 import { AnimationAttrs, WatchForRendition } from "./tsx/WatchForRendition";
13 import { AnimationAttrs, WatchForRendition } from "./tsx/WatchForRendition";
@@ -45,17 +45,6 export interface EventSelector {
45 target: HTMLElement;
45 target: HTMLElement;
46 }
46 }
47
47
48 export interface QueryResultUpdate<T> {
49 /** The item is being updated */
50 item: T;
51
52 /** The previous index of the item, -1 in case it is inserted */
53 prevIndex: number;
54
55 /** The new index of the item, -1 in case it is deleted */
56 newIndex: number;
57 }
58
59 export type DojoMouseEvent<T = unknown> = MouseEvent & EventSelector & EventDetails<T>;
48 export type DojoMouseEvent<T = unknown> = MouseEvent & EventSelector & EventDetails<T>;
60
49
61 type StatefulProps<T> = T extends Stateful<infer A> ? A :
50 type StatefulProps<T> = T extends Stateful<infer A> ? A :
@@ -112,7 +101,7 export function watch(
112 }
101 }
113 }
102 }
114
103
115 export const watchFor = <T>(source: T[] | Subscribable<QueryResultUpdate<T>>, render: (item: T, index: number) => unknown, opts: AnimationAttrs = {}) => {
104 export const watchFor = <T>(source: T[] | Subscribable<OrderUpdate<T>>, render: (item: T, index: number) => unknown, opts: AnimationAttrs = {}) => {
116 return new WatchForRendition({
105 return new WatchForRendition({
117 ...opts,
106 ...opts,
118 subject: source,
107 subject: source,
@@ -9,8 +9,7 import { collectNodes, destroy as safeDe
9 import { IDestroyable } from "@implab/core-amd/interfaces";
9 import { IDestroyable } from "@implab/core-amd/interfaces";
10 import { play } from "../play";
10 import { play } from "../play";
11 import * as fx from "dojo/fx";
11 import * as fx from "dojo/fx";
12 import { isSubsribable, Subscribable } from "../observable";
12 import { isObservableResults, isSubsribable, OrderUpdate, Subscribable } from "../observable";
13 import { QueryResultUpdate } from "../tsx";
14
13
15 const trace = TraceSource.get(mid);
14 const trace = TraceSource.get(mid);
16
15
@@ -22,16 +21,7 interface ItemRendition {
22 destroy(): void;
21 destroy(): void;
23 }
22 }
24
23
25 interface ObservableResults<T> {
24 interface RenderTask<T> extends OrderUpdate<T> {
26 /**
27 * Allows observation of results
28 */
29 observe(listener: (object: T, previousIndex: number, newIndex: number) => void, includeUpdates?: boolean): {
30 remove(): void;
31 };
32 }
33
34 interface RenderTask<T> extends QueryResultUpdate<T> {
35 animate: boolean;
25 animate: boolean;
36 }
26 }
37
27
@@ -44,13 +34,11 export interface AnimationAttrs {
44 }
34 }
45
35
46 export interface WatchForRenditionAttrs<T> extends AnimationAttrs {
36 export interface WatchForRenditionAttrs<T> extends AnimationAttrs {
47 subject: T[] | Subscribable<QueryResultUpdate<T>>;
37 subject: T[] | Subscribable<OrderUpdate<T>>;
48
38
49 component: (arg: T, index: number) => unknown;
39 component: (arg: T, index: number) => unknown;
50 }
40 }
51
41
52 const isObservable = <T>(v: ArrayLike<T>): v is ArrayLike<T> & ObservableResults<T> =>
53 v && (typeof (v as { observe?: unknown; }).observe === "function");
54
42
55 const noop = () => { };
43 const noop = () => { };
56
44
@@ -72,7 +60,7 export class WatchForRendition<T> extend
72
60
73 private readonly _itemRenditions: ItemRendition[] = [];
61 private readonly _itemRenditions: ItemRendition[] = [];
74
62
75 private readonly _subject: T[] | Subscribable<QueryResultUpdate<T>>;
63 private readonly _subject: T[] | Subscribable<OrderUpdate<T>>;
76
64
77 private readonly _renderTasks: RenderTask<T>[] = [];
65 private readonly _renderTasks: RenderTask<T>[] = [];
78
66
@@ -109,7 +97,7 export class WatchForRendition<T> extend
109 const result = this._subject;
97 const result = this._subject;
110
98
111 if (result) {
99 if (result) {
112 if (isSubsribable<QueryResultUpdate<T>>(result)) {
100 if (isSubsribable<OrderUpdate<T>>(result)) {
113 let animate = false;
101 let animate = false;
114 const subscription = result.subscribe({
102 const subscription = result.subscribe({
115 next: ({ item, prevIndex, newIndex }) => this._onItemUpdated({ item, prevIndex, newIndex, animate })
103 next: ({ item, prevIndex, newIndex }) => this._onItemUpdated({ item, prevIndex, newIndex, animate })
@@ -117,7 +105,7 export class WatchForRendition<T> extend
117 scope.own(subscription);
105 scope.own(subscription);
118 animate = this._animate;
106 animate = this._animate;
119 } else {
107 } else {
120 if (isObservable(result))
108 if (isObservableResults<T>(result))
121 scope.own(result.observe((item, prevIndex, newIndex) => this._onItemUpdated({ item, prevIndex, newIndex, animate: false }), true));
109 scope.own(result.observe((item, prevIndex, newIndex) => this._onItemUpdated({ item, prevIndex, newIndex, animate: false }), true));
122
110
123 for (let i = 0, n = result.length; i < n; i++)
111 for (let i = 0, n = result.length; i < n; i++)
@@ -49,4 +49,4 subj1
49
49
50 t.equal(consumer2.value, 8, "Should map");
50 t.equal(consumer2.value, 8, "Should map");
51 t.equal(maps, 3, "The map chain should not be executed after completion");
51 t.equal(maps, 3, "The map chain should not be executed after completion");
52 t.ok(consumer2.completed, "The completion signal should pass through"); No newline at end of file
52 t.ok(consumer2.completed, "The completion signal should pass through");
@@ -94,6 +94,9 task copyModules(type: Copy) {
94
94
95 pack("@implab/djx")
95 pack("@implab/djx")
96 pack("@implab/core-amd")
96 pack("@implab/core-amd")
97 into("@js-joda/core") {
98 from(npm.module("@js-joda/core/dist"))
99 }
97 pack("dojo")
100 pack("dojo")
98 pack("dijit")
101 pack("dijit")
99 into("rxjs") {
102 into("rxjs") {
@@ -6,6 +6,7
6 "": {
6 "": {
7 "name": "@implab/djx-playground",
7 "name": "@implab/djx-playground",
8 "dependencies": {
8 "dependencies": {
9 "@js-joda/core": "5.3.1",
9 "dijit": "1.17.3",
10 "dijit": "1.17.3",
10 "dojo": "1.17.3",
11 "dojo": "1.17.3",
11 "requirejs": "2.3.6",
12 "requirejs": "2.3.6",
@@ -121,6 +122,11
121 "integrity": "sha512-/lbcMCHdRoHJLKFcT8xdk1KbGazSlb1pGSDJ406io7iMenPm/XbJYcUti+VzXnn71zOJ8aYpGT12T5L0rfOZNA==",
122 "integrity": "sha512-/lbcMCHdRoHJLKFcT8xdk1KbGazSlb1pGSDJ406io7iMenPm/XbJYcUti+VzXnn71zOJ8aYpGT12T5L0rfOZNA==",
122 "dev": true
123 "dev": true
123 },
124 },
125 "node_modules/@js-joda/core": {
126 "version": "5.3.1",
127 "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.3.1.tgz",
128 "integrity": "sha512-iHHyIRLEfXLqBN+BkyH8u8imMYr4ihRbFDEk8toqTwUECETVQFCTh2U59Sw2oMoRVaS3XRIb7pyCulltq2jFVA=="
129 },
124 "node_modules/@nodelib/fs.scandir": {
130 "node_modules/@nodelib/fs.scandir": {
125 "version": "2.1.5",
131 "version": "2.1.5",
126 "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
132 "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -2786,6 +2792,11
2786 "integrity": "sha512-/lbcMCHdRoHJLKFcT8xdk1KbGazSlb1pGSDJ406io7iMenPm/XbJYcUti+VzXnn71zOJ8aYpGT12T5L0rfOZNA==",
2792 "integrity": "sha512-/lbcMCHdRoHJLKFcT8xdk1KbGazSlb1pGSDJ406io7iMenPm/XbJYcUti+VzXnn71zOJ8aYpGT12T5L0rfOZNA==",
2787 "dev": true
2793 "dev": true
2788 },
2794 },
2795 "@js-joda/core": {
2796 "version": "5.3.1",
2797 "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.3.1.tgz",
2798 "integrity": "sha512-iHHyIRLEfXLqBN+BkyH8u8imMYr4ihRbFDEk8toqTwUECETVQFCTh2U59Sw2oMoRVaS3XRIb7pyCulltq2jFVA=="
2799 },
2789 "@nodelib/fs.scandir": {
2800 "@nodelib/fs.scandir": {
2790 "version": "2.1.5",
2801 "version": "2.1.5",
2791 "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
2802 "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -2,6 +2,7
2 "name": "@implab/djx-playground",
2 "name": "@implab/djx-playground",
3 "private": true,
3 "private": true,
4 "dependencies": {
4 "dependencies": {
5 "@js-joda/core": "5.3.1",
5 "dijit": "1.17.3",
6 "dijit": "1.17.3",
6 "dojo": "1.17.3",
7 "dojo": "1.17.3",
7 "requirejs": "2.3.6",
8 "requirejs": "2.3.6",
@@ -10,6 +10,11 requirejs.config({
10 name: "rxjs",
10 name: "rxjs",
11 location: "rxjs",
11 location: "rxjs",
12 main: "rxjs.umd.min"
12 main: "rxjs.umd.min"
13 },
14 {
15 name: "@js-joda/core",
16 location: "@js-joda/core",
17 main: "js-joda"
13 }
18 }
14 ],
19 ],
15 deps: ["app"]
20 deps: ["app"]
@@ -4,4 +4,5 import "@implab/djx/css!dijit/themes/dij
4 import "@implab/djx/css!dijit/themes/tundra/tundra.css";
4 import "@implab/djx/css!dijit/themes/tundra/tundra.css";
5
5
6 const w = new MainWidget();
6 const w = new MainWidget();
7 w.placeAt(document.body); No newline at end of file
7 w.placeAt(document.body);
8 w.load();
@@ -1,12 +1,14
1 import { Contact } from "./Contact";
1 import { Contact } from "./Contact";
2
2
3 type AppointmentRole = "organizer" | "speaker" | "participant";
3 export type AppointmentRole = "organizer" | "speaker" | "participant";
4
4
5 export interface Member extends Contact {
5 export interface Member extends Contact {
6 role: AppointmentRole;
6 role: AppointmentRole;
7 }
7 }
8
8
9 export interface Appointment {
9 export interface Appointment {
10 id: string;
11
10 title: string;
12 title: string;
11
13
12 startAt: Date;
14 startAt: Date;
@@ -1,11 +1,11
1 import Memory = require("dojo/store/Memory");
1 import Memory = require("dojo/store/Memory");
2 import Observable = require("dojo/store/Observable");
2 import Observable = require("dojo/store/Observable");
3 import { Appointment, Member } from "./Appointment";
3 import { Appointment, AppointmentRole, Member } from "./Appointment";
4 import { Contact } from "./Contact";
4 import { Contact } from "./Contact";
5 import { Uuid } from "@implab/core-amd/Uuid";
5 import { Uuid } from "@implab/core-amd/Uuid";
6 import { Observable as RxjsObservable } from "rxjs";
6 import { query } from "@implab/djx/observable";
7 import { QueryResultUpdate } from "@implab/djx/tsx";
7 import { IDestroyable } from "@implab/core-amd/interfaces";
8 import {isPromise} from "@implab/core-amd/safe";
8 import { delay } from "@implab/core-amd/safe";
9
9
10 type AppointmentRecord = Omit<Appointment, "getMembers"> & {id: string};
10 type AppointmentRecord = Omit<Appointment, "getMembers"> & { id: string };
11
11
@@ -13,76 +13,21 type ContactRecord = Contact;
13
13
14 type MemberRecord = Member & { appointmentId: string; };
14 type MemberRecord = Member & { appointmentId: string; };
15
15
16 export interface ObservableResults<T> {
16 const item = <T, T2>(map: (x: T) => T2) => <U extends { item: T }>({ item, ...props }: U) => ({ item: map(item), ...props });
17 /**
18 * Allows observation of results
19 */
20 observe(listener: (object: T, previousIndex: number, newIndex: number) => void, includeUpdates?: boolean): {
21 remove(): void;
22 };
23 }
24
17
25
18
26 export function isObservable<T>(v: unknown): v is ObservableResults<T> {
19 export class MainContext implements IDestroyable {
27 return !!v && (typeof (v as {observe?: unknown}).observe === "function");
28 }
29
30 export function observe<T>(results: T[], includeObjectUpdates?: boolean): RxjsObservable<QueryResultUpdate<T>>;
31 export function observe<T>(results: PromiseLike<T[]>, includeObjectUpdates?: boolean): PromiseLike<RxjsObservable<QueryResultUpdate<T>>>;
32 export function observe(results: unknown[] | PromiseLike<unknown[]>, includeObjectUpdates = true) {
33 // results может быть асинхронным, т.е. до завершения
34 // получения результатов store может быть обновлен. В любом
35 // случае, если между подключением хотя бы одного наблюдателя
36 // была выполнена команда обновления, results считается устаревшим
37 // и не может быть использован для отслеживания обновлений.
38 // Конкретно с dojo/store/Observable тут вообще возникает проблема:
39 // 1. Синхронные store типа Memory будут давать ошибку на методах
40 // обновления (add,put,remove)
41 // 2. Асинхронные store типа JsonRest будут выдавать предупреждения
42 // о необработанной ошибке в Promise при обращении к методам
43 // обновления (add,put,remove)
44
45 const _subscribe = (items: unknown[]) => new RxjsObservable<QueryResultUpdate<unknown>>(subscriber => {
46 items
47 .forEach((value, newIndex) => subscriber.next({ item: value, newIndex, prevIndex: -1}));
48
49 try {
50 if (isObservable(results)) {
51 const h = results.observe(
52 (value, prevIndex, newIndex) => subscriber.next({
53 item: value,
54 prevIndex,
55 newIndex
56 }),
57 includeObjectUpdates
58 );
59
60 return () => { h.remove(); };
61 }
62 } catch (err) {
63 subscriber.error(err);
64 }
65 });
66
67 return isPromise(results) ?
68 results.then(_subscribe) :
69 _subscribe(results || []);
70 }
71
72
73
74
75 export class MainContext {
76 private readonly _appointments = new Observable(new Memory<AppointmentRecord>());
20 private readonly _appointments = new Observable(new Memory<AppointmentRecord>());
77
21
78 private readonly _contacts = new Observable(new Memory<ContactRecord>());
22 private readonly _contacts = new Observable(new Memory<ContactRecord>());
79
23
80 private readonly _members = new Observable(new Memory<MemberRecord>());
24 private readonly _members = new Observable(new Memory<MemberRecord>());
81
25
82 createAppointment(title: string, startAt: Date, duration: number, members: Member[]) {
26 async createAppointment(title: string, startAt: Date, duration: number, members: Member[]) {
27 await delay(1000);
83 const id = Uuid();
28 const id = Uuid();
84 this._appointments.add({
29 this._appointments.add({
85 id: Uuid(),
30 id,
86 startAt,
31 startAt,
87 duration,
32 duration,
88 title
33 title
@@ -96,12 +41,31 export class MainContext {
96 );
41 );
97 }
42 }
98
43
99 queryAppointments(dateFrom: Date, dateTo: Date) {
44 queryAppointments({ dateFrom, dateTo }: { dateFrom?: Date; dateTo?: Date; } = {}) {
100 //this._appointments.query().map()
45 return query(this._appointments)(({ startAt }) =>
46 (!dateFrom || dateFrom <= startAt) &&
47 (!dateTo || startAt <= dateTo)
48 ).map(item(this._mapAppointment));
49 }
50
51 async addMember(appointmentId: string, member: Member) {
52 await delay(1000);
53 this._members.add({
54 appointmentId,
55 ...member
56 });
101 }
57 }
102
58
103 private readonly _mapAppointment = ({startAt, title, duration, id}: AppointmentRecord) => ({
59 private readonly _mapAppointment = ({ startAt, title, duration, id }: AppointmentRecord) => ({
104
60 id,
61 title,
62 startAt,
63 duration,
64 getMembers: (role?: AppointmentRole) => this._members.query(role ? { appointmentId: id, role } : { appointmentId: id })
105 });
65 });
106
66
67 destroy() {
68
107 }
69 }
70
71 }
@@ -1,20 +1,37
1 import { BehaviorSubject, Observer, Unsubscribable, Subscribable } from "rxjs";
1 import { id as mid } from "module";
2 import { IDestroyable} from "@implab/core-amd/interfaces"
2 import { BehaviorSubject, Observer, Unsubscribable } from "rxjs";
3
3 import { IDestroyable } from "@implab/core-amd/interfaces";
4 interface State {
4 import { OrderUpdate, Observable } from "@implab/djx/observable";
5 color: string;
5 import { Appointment, Member } from "./Appointment";
6 import { MainContext } from "./MainContext";
7 import { LocalDate } from "@js-joda/core";
8 import { error } from "../logging";
9 import { TraceSource } from "@implab/core-amd/log/TraceSource";
6
10
7 label: string;
11 const trace = TraceSource.get(mid);
12
13 export interface State {
14 appointments: Observable<OrderUpdate<Appointment>>;
8
15
9 current: number;
16 dateTo: LocalDate;
10
17
11 max: number;
18 dateFrom: LocalDate;
19
20 title: string;
12 }
21 }
13
22
14 export default class MainModel implements IDestroyable {
23 export default class MainModel implements IDestroyable {
15 private readonly _state: BehaviorSubject<State>;
24 private readonly _state: BehaviorSubject<State>;
16 constructor(initialState: State) {
25
17 this._state = new BehaviorSubject(initialState);
26 private readonly _context = new MainContext();
27
28 constructor() {
29 this._state = new BehaviorSubject<State>({
30 dateTo: LocalDate.now(),
31 dateFrom: LocalDate.now().minusMonths(1),
32 appointments: this._context.queryAppointments(),
33 title: "Appointments"
34 });
18 }
35 }
19 getState() {
36 getState() {
20 return this._state.getValue();
37 return this._state.getValue();
@@ -29,6 +46,18 export default class MainModel implement
29 this._state.next({...state, ... command});
46 this._state.next({ ...state, ...command });
30 }
47 }
31
48
32 load() { }
49 addMember(appointmentId: string, member: Member) {
33 destroy() { }
50 this._context.addMember(appointmentId, member).catch(error(trace));
51 }
52
53 addAppointment(title: string, startAt: Date, duration: number) {
54 this._context.createAppointment(title,startAt, duration, []).catch(error(trace));
55 }
56
57 load() {
58 }
59
60 destroy() {
61 this._context.destroy();
62 }
34 } No newline at end of file
63 }
@@ -1,81 +1,72
1 import { djbase, djclass } from "@implab/djx/declare";
1 import { djbase, djclass } from "@implab/djx/declare";
2 import { DjxWidgetBase } from "@implab/djx/tsx/DjxWidgetBase";
2 import { DjxWidgetBase } from "@implab/djx/tsx/DjxWidgetBase";
3 import { createElement, watch, prop, attach, all, bind, toggleClass } from "@implab/djx/tsx";
3 import { bind, createElement, prop, watch, watchFor } from "@implab/djx/tsx";
4 import ProgressBar from "./ProgressBar";
4 import MainModel from "../model/MainModel";
5 import { OrderUpdate, Observable } from "@implab/djx/observable";
6 import { Appointment } from "../model/Appointment";
7 import { LocalDate } from "@js-joda/core";
5 import Button = require("dijit/form/Button");
8 import Button = require("dijit/form/Button");
6 import { interval } from "rxjs";
7
8 const Counter = ({ children }: { children: unknown[] }) => <span>Counter: {children}</span>;
9
9
10 @djclass
10 @djclass
11 export default class MainWidget extends djbase(DjxWidgetBase) {
11 export default class MainWidget extends djbase(DjxWidgetBase) {
12
12
13 titleNode?: HTMLHeadingElement;
13 appointments?: Observable<OrderUpdate<Appointment>>;
14
15 model: MainModel;
14
16
15 progressBar?: ProgressBar;
17 dateTo?: LocalDate;
16
18
17 count = 0;
19 dateFrom?: LocalDate;
18
20
19 showCounter = false;
21 constructor(opts?: Partial<MainWidget> & ThisType<MainWidget>, srcNode?: string | Node) {
22 super(opts, srcNode);
20
23
21 counterNode?: HTMLInputElement;
24 const model = this.model = new MainModel();
25 this.own(model);
26 model.subscribe({ next: x => this.set(x) });
27 }
22
28
23 paused = false;
24
29
25 render() {
30 render() {
26
31
27 return <div className="tundra">
32 return <div className="tundra">
28 <h2 ref={attach(this, "titleNode")}>Hi!</h2>
33 <h2 ref={bind("innerHTML", prop(this, "title"))} />
29 <section style={{ padding: "10px" }}>
34 {watch(prop(this, "appointments"), items => items &&
30 {watch(prop(this, "showCounter"), flag => flag &&
35 <ul>
31 [
36 {watchFor(items, ({ id, title, getMembers }) =>
32 <Counter><input ref={all(
37 <li>{title}
33 bind("value", prop(this, "count")
38 <ul>
34 .map(x => x/10)
39 {watchFor(getMembers(), ({ role, name, position }) =>
35 ),
40 <li className={role}>{name}({position})</li>
36 attach(this, "counterNode")
37 )} /> <span>s</span></Counter>,
38 " | ",
39 <span ref={bind("innerHTML", interval(1000))}></span>,
40 " | ",
41 <Button
42 ref={all(
43 bind("label", prop(this, "paused")
44 .map(x => x ? "Unpause" : "Pause")
45 ),
46 toggleClass("paused", prop(this,"paused"))
47 )}
41 )}
48 onClick={this._onPauseClick}
42 </ul>
49 />
43 <div>
50 ]
44 <Button onClick={() => this._onAddMemberClick(id)}>Add member</Button>
51
45 </div>
46 </li>
52 )}
47 )}
53 </section>
48 </ul>
54 <Button onClick={this._onToggleCounterClick}>Toggle counter</Button>
49 )}
50 <div>
51 <Button onClick={this._onAddAppointmentClick}>Add new appointment</Button>
52 </div>
55 </div>;
53 </div>;
56 }
54 }
57
55
58 postCreate(): void {
56 load() {
59 super.postCreate();
57 this.model.load();
60
61 const h = setInterval(
62 () => {
63 this.set("count", this.count + 1);
64 },
65 100
66 );
67 this.own({
68 destroy: () => {
69 clearInterval(h);
70 }
71 });
72 }
58 }
73
59
74 private readonly _onPauseClick = () => {
60 private readonly _onAddMemberClick = (appointmentId: string) => {
75 this.set("paused", !this.paused);
61 this.model.addMember(appointmentId, {
62 email: "some-mail",
63 name: "Member Name",
64 position: "Member position",
65 role: "participant"
66 });
76 };
67 };
77
68
78 private readonly _onToggleCounterClick = () => {
69 private readonly _onAddAppointmentClick = () => {
79 this.set("showCounter", !this.showCounter);
70 this.model.addAppointment("Appointment", new Date, 30);
80 };
71 };
81 }
72 }
@@ -11,6 +11,8
11 "@implab/djx",
11 "@implab/djx",
12 "@implab/dojo-typings"
12 "@implab/dojo-typings"
13 ],
13 ],
14 "skipLibCheck": true
14 "skipLibCheck": true,
15 "target": "ES5",
16 "lib": ["ES2015"]
15 }
17 }
16 } No newline at end of file
18 }
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now