##// END OF EJS Templates
Fixed queryEx: query fresh results every time when no active observers are exist
cin -
r150:d9c99ae7dec8 v1.10.1 default
parent child
Show More
@@ -1,152 +1,171
1 1 import { PromiseOrValue } from "@implab/core-amd/interfaces";
2 2 import { isCancellable, isPromise } from "@implab/core-amd/safe";
3 3 import { observe, Observable, empty } from "./observable";
4 4 import { after } from "dojo/aspect";
5 5
6 6 export interface OrderedUpdate<T> {
7 7 /** The item is being updated */
8 8 readonly item: T;
9 9
10 10 /** The previous index of the item, -1 in case it is inserted */
11 11 readonly prevIndex: number;
12 12
13 13 /** The new index of the item, -1 in case it is deleted */
14 14 readonly newIndex: number;
15 15
16 16 }
17 17
18 18 export type QueryResults<T> = Observable<OrderedUpdate<T>>;
19 19
20 20 interface DjObservableResults<T> {
21 21 /**
22 22 * Allows observation of results
23 23 */
24 24 observe(listener: (object: T, previousIndex: number, newIndex: number) => void, includeUpdates?: boolean): {
25 25 remove(): void;
26 26 };
27 27 }
28 28
29 29 interface Queryable<T, Q, O> {
30 30 query(query?: Q, options?: O): PromiseOrValue<T[]>;
31 31 }
32 32
33 33 export const isDjObservableResults = <T>(v: object): v is DjObservableResults<T> =>
34 34 v && (typeof (v as { observe?: unknown; }).observe === "function");
35 35
36 36 export const query = <T, Q, O>(store: Queryable<T, Q, O>, includeUpdates = true) => {
37 37 const q = queryEx(store, includeUpdates);
38 38 return (query?: Q, options?: O & { observe?: boolean }) => {
39 39 const [data, updates] = q(query, options);
40 40
41 41 return options?.observe === false ? data : data.cat(updates);
42 42 };
43 43 };
44 44
45 /**
46 * Wraps the query method of the store, the resulting method takes a query
47 * expression and returns two observable sequences. The first sequence represents
48 * the results of the query, the second sequence provides the updates to the
49 * query results.
50 *
51 * @param store The store used to query data
52 * @param includeUpdates The flag to include item updates not only additions and
53 * deletions. By default this flag is set to true.
54 * @returns Two observable sequences
55 */
45 56 export const queryEx = <T, Q, O>(store: Queryable<T, Q, O>, includeUpdates = true) =>
46 57 (query?: Q, options?: O): [data: QueryResults<T>, updates: QueryResults<T>] => {
47 58
48 const pending: T[] = [];
49
50 let results: PromiseOrValue<T[]> = pending;
59 /** count active observers */
60 let listeners = 0;
61 let results: PromiseOrValue<T[]> = [];
51 62
52 63 const data = observe<OrderedUpdate<T>>(({ next, complete, error }) => {
53 64 const processResults = (items: T[]) =>
54 65 items.forEach((item, newIndex) => next({ item, newIndex, prevIndex: -1 }));
55 66
56 67 try {
57 if (results === pending)
68 // is there are no active observers here, we need to query actual
69 // data from the store.
70 if (listeners === 0)
58 71 results = store.query(query, options);
59 72
60 73 if (isPromise(results)) {
61 74 results.then(processResults).then(complete, error);
62 75
63 76 if (isCancellable(results))
64 77 return results.cancel.bind(results);
65 78 } else {
66 79 processResults(results);
67 80 complete();
68 81 }
69 82 } catch (e) {
70 83 error(e);
71 84 }
72 85 });
73 86
74 87 const updates = observe<OrderedUpdate<T>>(({ next, complete, error, isClosed }) => {
75 88 try {
76 89 if (!isClosed() && isDjObservableResults<T>(results)) {
90 // subscribe fot the changes
91 listeners++;
77 92 const h = results.observe((item, prevIndex, newIndex) => next({ item, prevIndex, newIndex }), includeUpdates);
78 return () => h.remove();
93 return () => {
94 // unsubscribe from changes
95 listeners--;
96 h.remove();
97 };
79 98 } else {
80 99 complete();
81 100 }
82 101 } catch (e) {
83 102 error(e);
84 103 }
85 104 });
86 105
87 106 return [data, updates];
88 107 };
89 108
90 109
91 110 interface IndexedStore<T> {
92 111 get(id: string | number): PromiseLike<T> | T | null | undefined;
93 112 }
94 113
95 114 interface Notifications<T> {
96 115 notify(item: T | undefined, id: string | number | undefined): void;
97 116 }
98 117
99 118 const hasNotifications = <T>(x: unknown): x is Notifications<T> =>
100 119 typeof x === "object" && x !== null && (typeof (x as Notifications<T>).notify === "function");
101 120
102 121 interface GetOpts {
103 122 observe?: boolean;
104 123 }
105 124
106 125 export type ItemUpdate<T> = [item: NonNullable<T>, id: string | number | undefined] |
107 126 [item: undefined | null, id: string | number];
108 127
109 128 const filterItem = (itemId: string | number) =>
110 129 <T>(source: Observable<ItemUpdate<T>>) =>
111 130 observe<T>(({ next, complete, error }) => {
112 131 const subscription = source
113 132 .filter(([, id]) => id === itemId)
114 133 .subscribe({
115 134 next: ([item]) => item !== null && item !== undefined ? next(item) : complete(),
116 135 complete,
117 136 error
118 137 });
119 138 return () => subscription.unsubscribe();
120 139 });
121 140
122 141 export const get = <T>(store: IndexedStore<T>) => {
123 142 const changes = hasNotifications<T>(store) ?
124 143 observe<ItemUpdate<T>>(({ next }) => {
125 144 const handle = after(store, "notify", (...args: ItemUpdate<T>) => next(args), true);
126 145 return () => handle.remove();
127 146 }) : empty;
128 147
129 148 return (id: string | number, opts: GetOpts = {}) =>
130 149 observe<T>(({ next, complete, error }) => {
131 150 try {
132 151 const result = store.get(id);
133 152
134 153 const handle = (x: T | null | undefined) => {
135 154 if (x !== null && x !== undefined)
136 155 next(x);
137 156 complete();
138 157 };
139 158
140 159 if (isPromise(result)) {
141 160 result.then(handle).then(undefined, error);
142 161
143 162 if (isCancellable(result))
144 163 return () => result.cancel();
145 164 } else {
146 165 handle(result);
147 166 }
148 167 } catch (e) {
149 168 error(e);
150 169 }
151 170 }).cat(opts.observe !== false ? changes.pipe(filterItem(id)) : empty);
152 171 }; No newline at end of file
@@ -1,133 +1,133
1 1 plugins {
2 2 id "org.implab.gradle-typescript" version "1.3.4"
3 3 id "ivy-publish"
4 4 }
5 5
6 6 def container = "djx-playground"
7 7
8 8 configurations {
9 9 npmLocal
10 10 }
11 11
12 12 dependencies {
13 13 npmLocal project(":djx")
14 14 }
15 15
16 16 def bundleDir = fileTree(layout.buildDirectory.dir("bundle")) {
17 17 builtBy "bundle"
18 18 }
19 19
20 20 typescript {
21 21 compilerOptions {
22 22 lib = ["es5", "dom", "scripthost", "es2015.promise", "es2015.symbol", "es2015.iterable"]
23 23 // listFiles = true
24 24 strict = true
25 25 types = ["requirejs", "@implab/dojo-typings", "@implab/djx"]
26 26 module = "amd"
27 27 it.target = "es5"
28 28 experimentalDecorators = true
29 29 noUnusedLocals = false
30 30 jsx = "react"
31 31 jsxFactory = "createElement"
32 32 moduleResolution = "node"
33 33 // dojo-typings are sick
34 34 skipLibCheck = true
35 35 // traceResolution = true
36 36 // baseUrl = "./"
37 37 // paths = [ "*": [ "$projectDir/src/typings/*" ] ]
38 38 // baseUrl = "$projectDir/src/typings"
39 39 // typeRoots = ["$projectDir/src/typings"]
40 40 }
41 41 tscCmd = "$projectDir/node_modules/.bin/tsc"
42 42 tsLintCmd = "$projectDir/node_modules/.bin/tslint"
43 43 esLintCmd = "$projectDir/node_modules/.bin/eslint"
44 44 }
45 45
46 46 tasks.matching{ it.name =~ /^configureTs/ }.configureEach {
47 47 compilerOptions {
48 48 if (symbols != 'none') {
49 49 sourceMap = true
50 50 switch(symbols) {
51 51 case "local":
52 52 sourceRoot = ( isWindows ? "file:///" : "file://" ) + it.rootDir
53 53 break;
54 54 }
55 55 }
56 56 }
57 57 }
58 58
59 npmInstall {
60 //npmInstall.dependsOn it
59 task npmInstallLocalDeps {
60 npmInstall.dependsOn it
61 61 dependsOn configurations.npmLocal
62 62
63 63 doFirst {
64 64 configurations.npmLocal.each { f ->
65 65 exec {
66 66 commandLine "npm", "install", f, "--save-dev"
67 67 }
68 68 }
69 69 }
70 70 }
71 71
72 72 clean {
73 73 doFirst {
74 74 delete "$buildDir/bundle"
75 75 }
76 76 }
77 77
78 78
79 79 task processResourcesBundle(type: Copy) {
80 80 from "src/bundle"
81 81 into layout.buildDirectory.dir("bundle")
82 82 }
83 83
84 84 task copyModules(type: Copy) {
85 85 dependsOn npmInstall
86 86 into layout.buildDirectory.dir("bundle/js");
87 87
88 88 def pack = { String jsmod ->
89 89 into(jsmod) {
90 90 from npm.module(jsmod)
91 91 }
92 92 }
93 93
94 94
95 95 pack("@implab/djx")
96 96 pack("@implab/core-amd")
97 97 into("@js-joda/core") {
98 98 from(npm.module("@js-joda/core/dist"))
99 99 }
100 100 pack("dojo")
101 101 pack("dijit")
102 102 into("rxjs") {
103 103 from(npm.module("rxjs/dist/bundles"))
104 104 }
105 105 from npm.module("requirejs/require.js")
106 106 }
107 107
108 108 npmPublish {
109 109 enabled = false
110 110 }
111 111
112 112 task copyApp(type: Copy) {
113 113 dependsOn assemble
114 114 from typescript.assemblyDir
115 115 into layout.buildDirectory.dir("bundle/js/app")
116 116 }
117 117
118 118 task bundle {
119 119 dependsOn copyModules, processResourcesBundle, copyApp
120 120 }
121 121
122 122 task up(type: Exec) {
123 123 dependsOn bundle
124 124 commandLine "podman", "run", "--rm", "-d",
125 125 "--name", container,
126 126 "-p", "2078:80",
127 127 "-v", "$buildDir/bundle:/srv/www/htdocs",
128 128 "registry.implab.org/implab/apache2:latest"
129 129 }
130 130
131 131 task stop(type: Exec) {
132 132 commandLine "podman", "stop", container
133 133 } No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now