@livequery/client 1.0.93 → 2.0.0
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 +162 -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,127 @@ 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));
|
|
168
|
+
}
|
|
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
|
+
};
|
|
164
194
|
}
|
|
165
|
-
actions.update && this.$.next(
|
|
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
|
+
};
|
|
172
216
|
if (flush) {
|
|
173
|
-
this.#
|
|
217
|
+
this.#pages.clear();
|
|
174
218
|
this.#queries.forEach(s => s.unsubscribe());
|
|
175
219
|
this.#queries.clear();
|
|
176
220
|
this.#IdMap.clear();
|
|
177
221
|
}
|
|
178
|
-
const
|
|
179
|
-
|
|
222
|
+
const remain_data_refs = this.#refs.filter(ref => {
|
|
223
|
+
const paging = this.#pages.get(ref);
|
|
224
|
+
if (!paging)
|
|
225
|
+
return true;
|
|
226
|
+
return loading == 'forward' ? paging.has.next : paging.has.prev;
|
|
227
|
+
});
|
|
228
|
+
const no_more_data = !flush && (remain_data_refs.length == 0 || this.$.getValue().loading);
|
|
229
|
+
if (no_more_data)
|
|
180
230
|
return;
|
|
181
|
-
this
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
231
|
+
this.$.next(state);
|
|
232
|
+
const queries = remain_data_refs.map((ref, index) => {
|
|
233
|
+
const cursor = this.#pages.get(ref)?.cursor;
|
|
234
|
+
const opts = {
|
|
235
|
+
...options,
|
|
236
|
+
...loading == 'backward' && cursor?.first ? { ':before': cursor?.first } : {},
|
|
237
|
+
...loading == 'forward' && cursor?.last ? { ':after': cursor?.last } : {},
|
|
238
|
+
};
|
|
239
|
+
return this
|
|
240
|
+
.collection_options
|
|
241
|
+
.transporter
|
|
242
|
+
.query(ref, opts).pipe(map(data => ({
|
|
243
|
+
...data,
|
|
244
|
+
ref
|
|
245
|
+
})), share());
|
|
246
|
+
});
|
|
247
|
+
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();
|
|
248
|
+
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
249
|
this.#queries.add(subscription);
|
|
197
250
|
}
|
|
198
|
-
reload() {
|
|
199
|
-
this.#queries.forEach(s => s.reload());
|
|
200
|
-
}
|
|
201
251
|
reset() {
|
|
202
|
-
this.fetch_data({}, true);
|
|
252
|
+
this.fetch_data({}, 'both', true);
|
|
203
253
|
}
|
|
204
254
|
fetch_more() {
|
|
205
|
-
this
|
|
255
|
+
const { options } = this.$.getValue();
|
|
256
|
+
this.fetch_data(options, 'forward');
|
|
257
|
+
}
|
|
258
|
+
fetch_prev() {
|
|
259
|
+
const { options } = this.$.getValue();
|
|
260
|
+
this.fetch_data(options, 'backward');
|
|
206
261
|
}
|
|
262
|
+
// public fetch_around_cursor(cursor: string) {
|
|
263
|
+
// const state = this.$.getValue()
|
|
264
|
+
// this.fetch_data(state.options, 'both')
|
|
265
|
+
// }
|
|
207
266
|
filter(filters) {
|
|
208
|
-
this.fetch_data(filters, true);
|
|
267
|
+
this.fetch_data(filters, 'forward', true);
|
|
209
268
|
}
|
|
210
269
|
#find_ref_by_id(id) {
|
|
211
270
|
if (!id || !this.ref)
|
|
@@ -213,7 +272,7 @@ export class CollectionObservable extends Observable {
|
|
|
213
272
|
const index = this.#IdMap.get(id);
|
|
214
273
|
if (index == undefined)
|
|
215
274
|
return {};
|
|
216
|
-
const origin_ref = this.
|
|
275
|
+
const origin_ref = this.$.getValue().items[index].__ref;
|
|
217
276
|
if (!origin_ref)
|
|
218
277
|
throw 'COLLECTION_REF_NOT_FOUND';
|
|
219
278
|
const refs = origin_ref.split('/');
|
|
@@ -291,7 +350,7 @@ export class CollectionObservable extends Observable {
|
|
|
291
350
|
async trigger(name, payload, trigger_document_id, query = {}) {
|
|
292
351
|
const { ref } = this.#find_ref_by_id(trigger_document_id);
|
|
293
352
|
if (!ref)
|
|
294
|
-
|
|
353
|
+
throw new Error('INVAILD_REF');
|
|
295
354
|
return await this.collection_options.transporter.trigger(ref, name, query, payload);
|
|
296
355
|
}
|
|
297
356
|
}
|
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.0",
|
|
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",
|