@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.
@@ -1,46 +1,47 @@
1
- import { Subject, Observable, ReplaySubject } from 'rxjs';
2
- import { ErrorInfo, LivequeryBaseEntity, QueryOption, Transporter, UpdatedData, DocumentResponse } from '@livequery/types';
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
- filters?: Partial<QueryOption<T>>;
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
- __remove: Function;
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
- error?: ErrorInfo;
22
- has_more: boolean;
23
- loading?: boolean;
24
- filters: Partial<QueryOption<T>>;
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
- value: CollectionStream<T>;
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<void>;
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 | undefined>;
46
+ }): Promise<Response<R>>;
46
47
  }
@@ -1,35 +1,30 @@
1
- import { Subject, Observable, merge, ReplaySubject } from 'rxjs';
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
- #next_cursor = {};
9
+ #sorters = new Array;
9
10
  #IdMap = new Map();
10
11
  #refs = [];
11
- value = {
12
- has_more: false,
12
+ $ = new BehaviorSubject({
13
13
  items: [],
14
- filters: {},
15
- loading: false
16
- };
17
- $ = new ReplaySubject(1);
14
+ loading: false,
15
+ options: {},
16
+ paging: {}
17
+ });
18
18
  constructor(ref, collection_options) {
19
19
  super(o => {
20
- const subscription = this.$.subscribe(o);
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
- subscription.unsubscribe();
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, ref } of stream) {
57
- // Error & paging
58
- if (error) {
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
- if (data?.paging?.n == 0) {
63
- this.#next_cursor[ref] = data?.paging?.next_cursor;
64
- this.value.has_more = Object.values(this.#next_cursor).some(v => v && v != '#');
65
- this.value.loading = false;
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
- data?.paging?.n == 0
79
- || (
80
- // Is realtime update that match filters
81
- (realtime || from_local) && Object
82
- .keys(this.value.filters || {})
83
- .filter(key => !key.includes('_'))
84
- .every(key => {
85
- try {
86
- const [field, expression] = key.split(':');
87
- const a = payload[field];
88
- const b = this.value.filters?.[field];
89
- if (!expression)
90
- return a == b;
91
- if (expression == 'ne')
92
- return a != b;
93
- if (expression == 'lt')
94
- return typeof a == 'number' && typeof b == 'number' && a < b;
95
- if (expression == 'lte')
96
- return typeof a == 'number' && typeof b == 'number' && a <= b;
97
- if (expression == 'gt')
98
- return typeof a == 'number' && typeof b == 'number' && a > b;
99
- if (expression == 'gte')
100
- return typeof a == 'number' && typeof b == 'number' && a >= b;
101
- if (expression == 'in' || expression == 'like')
102
- return Array.isArray(a) && a?.includes(b);
103
- if (expression == 'between')
104
- return (b[0] <= a && a <= b[1]);
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
- if (Object.keys(payload).some(key => ['created_at', this.collection_options?.filters?._order_by].includes(key))) {
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
- this.value.items[index] = {
130
- ...this.value.items[index],
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
- this.value.items.splice(index, 1);
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
- const _sort = this.collection_options?.filters?._sort || 'desc';
151
- const _order_by = this.collection_options?.filters?._order_by || 'created_at';
152
- this.value.items = this.value.items.sort((a, b) => {
153
- const ka = a[_order_by];
154
- const kb = b[_order_by];
155
- if (typeof ka == 'string' && typeof kb == 'string')
156
- return ka.localeCompare(ka) * (_sort == 'desc' ? -1 : 1);
157
- if ((typeof ka == 'number' || typeof ka == 'number')
158
- && (typeof kb == 'number' || typeof kb == 'number'))
159
- return (ka - kb) * (_sort == 'desc' ? -1 : 1);
160
- return 1;
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
- this.value.items.map((item, index) => this.#IdMap.set(item.id, index));
167
+ state.items.map((item, index) => this.#IdMap.set(item.id, index));
164
168
  }
165
- actions.update && this.$.next(this.value);
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(filters = {}, flush = false) {
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.#next_cursor = {};
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 has_more_data_refs = this.#refs.filter(ref => this.#next_cursor[ref] === undefined || (this.#next_cursor[ref] && this.#next_cursor[ref] != '#'));
179
- if (!flush && (has_more_data_refs.length == 0 || this.value.loading))
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.value = {
182
- ...this.value,
183
- items: flush ? [] : this.value.items,
184
- error: undefined,
185
- loading: true,
186
- filters
187
- };
188
- this.$.next(this.value);
189
- const queries = has_more_data_refs.map(ref => (this
190
- .collection_options
191
- .transporter
192
- .query(ref, { ...filters, _cursor: this.#next_cursor[ref] })));
193
- const reload = () => queries.map(q => q.reload());
194
- const $ = merge(...queries.map((q, index) => q.pipe(map(data => ({ ...data, ref: has_more_data_refs[index] }))))).pipe(bufferTime(500), filter(list => list.length > 0), map(data => this.#sync(data)));
195
- const subscription = Object.assign($.subscribe(), { reload });
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.fetch_data(this.value?.filters);
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.value.items[index].__ref;
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
- return;
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": "1.0.93",
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
- "@livequery/types": "^1.0.79"
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",