##// 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 25 "eslint-plugin-promise": "^6.0.0",
26 26 "eslint-plugin-react": "^7.29.4",
27 27 "requirejs": "2.3.6",
28 "rxjs": "7.5.6",
28 29 "tap": "16.3.0",
29 30 "typescript": "4.8.3",
30 31 "yaml": "~1.7.2"
@@ -3994,6 +3995,21
3994 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 4013 "node_modules/safe-buffer": {
3998 4014 "version": "5.1.2",
3999 4015 "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -9572,6 +9588,23
9572 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 9608 "safe-buffer": {
9576 9609 "version": "5.1.2",
9577 9610 "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -24,6 +24,7
24 24 "@types/requirejs": "2.1.31",
25 25 "@types/yaml": "1.2.0",
26 26 "@types/tap": "15.0.7",
27 "rxjs": "7.5.6",
27 28 "dojo": "1.16.0",
28 29 "@implab/dojo-typings": "1.0.3",
29 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 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 26 * the producer to generate a stream of events.
24 27 */
25 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 51 export type Producer<T> = (sink: Sink<T>) => (void | (() => void));
@@ -64,6 +86,8 export interface Observable<T> extends S
64 86 * @param initial
65 87 */
66 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 93 const noop = () => { };
@@ -73,75 +97,133 const sink = <T>(consumer: Partial<Obser
73 97 return {
74 98 next: next ? next.bind(consumer) : noop,
75 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 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 120 next: (value: T) => { !done && next(value); },
84 error: (e: unknown) => { !done && (done = true, error(e)); },
85 complete: () => { !done && (done = true, complete()); }
121 error: _fin(error),
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 131 const _observe = <T>(producer: Producer<T>): Observable<T> => ({
90 132 subscribe: (consumer: Partial<Observer<T>>) => ({
91 133 unsubscribe: producer(sink(consumer)) ?? noop
92 134 }),
93 map: (mapper) => _observe(({ next, error, complete }) =>
135 map: (mapper) => _observe(({ next, ...rest }) =>
94 136 producer({
95 137 next: next !== noop ? (v: T) => next(mapper(v)) : noop,
96 error,
97 complete
138 ...rest
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 }) =>
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 }) => {
147 scan: (accumulator, initial) => _observe(({ next, ...rest }) => {
109 148 let _acc = initial;
110 149 return producer({
111 next: next !== noop ?
112 (v: T) => next(_acc = accumulator(_acc, v)) : noop,
113 error,
114 complete
150 next: next !== noop ? (v: T) => next(_acc = accumulator(_acc, v)) : noop,
151 ...rest
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> => ({
120 subscribe: (consumer: Partial<Observer<T>>) => ({
121 unsubscribe: producer(fuse(sink(consumer))) ?? noop
122 }),
123 map: (mapper) => _observe(({ next, error, complete }) =>
124 producer(fuse({
125 next: next !== noop ?
126 (v: T) => next(mapper(v)) : noop,
127 error,
128 complete
129 }))
130 ),
131 filter: (predicate) => _observe(({ next, error, complete }) =>
132 producer(fuse({
133 next: next !== noop ?
134 (v: T) => predicate(v) ? next(v) : void (0) : noop,
135 error,
136 complete
137 }))
138 ),
139 scan: (accumulator, initial) => observe(({ next, error, complete }) => {
140 let _acc = initial;
141 return producer(fuse({
142 next: next !== noop ? (v: T) => next(_acc = accumulator(_acc, v)) : noop,
143 error,
144 complete
145 }));
146 })
147 });
175 export interface OrderUpdate<T> {
176 /** The item is being updated */
177 item: T;
178
179 /** The previous index of the item, -1 in case it is inserted */
180 prevIndex: number;
181
182 /** The new index of the item, -1 in case it is deleted */
183 newIndex: number;
184 }
185
186 interface ObservableResults<T> {
187 /**
188 * Allows observation of results
189 */
190 observe(listener: (object: T, previousIndex: number, newIndex: number) => void, includeUpdates?: boolean): {
191 remove(): void;
192 };
193 }
194
195 interface Queryable<T, A extends unknown[]> {
196 query(...args: A): PromiseOrValue<T[]>;
197 }
198
199 export const isObservableResults = <T>(v: object): v is ObservableResults<T> =>
200 v && (typeof (v as { observe?: unknown; }).observe === "function");
201
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 }
227 });
228
229 };
@@ -7,7 +7,7 import Stateful = require("dojo/Stateful
7 7 import _WidgetBase = require("dijit/_WidgetBase");
8 8 import { DjxWidgetBase } from "./tsx/DjxWidgetBase";
9 9 import { WatchRendition } from "./tsx/WatchRendition";
10 import { Observable, observe, Subscribable } from "./observable";
10 import { Observable, observe, OrderUpdate, Subscribable } from "./observable";
11 11 import djAttr = require("dojo/dom-attr");
12 12 import djClass = require("dojo/dom-class");
13 13 import { AnimationAttrs, WatchForRendition } from "./tsx/WatchForRendition";
@@ -45,17 +45,6 export interface EventSelector {
45 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 48 export type DojoMouseEvent<T = unknown> = MouseEvent & EventSelector & EventDetails<T>;
60 49
61 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 105 return new WatchForRendition({
117 106 ...opts,
118 107 subject: source,
@@ -9,8 +9,7 import { collectNodes, destroy as safeDe
9 9 import { IDestroyable } from "@implab/core-amd/interfaces";
10 10 import { play } from "../play";
11 11 import * as fx from "dojo/fx";
12 import { isSubsribable, Subscribable } from "../observable";
13 import { QueryResultUpdate } from "../tsx";
12 import { isObservableResults, isSubsribable, OrderUpdate, Subscribable } from "../observable";
14 13
15 14 const trace = TraceSource.get(mid);
16 15
@@ -22,16 +21,7 interface ItemRendition {
22 21 destroy(): void;
23 22 }
24 23
25 interface ObservableResults<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> {
24 interface RenderTask<T> extends OrderUpdate<T> {
35 25 animate: boolean;
36 26 }
37 27
@@ -44,13 +34,11 export interface AnimationAttrs {
44 34 }
45 35
46 36 export interface WatchForRenditionAttrs<T> extends AnimationAttrs {
47 subject: T[] | Subscribable<QueryResultUpdate<T>>;
37 subject: T[] | Subscribable<OrderUpdate<T>>;
48 38
49 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 43 const noop = () => { };
56 44
@@ -72,7 +60,7 export class WatchForRendition<T> extend
72 60
73 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 65 private readonly _renderTasks: RenderTask<T>[] = [];
78 66
@@ -109,7 +97,7 export class WatchForRendition<T> extend
109 97 const result = this._subject;
110 98
111 99 if (result) {
112 if (isSubsribable<QueryResultUpdate<T>>(result)) {
100 if (isSubsribable<OrderUpdate<T>>(result)) {
113 101 let animate = false;
114 102 const subscription = result.subscribe({
115 103 next: ({ item, prevIndex, newIndex }) => this._onItemUpdated({ item, prevIndex, newIndex, animate })
@@ -117,7 +105,7 export class WatchForRendition<T> extend
117 105 scope.own(subscription);
118 106 animate = this._animate;
119 107 } else {
120 if (isObservable(result))
108 if (isObservableResults<T>(result))
121 109 scope.own(result.observe((item, prevIndex, newIndex) => this._onItemUpdated({ item, prevIndex, newIndex, animate: false }), true));
122 110
123 111 for (let i = 0, n = result.length; i < n; i++)
@@ -49,4 +49,4 subj1
49 49
50 50 t.equal(consumer2.value, 8, "Should map");
51 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 95 pack("@implab/djx")
96 96 pack("@implab/core-amd")
97 into("@js-joda/core") {
98 from(npm.module("@js-joda/core/dist"))
99 }
97 100 pack("dojo")
98 101 pack("dijit")
99 102 into("rxjs") {
@@ -6,6 +6,7
6 6 "": {
7 7 "name": "@implab/djx-playground",
8 8 "dependencies": {
9 "@js-joda/core": "5.3.1",
9 10 "dijit": "1.17.3",
10 11 "dojo": "1.17.3",
11 12 "requirejs": "2.3.6",
@@ -121,6 +122,11
121 122 "integrity": "sha512-/lbcMCHdRoHJLKFcT8xdk1KbGazSlb1pGSDJ406io7iMenPm/XbJYcUti+VzXnn71zOJ8aYpGT12T5L0rfOZNA==",
122 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 130 "node_modules/@nodelib/fs.scandir": {
125 131 "version": "2.1.5",
126 132 "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -2786,6 +2792,11
2786 2792 "integrity": "sha512-/lbcMCHdRoHJLKFcT8xdk1KbGazSlb1pGSDJ406io7iMenPm/XbJYcUti+VzXnn71zOJ8aYpGT12T5L0rfOZNA==",
2787 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 2800 "@nodelib/fs.scandir": {
2790 2801 "version": "2.1.5",
2791 2802 "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -2,6 +2,7
2 2 "name": "@implab/djx-playground",
3 3 "private": true,
4 4 "dependencies": {
5 "@js-joda/core": "5.3.1",
5 6 "dijit": "1.17.3",
6 7 "dojo": "1.17.3",
7 8 "requirejs": "2.3.6",
@@ -10,6 +10,11 requirejs.config({
10 10 name: "rxjs",
11 11 location: "rxjs",
12 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 20 deps: ["app"]
@@ -4,4 +4,5 import "@implab/djx/css!dijit/themes/dij
4 4 import "@implab/djx/css!dijit/themes/tundra/tundra.css";
5 5
6 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 1 import { Contact } from "./Contact";
2 2
3 type AppointmentRole = "organizer" | "speaker" | "participant";
3 export type AppointmentRole = "organizer" | "speaker" | "participant";
4 4
5 5 export interface Member extends Contact {
6 6 role: AppointmentRole;
7 7 }
8 8
9 9 export interface Appointment {
10 id: string;
11
10 12 title: string;
11 13
12 14 startAt: Date;
@@ -1,88 +1,33
1 1 import Memory = require("dojo/store/Memory");
2 2 import Observable = require("dojo/store/Observable");
3 import { Appointment, Member } from "./Appointment";
3 import { Appointment, AppointmentRole, Member } from "./Appointment";
4 4 import { Contact } from "./Contact";
5 5 import { Uuid } from "@implab/core-amd/Uuid";
6 import { Observable as RxjsObservable } from "rxjs";
7 import { QueryResultUpdate } from "@implab/djx/tsx";
8 import {isPromise} from "@implab/core-amd/safe";
6 import { query } from "@implab/djx/observable";
7 import { IDestroyable } from "@implab/core-amd/interfaces";
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
12 12 type ContactRecord = Contact;
13 13
14 14 type MemberRecord = Member & { appointmentId: string; };
15 15
16 export interface ObservableResults<T> {
17 /**
18 * Allows observation of results
19 */
20 observe(listener: (object: T, previousIndex: number, newIndex: number) => void, includeUpdates?: boolean): {
21 remove(): void;
22 };
23 }
16 const item = <T, T2>(map: (x: T) => T2) => <U extends { item: T }>({ item, ...props }: U) => ({ item: map(item), ...props });
24 17
25 18
26 export function isObservable<T>(v: unknown): v is ObservableResults<T> {
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 {
19 export class MainContext implements IDestroyable {
76 20 private readonly _appointments = new Observable(new Memory<AppointmentRecord>());
77 21
78 22 private readonly _contacts = new Observable(new Memory<ContactRecord>());
79 23
80 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 28 const id = Uuid();
84 29 this._appointments.add({
85 id: Uuid(),
30 id,
86 31 startAt,
87 32 duration,
88 33 title
@@ -92,16 +37,35 export class MainContext {
92 37 this._members.add({
93 38 appointmentId: id,
94 39 ...member
95 }, {id: Uuid()}) as void
40 }, { id: Uuid() }) as void
96 41 );
97 42 }
98 43
99 queryAppointments(dateFrom: Date, dateTo: Date) {
100 //this._appointments.query().map()
44 queryAppointments({ dateFrom, dateTo }: { dateFrom?: Date; dateTo?: Date; } = {}) {
45 return query(this._appointments)(({ startAt }) =>
46 (!dateFrom || dateFrom <= startAt) &&
47 (!dateTo || startAt <= dateTo)
48 ).map(item(this._mapAppointment));
101 49 }
102 50
103 private readonly _mapAppointment = ({startAt, title, duration, id}: AppointmentRecord) => ({
51 async addMember(appointmentId: string, member: Member) {
52 await delay(1000);
53 this._members.add({
54 appointmentId,
55 ...member
56 });
57 }
104 58
59 private readonly _mapAppointment = ({ startAt, title, duration, id }: AppointmentRecord) => ({
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
69 }
70
107 71 }
@@ -1,20 +1,37
1 import { BehaviorSubject, Observer, Unsubscribable, Subscribable } from "rxjs";
2 import { IDestroyable} from "@implab/core-amd/interfaces"
3
4 interface State {
5 color: string;
1 import { id as mid } from "module";
2 import { BehaviorSubject, Observer, Unsubscribable } from "rxjs";
3 import { IDestroyable } from "@implab/core-amd/interfaces";
4 import { OrderUpdate, Observable } from "@implab/djx/observable";
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 23 export default class MainModel implements IDestroyable {
15 24 private readonly _state: BehaviorSubject<State>;
16 constructor(initialState: State) {
17 this._state = new BehaviorSubject(initialState);
25
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 36 getState() {
20 37 return this._state.getValue();
@@ -22,13 +39,25 export default class MainModel implement
22 39
23 40 subscribe(observer: Partial<Observer<State>>): Unsubscribable {
24 41 return this._state.subscribe(observer);
25 }
26
42 }
43
27 44 protected dispatch(command: Partial<State>) {
28 45 const state = this.getState();
29 this._state.next({...state, ... command});
46 this._state.next({ ...state, ...command });
47 }
48
49 addMember(appointmentId: string, member: Member) {
50 this._context.addMember(appointmentId, member).catch(error(trace));
30 51 }
31 52
32 load() { }
33 destroy() { }
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 63 } No newline at end of file
@@ -1,81 +1,72
1 1 import { djbase, djclass } from "@implab/djx/declare";
2 2 import { DjxWidgetBase } from "@implab/djx/tsx/DjxWidgetBase";
3 import { createElement, watch, prop, attach, all, bind, toggleClass } from "@implab/djx/tsx";
4 import ProgressBar from "./ProgressBar";
3 import { bind, createElement, prop, watch, watchFor } from "@implab/djx/tsx";
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 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 10 @djclass
11 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 30 render() {
26 31
27 32 return <div className="tundra">
28 <h2 ref={attach(this, "titleNode")}>Hi!</h2>
29 <section style={{ padding: "10px" }}>
30 {watch(prop(this, "showCounter"), flag => flag &&
31 [
32 <Counter><input ref={all(
33 bind("value", prop(this, "count")
34 .map(x => x/10)
35 ),
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 )}
48 onClick={this._onPauseClick}
49 />
50 ]
51
52 )}
53 </section>
54 <Button onClick={this._onToggleCounterClick}>Toggle counter</Button>
33 <h2 ref={bind("innerHTML", prop(this, "title"))} />
34 {watch(prop(this, "appointments"), items => items &&
35 <ul>
36 {watchFor(items, ({ id, title, getMembers }) =>
37 <li>{title}
38 <ul>
39 {watchFor(getMembers(), ({ role, name, position }) =>
40 <li className={role}>{name}({position})</li>
41 )}
42 </ul>
43 <div>
44 <Button onClick={() => this._onAddMemberClick(id)}>Add member</Button>
45 </div>
46 </li>
47 )}
48 </ul>
49 )}
50 <div>
51 <Button onClick={this._onAddAppointmentClick}>Add new appointment</Button>
52 </div>
55 53 </div>;
56 54 }
57 55
58 postCreate(): void {
59 super.postCreate();
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 });
56 load() {
57 this.model.load();
72 58 }
73 59
74 private readonly _onPauseClick = () => {
75 this.set("paused", !this.paused);
60 private readonly _onAddMemberClick = (appointmentId: string) => {
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 = () => {
79 this.set("showCounter", !this.showCounter);
69 private readonly _onAddAppointmentClick = () => {
70 this.model.addAppointment("Appointment", new Date, 30);
80 71 };
81 72 }
@@ -11,6 +11,8
11 11 "@implab/djx",
12 12 "@implab/dojo-typings"
13 13 ],
14 "skipLibCheck": true
14 "skipLibCheck": true,
15 "target": "ES5",
16 "lib": ["ES2015"]
15 17 }
16 18 } No newline at end of file
@@ -1,3 +0,0
1 export default class Observable<S extends dojo.store.api.Store<object>> {
2
3 } No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now