@livequery/client 1.0.93 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/Collection.d.ts +20 -19
- package/build/Collection.js +166 -103
- package/package.json +4 -4
package/build/Collection.d.ts
CHANGED
|
@@ -1,46 +1,47 @@
|
|
|
1
|
-
import { Subject, Observable,
|
|
2
|
-
import {
|
|
1
|
+
import { Subject, Observable, BehaviorSubject } from 'rxjs';
|
|
2
|
+
import { LivequeryBaseEntity, QueryOption, Transporter, UpdatedData, Paging, Response } from '@livequery/types';
|
|
3
|
+
export type LoadingIndicator = false | 'backward' | 'forward' | 'both';
|
|
3
4
|
export type CollectionOption<T extends LivequeryBaseEntity = LivequeryBaseEntity> = {
|
|
4
5
|
transporter: Transporter;
|
|
5
6
|
sync_delay?: number;
|
|
6
|
-
|
|
7
|
-
reload_interval?: number;
|
|
7
|
+
options?: Partial<QueryOption<T>>;
|
|
8
8
|
realtime?: boolean;
|
|
9
9
|
};
|
|
10
10
|
export type SmartQueryItem<T> = T & {
|
|
11
|
+
__remove: Function;
|
|
11
12
|
__removing: boolean;
|
|
13
|
+
__update: (data: Partial<T>) => any;
|
|
12
14
|
__updating: boolean;
|
|
13
15
|
__adding: boolean;
|
|
14
|
-
|
|
15
|
-
__update: (data: Partial<T>) => any;
|
|
16
|
-
__trigger: (name: string, payload?: any) => any;
|
|
16
|
+
__trigger: <R extends {}>(name: string, payload?: any) => Promise<Response<R>>;
|
|
17
17
|
__ref: string;
|
|
18
18
|
};
|
|
19
19
|
export type CollectionStream<T extends LivequeryBaseEntity = LivequeryBaseEntity> = {
|
|
20
20
|
items: SmartQueryItem<T>[];
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
paging: Partial<Paging>;
|
|
22
|
+
loading?: LoadingIndicator;
|
|
23
|
+
options: Partial<QueryOption<T>>;
|
|
24
|
+
error?: boolean;
|
|
25
|
+
code?: string;
|
|
26
|
+
message?: string;
|
|
25
27
|
};
|
|
26
28
|
export declare class CollectionObservable<T extends LivequeryBaseEntity = LivequeryBaseEntity> extends Observable<CollectionStream<T>> {
|
|
27
29
|
#private;
|
|
28
30
|
private ref;
|
|
29
31
|
private collection_options;
|
|
30
32
|
readonly $changes: Subject<UpdatedData<T>>;
|
|
31
|
-
|
|
32
|
-
$: ReplaySubject<CollectionStream<T>>;
|
|
33
|
+
$: BehaviorSubject<CollectionStream<T>>;
|
|
33
34
|
constructor(ref: string | false | null | '' | undefined, collection_options: CollectionOption<T>);
|
|
34
35
|
set_realtime(realtime: boolean): void;
|
|
35
36
|
private fetch_data;
|
|
36
|
-
reload(): void;
|
|
37
37
|
reset(): void;
|
|
38
38
|
fetch_more(): void;
|
|
39
|
+
fetch_prev(): void;
|
|
39
40
|
filter(filters: Partial<QueryOption<T>>): void;
|
|
40
|
-
add(payload: Partial<T>): Promise<DocumentResponse<T>>;
|
|
41
|
-
update({ id: update_payload_id, ...payload }: Partial<T>): Promise<DocumentResponse<T> | undefined>;
|
|
42
|
-
remove(remove_document_id?: string): Promise<
|
|
43
|
-
trigger<R>(name: string, payload?: object, trigger_document_id?: string, query?: {
|
|
41
|
+
add(payload: Partial<T>): Promise<import("@livequery/types").DocumentResponse<T & LivequeryBaseEntity>>;
|
|
42
|
+
update({ id: update_payload_id, ...payload }: Partial<T>): Promise<import("@livequery/types").DocumentResponse<T & LivequeryBaseEntity> | undefined>;
|
|
43
|
+
remove(remove_document_id?: string): Promise<import("@livequery/types").DocumentResponse<LivequeryBaseEntity> | undefined>;
|
|
44
|
+
trigger<R extends {}>(name: string, payload?: object, trigger_document_id?: string, query?: {
|
|
44
45
|
[key: string]: string | number | boolean;
|
|
45
|
-
}): Promise<R
|
|
46
|
+
}): Promise<Response<R>>;
|
|
46
47
|
}
|
package/build/Collection.js
CHANGED
|
@@ -1,35 +1,30 @@
|
|
|
1
|
-
import { Subject, Observable, merge,
|
|
2
|
-
import { bufferTime, filter, map } from 'rxjs/operators';
|
|
1
|
+
import { Subject, Observable, merge, BehaviorSubject } from 'rxjs';
|
|
2
|
+
import { bufferTime, filter, finalize, first, map, share, skip, tap, toArray } from 'rxjs/operators';
|
|
3
3
|
export class CollectionObservable extends Observable {
|
|
4
4
|
ref;
|
|
5
5
|
collection_options;
|
|
6
6
|
$changes = new Subject();
|
|
7
|
+
#pages = new Map();
|
|
7
8
|
#queries = new Set();
|
|
8
|
-
#
|
|
9
|
+
#sorters = new Array;
|
|
9
10
|
#IdMap = new Map();
|
|
10
11
|
#refs = [];
|
|
11
|
-
|
|
12
|
-
has_more: false,
|
|
12
|
+
$ = new BehaviorSubject({
|
|
13
13
|
items: [],
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
loading: false,
|
|
15
|
+
options: {},
|
|
16
|
+
paging: {}
|
|
17
|
+
});
|
|
18
18
|
constructor(ref, collection_options) {
|
|
19
19
|
super(o => {
|
|
20
|
-
const
|
|
21
|
-
const auto_reload_interval = collection_options.reload_interval && setInterval(() => this.reload(), collection_options.reload_interval);
|
|
20
|
+
const linker = this.$.subscribe(o);
|
|
22
21
|
return () => {
|
|
23
|
-
|
|
22
|
+
linker.unsubscribe();
|
|
24
23
|
this.#queries.forEach(s => s.unsubscribe());
|
|
25
|
-
clearInterval(auto_reload_interval);
|
|
26
24
|
};
|
|
27
25
|
});
|
|
28
26
|
this.ref = ref;
|
|
29
27
|
this.collection_options = collection_options;
|
|
30
|
-
this.collection_options.filters = this.collection_options.filters || {};
|
|
31
|
-
if (collection_options.filters)
|
|
32
|
-
this.value.filters = collection_options.filters;
|
|
33
28
|
if (ref && (ref.startsWith('/') || ref.endsWith('/')))
|
|
34
29
|
throw 'INVAILD_REF_FORMAT';
|
|
35
30
|
this.#refs = this.#ref_parser(ref);
|
|
@@ -50,19 +45,20 @@ export class CollectionObservable extends Observable {
|
|
|
50
45
|
set_realtime(realtime) {
|
|
51
46
|
this.collection_options.realtime = realtime;
|
|
52
47
|
}
|
|
53
|
-
#sync(stream, from_local = false) {
|
|
48
|
+
#sync(stream, from_local = false, direction) {
|
|
49
|
+
const state = this.$.getValue();
|
|
54
50
|
const realtime = this.collection_options.realtime ?? true;
|
|
55
51
|
const actions = { update: false, reindex: false };
|
|
56
|
-
for (const { data, error,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
this.value.error = error;
|
|
52
|
+
for (const { data, error, code, message } of stream) {
|
|
53
|
+
if (!from_local) {
|
|
54
|
+
state.loading = false;
|
|
60
55
|
actions.update = true;
|
|
61
56
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
57
|
+
// Error & paging
|
|
58
|
+
if (error) {
|
|
59
|
+
state.error = true;
|
|
60
|
+
state.code = code;
|
|
61
|
+
state.message = message;
|
|
66
62
|
actions.update = true;
|
|
67
63
|
}
|
|
68
64
|
// Sync
|
|
@@ -75,38 +71,34 @@ export class CollectionObservable extends Observable {
|
|
|
75
71
|
if (index == -1 && type == 'added') {
|
|
76
72
|
if (
|
|
77
73
|
// Is first value from HTTP query
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
catch (e) { }
|
|
107
|
-
return false;
|
|
108
|
-
}))) {
|
|
109
|
-
this.value.items.push({
|
|
74
|
+
true
|
|
75
|
+
// || (
|
|
76
|
+
// // Is realtime update that match filters
|
|
77
|
+
// (realtime || from_local) && Object
|
|
78
|
+
// .keys(state.options || {})
|
|
79
|
+
// .filter(key => !key.includes('_'))
|
|
80
|
+
// .every(key => {
|
|
81
|
+
// try {
|
|
82
|
+
// const [field, expression] = key.split(':')
|
|
83
|
+
// const a = payload[field as keyof typeof payload] as number
|
|
84
|
+
// const b = state.options?.[field as keyof QueryOption<T>] as any as number
|
|
85
|
+
// if (!expression) return a == b
|
|
86
|
+
// if (expression == 'ne') return a != b
|
|
87
|
+
// if (expression == 'lt') return typeof a == 'number' && typeof b == 'number' && a < b
|
|
88
|
+
// if (expression == 'lte') return typeof a == 'number' && typeof b == 'number' && a <= b
|
|
89
|
+
// if (expression == 'gt') return typeof a == 'number' && typeof b == 'number' && a > b
|
|
90
|
+
// if (expression == 'gte') return typeof a == 'number' && typeof b == 'number' && a >= b
|
|
91
|
+
// if (expression == 'in' || expression == 'like') return Array.isArray(a) && a?.includes(b)
|
|
92
|
+
// if (expression == 'between') {
|
|
93
|
+
// const [x, y] = b as any as number[]
|
|
94
|
+
// return x <= a && a <= y
|
|
95
|
+
// }
|
|
96
|
+
// } catch (e) { }
|
|
97
|
+
// return false
|
|
98
|
+
// })
|
|
99
|
+
// )
|
|
100
|
+
) {
|
|
101
|
+
const item = {
|
|
110
102
|
...payload,
|
|
111
103
|
__adding: false,
|
|
112
104
|
__updating: false,
|
|
@@ -115,7 +107,8 @@ export class CollectionObservable extends Observable {
|
|
|
115
107
|
__trigger: (name, input) => this.trigger(name, input, payload?.id),
|
|
116
108
|
__update: (input) => this.update({ ...input, id: payload?.id }),
|
|
117
109
|
__ref: change.ref
|
|
118
|
-
}
|
|
110
|
+
};
|
|
111
|
+
direction == 'forward' ? state.items.push(item) : state.items.unshift(item);
|
|
119
112
|
actions.reindex = true;
|
|
120
113
|
actions.update = true;
|
|
121
114
|
}
|
|
@@ -123,11 +116,16 @@ export class CollectionObservable extends Observable {
|
|
|
123
116
|
if (index >= 0 && (realtime || from_local)) {
|
|
124
117
|
if (type == 'added' || type == 'modified') {
|
|
125
118
|
actions.update = true;
|
|
126
|
-
|
|
119
|
+
const sort_key_value_updated = this.#sorters.some(({ key }) => {
|
|
120
|
+
const value = payload[key];
|
|
121
|
+
if (typeof value == 'string' || typeof value == 'number')
|
|
122
|
+
return true;
|
|
123
|
+
});
|
|
124
|
+
if (sort_key_value_updated) {
|
|
127
125
|
actions.reindex = true;
|
|
128
126
|
}
|
|
129
|
-
|
|
130
|
-
...
|
|
127
|
+
state.items[index] = {
|
|
128
|
+
...state.items[index],
|
|
131
129
|
__adding: false,
|
|
132
130
|
__updating: false,
|
|
133
131
|
__removing: false,
|
|
@@ -135,9 +133,8 @@ export class CollectionObservable extends Observable {
|
|
|
135
133
|
};
|
|
136
134
|
}
|
|
137
135
|
if (type == 'removed') {
|
|
138
|
-
actions.reindex = true;
|
|
139
136
|
actions.update = true;
|
|
140
|
-
|
|
137
|
+
state.items.splice(index, 1);
|
|
141
138
|
for (const [document_id, i] of this.#IdMap) {
|
|
142
139
|
i == index && this.#IdMap.delete(document_id);
|
|
143
140
|
i > index && this.#IdMap.set(document_id, i - 1);
|
|
@@ -147,65 +144,131 @@ export class CollectionObservable extends Observable {
|
|
|
147
144
|
}
|
|
148
145
|
}
|
|
149
146
|
if (actions.reindex) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
147
|
+
state.items = state.items.sort((a, b) => {
|
|
148
|
+
for (const { key, order } of this.#sorters) {
|
|
149
|
+
const aa = a[key];
|
|
150
|
+
const bb = b[key];
|
|
151
|
+
if (typeof aa == 'number' && typeof bb == 'number') {
|
|
152
|
+
const rs = aa - bb;
|
|
153
|
+
if (rs == 0)
|
|
154
|
+
continue;
|
|
155
|
+
return rs * order;
|
|
156
|
+
}
|
|
157
|
+
if (typeof aa == 'string' && typeof bb == 'string') {
|
|
158
|
+
const rs = aa.localeCompare(bb);
|
|
159
|
+
if (rs == 0)
|
|
160
|
+
continue;
|
|
161
|
+
return rs * order;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return -1;
|
|
161
165
|
});
|
|
162
166
|
this.#IdMap.clear();
|
|
163
|
-
|
|
167
|
+
state.items.map((item, index) => this.#IdMap.set(item.id, index));
|
|
164
168
|
}
|
|
165
|
-
|
|
169
|
+
if (direction) {
|
|
170
|
+
// Cache paging
|
|
171
|
+
this.#pages.clear();
|
|
172
|
+
stream.forEach(s => s.data?.paging && this.#pages.set(s.ref, s.data?.paging));
|
|
173
|
+
// Caculate paging here
|
|
174
|
+
const last_page_total = state.items.length;
|
|
175
|
+
const total = stream.reduce((p, c) => p + (c.data?.paging?.count?.total || 0), 0);
|
|
176
|
+
const prev = stream.reduce((p, c) => p + (c.data?.paging?.count?.prev || 0), 0);
|
|
177
|
+
const next = stream.reduce((p, c) => p + (c.data?.paging?.count?.next || 0), 0);
|
|
178
|
+
state.paging = {
|
|
179
|
+
count: {
|
|
180
|
+
current: state.items.length,
|
|
181
|
+
next: next - (direction == 'backward' ? last_page_total : 0),
|
|
182
|
+
prev: prev - (direction == 'forward' ? last_page_total : 0),
|
|
183
|
+
total
|
|
184
|
+
},
|
|
185
|
+
has: {
|
|
186
|
+
next: stream.some(s => s.data?.paging?.has?.next),
|
|
187
|
+
prev: stream.some(s => s.data?.paging?.has?.prev)
|
|
188
|
+
},
|
|
189
|
+
page: {
|
|
190
|
+
current: Math.min(...stream.map(s => s.data?.paging?.page.current || 0)),
|
|
191
|
+
total: Math.max(...stream.map(s => s.data?.paging?.page.total || 0))
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
actions.update && this.$.next(state);
|
|
166
196
|
}
|
|
167
|
-
fetch_data(
|
|
197
|
+
fetch_data(options = {}, loading, flush = false) {
|
|
168
198
|
if (!this.ref)
|
|
169
199
|
return;
|
|
170
200
|
if (this.#refs.length == 0)
|
|
171
201
|
return;
|
|
202
|
+
if (this.$.getValue().loading)
|
|
203
|
+
return;
|
|
204
|
+
this.collection_options.options = options;
|
|
205
|
+
this.#sorters = Object.keys(options).filter(k => k.endsWith(':sort')).map(k => {
|
|
206
|
+
const key = k.split(':sort')[0];
|
|
207
|
+
const order = options[k] == 1 ? 1 : -1;
|
|
208
|
+
return { key, order };
|
|
209
|
+
});
|
|
210
|
+
this.#sorters.every(a => a.key != 'id') && this.#sorters.push({ key: 'id', order: -1 });
|
|
211
|
+
const state = {
|
|
212
|
+
...this.$.getValue(),
|
|
213
|
+
items: flush ? [] : this.$.getValue().items,
|
|
214
|
+
loading,
|
|
215
|
+
options: {
|
|
216
|
+
...this.$.getValue().options || {},
|
|
217
|
+
...options
|
|
218
|
+
}
|
|
219
|
+
};
|
|
172
220
|
if (flush) {
|
|
173
|
-
this.#
|
|
221
|
+
this.#pages.clear();
|
|
174
222
|
this.#queries.forEach(s => s.unsubscribe());
|
|
175
223
|
this.#queries.clear();
|
|
176
224
|
this.#IdMap.clear();
|
|
177
225
|
}
|
|
178
|
-
const
|
|
179
|
-
|
|
226
|
+
const remain_data_refs = this.#refs.filter(ref => {
|
|
227
|
+
const paging = this.#pages.get(ref);
|
|
228
|
+
if (!paging)
|
|
229
|
+
return true;
|
|
230
|
+
return loading == 'forward' ? paging.has.next : paging.has.prev;
|
|
231
|
+
});
|
|
232
|
+
const no_more_data = !flush && (remain_data_refs.length == 0 || this.$.getValue().loading);
|
|
233
|
+
if (no_more_data)
|
|
180
234
|
return;
|
|
181
|
-
this
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
235
|
+
this.$.next(state);
|
|
236
|
+
const queries = remain_data_refs.map((ref, index) => {
|
|
237
|
+
const cursor = this.#pages.get(ref)?.cursor;
|
|
238
|
+
const opts = {
|
|
239
|
+
...options,
|
|
240
|
+
...loading == 'backward' && cursor?.first ? { ':before': cursor?.first } : {},
|
|
241
|
+
...loading == 'forward' && cursor?.last ? { ':after': cursor?.last } : {},
|
|
242
|
+
};
|
|
243
|
+
return this
|
|
244
|
+
.collection_options
|
|
245
|
+
.transporter
|
|
246
|
+
.query(ref, opts).pipe(map(data => ({
|
|
247
|
+
...data,
|
|
248
|
+
ref
|
|
249
|
+
})), share());
|
|
250
|
+
});
|
|
251
|
+
const first_values = merge(...queries.map(q => q.pipe(filter(r => !!r.data?.paging || !!r.error), first()))).pipe(toArray(), tap(list => this.#sync(list, false, loading))).subscribe();
|
|
252
|
+
const subscription = merge(...queries.map(q => q.pipe(skip(1)))).pipe(bufferTime(500), filter(list => list.length > 0), map(data => this.#sync(data, false, loading)), finalize(() => first_values.unsubscribe())).subscribe();
|
|
196
253
|
this.#queries.add(subscription);
|
|
197
254
|
}
|
|
198
|
-
reload() {
|
|
199
|
-
this.#queries.forEach(s => s.reload());
|
|
200
|
-
}
|
|
201
255
|
reset() {
|
|
202
|
-
this.fetch_data({}, true);
|
|
256
|
+
this.fetch_data({}, 'both', true);
|
|
203
257
|
}
|
|
204
258
|
fetch_more() {
|
|
205
|
-
this
|
|
259
|
+
const { options } = this.$.getValue();
|
|
260
|
+
this.fetch_data(options, 'forward');
|
|
261
|
+
}
|
|
262
|
+
fetch_prev() {
|
|
263
|
+
const { options } = this.$.getValue();
|
|
264
|
+
this.fetch_data(options, 'backward');
|
|
206
265
|
}
|
|
266
|
+
// public fetch_around_cursor(cursor: string) {
|
|
267
|
+
// const state = this.$.getValue()
|
|
268
|
+
// this.fetch_data(state.options, 'both')
|
|
269
|
+
// }
|
|
207
270
|
filter(filters) {
|
|
208
|
-
this.fetch_data(filters, true);
|
|
271
|
+
this.fetch_data(filters, 'forward', true);
|
|
209
272
|
}
|
|
210
273
|
#find_ref_by_id(id) {
|
|
211
274
|
if (!id || !this.ref)
|
|
@@ -213,7 +276,7 @@ export class CollectionObservable extends Observable {
|
|
|
213
276
|
const index = this.#IdMap.get(id);
|
|
214
277
|
if (index == undefined)
|
|
215
278
|
return {};
|
|
216
|
-
const origin_ref = this.
|
|
279
|
+
const origin_ref = this.$.getValue().items[index].__ref;
|
|
217
280
|
if (!origin_ref)
|
|
218
281
|
throw 'COLLECTION_REF_NOT_FOUND';
|
|
219
282
|
const refs = origin_ref.split('/');
|
|
@@ -291,7 +354,7 @@ export class CollectionObservable extends Observable {
|
|
|
291
354
|
async trigger(name, payload, trigger_document_id, query = {}) {
|
|
292
355
|
const { ref } = this.#find_ref_by_id(trigger_document_id);
|
|
293
356
|
if (!ref)
|
|
294
|
-
|
|
357
|
+
throw new Error('INVAILD_REF');
|
|
295
358
|
return await this.collection_options.transporter.trigger(ref, name, query, payload);
|
|
296
359
|
}
|
|
297
360
|
}
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"repository": {
|
|
5
5
|
"url": "https://github.com/livequery/client"
|
|
6
6
|
},
|
|
7
|
-
"version": "
|
|
7
|
+
"version": "2.0.1",
|
|
8
8
|
"description": "",
|
|
9
9
|
"main": "build/index.js",
|
|
10
10
|
"types": "build/index.d.ts",
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
"build/**/*"
|
|
13
13
|
],
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"rxjs": "^7.1.0",
|
|
16
|
-
"typescript": "^4.9.5",
|
|
17
15
|
"uuid": "^8.3.2"
|
|
18
16
|
},
|
|
19
17
|
"devDependencies": {
|
|
20
|
-
"
|
|
18
|
+
"typescript": "^4.9.5",
|
|
19
|
+
"rxjs": "^7.1.0",
|
|
20
|
+
"@livequery/types": "^2.0.0"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
23
|
"test": "echo \"Error: no test specified\" && exit 1",
|