prostgles-client 4.0.173 → 4.0.176

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.
Files changed (55) hide show
  1. package/.eslintignore +1 -0
  2. package/.prettierignore +2 -0
  3. package/.prettierrc +4 -0
  4. package/.vscode/settings.json +10 -1
  5. package/dist/Auth.d.ts +2 -2
  6. package/dist/Auth.d.ts.map +1 -1
  7. package/dist/Auth.js +28 -18
  8. package/dist/SyncedTable/SyncedTable.d.ts +12 -12
  9. package/dist/SyncedTable/SyncedTable.d.ts.map +1 -1
  10. package/dist/SyncedTable/SyncedTable.js +92 -78
  11. package/dist/SyncedTable/getMultiSyncSubscription.d.ts.map +1 -1
  12. package/dist/SyncedTable/getMultiSyncSubscription.js +11 -9
  13. package/dist/{getDBO.d.ts → getDbHandler.d.ts} +3 -3
  14. package/dist/getDbHandler.d.ts.map +1 -0
  15. package/dist/{subscriptionHandler.d.ts → getSubscriptionHandler.d.ts} +1 -1
  16. package/dist/getSubscriptionHandler.d.ts.map +1 -0
  17. package/dist/{syncHandler.d.ts → getSyncHandler.d.ts} +1 -1
  18. package/dist/getSyncHandler.d.ts.map +1 -0
  19. package/dist/index.js +1 -1
  20. package/dist/index.no-sync.js +1 -1
  21. package/dist/prostgles.d.ts +24 -0
  22. package/dist/prostgles.d.ts.map +1 -1
  23. package/dist/prostgles.js +11 -10
  24. package/package.json +3 -2
  25. package/tsconfig.json +1 -3
  26. package/dist/getDBO.d.ts.map +0 -1
  27. package/dist/subscriptionHandler.d.ts.map +0 -1
  28. package/dist/syncHandler.d.ts.map +0 -1
  29. package/dist/typeTests.d.ts +0 -2
  30. package/dist/typeTests.d.ts.map +0 -1
  31. package/dist/typeTests.js +0 -37
  32. package/lib/Auth.ts +0 -155
  33. package/lib/FunctionQueuer.ts +0 -71
  34. package/lib/SyncedTable/SyncedTable.ts +0 -1078
  35. package/lib/SyncedTable/getMultiSyncSubscription.ts +0 -67
  36. package/lib/SyncedTable/getSingleSyncSubscription.ts +0 -0
  37. package/lib/getDBO.ts +0 -152
  38. package/lib/getMethods.ts +0 -30
  39. package/lib/getSqlHandler.ts +0 -174
  40. package/lib/md5.ts +0 -183
  41. package/lib/prostgles-full-cdn.ts +0 -5
  42. package/lib/prostgles-full.ts +0 -8
  43. package/lib/prostgles.ts +0 -350
  44. package/lib/react-hooks.ts +0 -356
  45. package/lib/subscriptionHandler.ts +0 -211
  46. package/lib/syncHandler.ts +0 -201
  47. package/lib/typeTests.ts +0 -64
  48. package/lib/useProstglesClient.ts +0 -92
  49. package/tests/package-lock.json +0 -71
  50. package/tests/package.json +0 -17
  51. package/tests/test.ts +0 -10
  52. package/tests/tsconfig.json +0 -27
  53. /package/dist/{getDBO.js → getDbHandler.js} +0 -0
  54. /package/dist/{subscriptionHandler.js → getSubscriptionHandler.js} +0 -0
  55. /package/dist/{syncHandler.js → getSyncHandler.js} +0 -0
@@ -1,1078 +0,0 @@
1
- import type { FieldFilter, WALItem, AnyObject, ClientSyncHandles, SyncBatchParams, ClientSyncInfo, TableHandler, EqualityFilter } from "prostgles-types";
2
- import { getTextPatch, isEmpty, WAL, getKeys, isObject } from "prostgles-types";
3
- import type { DBHandlerClient } from "../prostgles";
4
- import { getMultiSyncSubscription } from "./getMultiSyncSubscription";
5
-
6
- const DEBUG_KEY = "DEBUG_SYNCEDTABLE";
7
- const hasWnd = typeof window !== "undefined";
8
- export const debug: any = function (...args: any[]) {
9
- if (hasWnd && (window as any)[DEBUG_KEY]) {
10
- (window as any)[DEBUG_KEY](...args);
11
- }
12
- };
13
-
14
- export type SyncOptions = Partial<SyncedTableOptions> & {
15
- select?: FieldFilter;
16
- handlesOnData?: boolean;
17
- }
18
- export type SyncOneOptions = Partial<SyncedTableOptions> & {
19
- handlesOnData?: boolean;
20
- }
21
-
22
- type SyncDebugEvent = {
23
- type: "sync";
24
- tableName: string;
25
- command: keyof ClientSyncHandles;
26
- data: AnyObject;
27
- };
28
-
29
- /**
30
- * Creates a local synchronized table
31
- */
32
- type OnChange<T> = (data: (SyncDataItem<Required<T>>)[], delta?: Partial<T>[]) => any
33
- export type Sync<
34
- T extends AnyObject,
35
- OnChangeFunc extends OnChange<T> = (data: (SyncDataItem<Required<T>>)[], delta?: Partial<T>[]) => any,
36
- Upsert extends ((newData: T[]) => any) = ((newData: T[]) => any)
37
- > = (
38
- basicFilter: EqualityFilter<T>,
39
- options: SyncOptions,
40
- onChange: OnChangeFunc,
41
- onError?: (error: any) => void
42
- ) => Promise<{
43
- $unsync: () => void;
44
- $upsert: Upsert;
45
- getItems: () => T[];
46
- }>;
47
-
48
- /**
49
- * Creates a local synchronized record
50
- */
51
- export type SyncOne<T extends AnyObject = AnyObject> = (basicFilter: Partial<T>, options: SyncOneOptions, onChange: (data: (SyncDataItem<Required<T>>), delta?: Partial<T>) => any, onError?: (error: any) => void) => Promise<SingleSyncHandles<T>>;
52
-
53
- export type SyncBatchRequest = {
54
- from_synced?: string | number;
55
- to_synced?: string | number;
56
- offset: number;
57
- limit: number;
58
- }
59
-
60
- export type ItemUpdate = {
61
- idObj: AnyObject;
62
- delta: AnyObject;
63
- opts?: $UpdateOpts;
64
- }
65
- export type ItemUpdated = ItemUpdate & {
66
- oldItem: any;
67
- newItem: any;
68
- status: "inserted" | "updated" | "deleted" | "unchanged";
69
- from_server: boolean;
70
- }
71
-
72
- export type CloneSync<T extends AnyObject, Full extends boolean> = (
73
- onChange: SingleChangeListener<T, Full>,
74
- onError?: (error: any) => void
75
- ) => SingleSyncHandles<T, Full>;
76
-
77
- export type CloneMultiSync<T extends AnyObject> = (
78
- onChange: MultiChangeListener<T>,
79
- onError?: (error: any) => void
80
- ) => MultiSyncHandles<T>;
81
-
82
- export type $UpdateOpts = {
83
- deepMerge: boolean
84
- }
85
- type DeepPartial<T> = T extends Array<any> ? T : T extends object ? {
86
- [P in keyof T]?: DeepPartial<T[P]>;
87
- } : T;
88
-
89
- /**
90
- * CRUD handles added if initialised with handlesOnData = true
91
- */
92
- export type SingleSyncHandles<T extends AnyObject = AnyObject, Full extends boolean = false> = {
93
- $get: () => T;
94
- $find: (idObj: Partial<T>) => (T | undefined);
95
- $unsync: () => any;
96
- $delete: () => void;
97
- $update: <OPTS extends $UpdateOpts>(newData: OPTS extends { deepMerge: true } ? DeepPartial<T> : Partial<T>, opts?: OPTS) => any;
98
- $cloneSync: CloneSync<T, Full>;
99
- $cloneMultiSync: CloneMultiSync<T>;
100
- }
101
-
102
- export type SyncDataItem<T extends AnyObject = AnyObject, Full extends boolean = false> = T & (Full extends true ? SingleSyncHandles<T, Full> : Partial<SingleSyncHandles<T, Full>>);
103
-
104
- export type MultiSyncHandles<T extends AnyObject> = {
105
- $unsync: () => void;
106
- $upsert: (newData: T[]) => any;
107
- getItems: () => AnyObject[];
108
- }
109
-
110
- export type SubscriptionSingle<T extends AnyObject = AnyObject, Full extends boolean = false> = {
111
- _onChange: SingleChangeListener<T, Full>
112
- notify: (data: T, delta?: DeepPartial<T>) => T;
113
- idObj: Partial<T>;
114
- handlesOnData?: boolean;
115
- handles?: SingleSyncHandles<T, Full>;
116
- }
117
- export type SubscriptionMulti<T extends AnyObject = AnyObject> = {
118
- _onChange: MultiChangeListener<T>;
119
- notify: (data: T[], delta: DeepPartial<T>[]) => T[];
120
- idObj?: Partial<T>;
121
- handlesOnData?: boolean;
122
- handles?: MultiSyncHandles<T>;
123
- }
124
-
125
- const STORAGE_TYPES = {
126
- array: "array",
127
- localStorage: "localStorage",
128
- object: "object"
129
- } as const;
130
-
131
- export type MultiChangeListener<T extends AnyObject = AnyObject> = (items: SyncDataItem<T>[], delta: DeepPartial<T>[]) => any;
132
- export type SingleChangeListener<T extends AnyObject = AnyObject, Full extends boolean = false> = (item: SyncDataItem<T, Full>, delta?: DeepPartial<T>) => any;
133
- type StorageType = keyof typeof STORAGE_TYPES;
134
- export type SyncedTableOptions = {
135
- name: string;
136
- filter?: AnyObject;
137
- onChange?: MultiChangeListener;
138
- onError?: (error: any) => void;
139
- db: any;
140
- pushDebounce?: number;
141
- skipFirstTrigger?: boolean;
142
- select?: "*" | {};
143
- storageType?: StorageType;
144
-
145
- /* If true then only the delta of text field is sent to server */
146
- patchText?: boolean;
147
- patchJSON?: boolean;
148
- onReady: () => any;
149
- skipIncomingDeltaCheck?: boolean;
150
- onDebug?: (event: SyncDebugEvent, tbl: SyncedTable) => Promise<void>;
151
- };
152
-
153
- export type DbTableSync = {
154
- unsync: () => void;
155
- syncData: (data?: AnyObject[], deleted?: AnyObject[], cb?: (err?: any) => void) => void;
156
- };
157
-
158
- export class SyncedTable {
159
-
160
- db: DBHandlerClient;
161
- name: string;
162
- select?: "*" | {};
163
- filter?: AnyObject;
164
- onChange?: MultiChangeListener;
165
- id_fields: string[];
166
- synced_field: string;
167
- throttle = 100;
168
- batch_size = 50;
169
- skipFirstTrigger = false;
170
-
171
- columns: { name: string, data_type: string }[] = [];
172
-
173
- wal?: WAL;
174
-
175
- notifyWal?: WAL;
176
-
177
- _multiSubscriptions: SubscriptionMulti[] = [];
178
- _singleSubscriptions: SubscriptionSingle[] = [];
179
-
180
- /**
181
- * add debug mode to fix sudden no data and sync listeners bug
182
- */
183
- set multiSubscriptions(mSubs: SubscriptionMulti[]) {
184
- debug(mSubs, this._multiSubscriptions);
185
- this._multiSubscriptions = mSubs.slice(0);
186
- }
187
- get multiSubscriptions(): SubscriptionMulti[] {
188
- return this._multiSubscriptions;
189
- }
190
-
191
- set singleSubscriptions(sSubs: SubscriptionSingle[]) {
192
- debug(sSubs, this._singleSubscriptions);
193
- this._singleSubscriptions = sSubs.slice(0);
194
- }
195
- get singleSubscriptions(): SubscriptionSingle[] {
196
- return this._singleSubscriptions
197
- }
198
-
199
- dbSync?: DbTableSync;
200
- items: AnyObject[] = [];
201
- storageType?: StorageType;
202
- itemsObj: AnyObject = {};
203
- patchText: boolean;
204
- patchJSON: boolean;
205
- isSynced = false;
206
- onError: SyncedTableOptions["onError"];
207
- onDebug?: (evt: Omit<SyncDebugEvent, "type" | "tableName" | "channelName">) => Promise<void>;
208
-
209
- constructor({ name, filter, onChange, onReady, onDebug, db, skipFirstTrigger = false, select = "*", storageType = "object", patchText = false, patchJSON = false, onError }: SyncedTableOptions) {
210
- this.name = name;
211
- this.filter = filter;
212
- this.select = select;
213
- this.onChange = onChange;
214
-
215
- if(onDebug){
216
- this.onDebug = evt => onDebug({ ...evt, type: "sync", tableName: name }, this)
217
- }
218
-
219
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
220
- if (!STORAGE_TYPES[storageType]) throw "Invalid storage type. Expecting one of: " + Object.keys(STORAGE_TYPES).join(", ");
221
- if (!hasWnd && storageType === STORAGE_TYPES.localStorage) {
222
- console.warn("Could not set storageType to localStorage: window object missing\nStorage changed to object");
223
- storageType = "object";
224
- }
225
- this.storageType = storageType;
226
- this.patchText = patchText
227
- this.patchJSON = patchJSON;
228
-
229
- if (!db) throw "db missing";
230
- this.db = db;
231
-
232
- const { id_fields, synced_field, throttle = 100, batch_size = 50 } = db[this.name]._syncInfo;
233
- if (!id_fields || !synced_field) throw "id_fields/synced_field missing";
234
- this.id_fields = id_fields;
235
- this.synced_field = synced_field;
236
- this.batch_size = batch_size;
237
- this.throttle = throttle;
238
-
239
- this.skipFirstTrigger = skipFirstTrigger;
240
-
241
- this.multiSubscriptions = [];
242
- this.singleSubscriptions = [];
243
-
244
- this.onError = onError || function (err) { console.error("Sync internal error: ", err) }
245
-
246
- const onSyncRequest: ClientSyncHandles["onSyncRequest"] = (syncBatchParams) => {
247
-
248
- let clientSyncInfo: ClientSyncInfo = { c_lr: undefined, c_fr: undefined, c_count: 0 };
249
-
250
- const batch = this.getBatch(syncBatchParams);
251
- if (batch.length) {
252
-
253
- clientSyncInfo = {
254
- c_fr: this.getRowSyncObj(batch[0]!),
255
- c_lr: this.getRowSyncObj(batch[batch.length - 1]!),
256
- c_count: batch.length
257
- };
258
- }
259
-
260
- this.onDebug?.({ command: "onUpdates", data: { syncBatchParams, batch, clientSyncInfo } });
261
- return clientSyncInfo;
262
- },
263
- onPullRequest = async (syncBatchParams: SyncBatchParams) => {
264
-
265
- // if(this.getDeleted().length){
266
- // await this.syncDeleted();
267
- // }
268
- const data = this.getBatch(syncBatchParams);
269
- await this.onDebug?.({ command: "onPullRequest", data: { syncBatchParams, data } });
270
- return data;
271
- },
272
- onUpdates: ClientSyncHandles["onUpdates"] = async (onUpdatesParams) => {
273
- await this.onDebug?.({ command: "onUpdates", data: { onUpdatesParams } });
274
- if ("err" in onUpdatesParams && onUpdatesParams.err) {
275
- this.onError?.(onUpdatesParams.err);
276
- } else if ("isSynced" in onUpdatesParams && onUpdatesParams.isSynced && !this.isSynced) {
277
- this.isSynced = onUpdatesParams.isSynced;
278
- const items = this.getItems().map(d => ({ ...d }));
279
- this.setItems([]);
280
- const updateItems = items.map(d => ({
281
- idObj: this.getIdObj(d),
282
- delta: { ...d }
283
- }))
284
- await this.upsert(updateItems, true)
285
- } else if ("data" in onUpdatesParams) {
286
- /* Delta left empty so we can prepare it here */
287
- const updateItems = onUpdatesParams.data.map(d => {
288
- return {
289
- idObj: this.getIdObj(d),
290
- delta: d
291
- }
292
- });
293
- await this.upsert(updateItems, true);
294
- } else {
295
- console.error("Unexpected onUpdates");
296
- }
297
-
298
- return true;
299
- };
300
-
301
- const opts = {
302
- id_fields,
303
- synced_field,
304
- throttle,
305
- }
306
-
307
- db[this.name]._sync(filter, { select }, { onSyncRequest, onPullRequest, onUpdates }).then((s: DbTableSync) => {
308
- this.dbSync = s;
309
-
310
- function confirmExit() { return "Data may be lost. Are you sure?"; }
311
-
312
- /**
313
- * Some syncs can be read only. Any changes are local
314
- */
315
- this.wal = new WAL({
316
- ...opts,
317
- batch_size,
318
- onSendStart: () => {
319
- if (hasWnd) window.onbeforeunload = confirmExit;
320
- },
321
- onSend: async (data, walData) => {
322
- // if(this.patchText){
323
- // const textCols = this.columns.filter(c => c.data_type.toLowerCase().startsWith("text"));
324
-
325
- // data = await Promise.all(data.map(d => {
326
- // const dataTextCols = Object.keys(d).filter(k => textCols.find(tc => tc.name === k));
327
- // if(dataTextCols.length){
328
- // /* Create text patches and update separately */
329
- // dada
330
- // }
331
- // return d;
332
- // }))
333
- // }
334
-
335
- const _data = await this.updatePatches(walData);
336
- if (!_data.length) return [];
337
- return this.dbSync!.syncData(data);
338
- },//, deletedData);,
339
- onSendEnd: () => {
340
- if (hasWnd) window.onbeforeunload = null;
341
- }
342
- });
343
-
344
- this.notifyWal = new WAL({
345
- ...opts,
346
- batch_size: Infinity,
347
- throttle: 5,
348
- onSend: async (items, fullItems) => {
349
- this._notifySubscribers(fullItems.map(d => ({
350
- delta: this.getDelta(d.initial ?? {}, d.current),
351
- idObj: this.getIdObj(d.current),
352
- newItem: d.current,
353
- })))
354
- }
355
- });
356
-
357
- onReady();
358
- });
359
-
360
- if (db[this.name].getColumns) {
361
- db[this.name].getColumns().then((cols: any) => {
362
- this.columns = cols;
363
- });
364
- }
365
-
366
- if (this.onChange && !this.skipFirstTrigger) {
367
- setTimeout(this.onChange, 0);
368
- }
369
- debug(this);
370
- }
371
-
372
- /**
373
- * Will update text/json fields through patching method
374
- * This will send less data to server
375
- * @param walData
376
- */
377
- updatePatches = async (walData: WALItem[]) => {
378
- let remaining: any[] = walData.map(d => d.current);
379
- const patched: [any, any][] = [],
380
- patchedItems: any[] = [];
381
- if (this.columns.length && this.tableHandler?.updateBatch && (this.patchText || this.patchJSON)) {
382
-
383
- // const jCols = this.columns.filter(c => c.data_type === "json")
384
- const txtCols = this.columns.filter(c => c.data_type === "text");
385
- if (this.patchText && txtCols.length) {
386
-
387
- remaining = [];
388
- const id_keys = [this.synced_field, ...this.id_fields];
389
- await Promise.all(walData.slice(0).map(async (d, i) => {
390
-
391
- const { current, initial } = { ...d };
392
- let patchedDelta: AnyObject | undefined;
393
- if (initial) {
394
- txtCols.map(c => {
395
- if (!id_keys.includes(c.name) && c.name in current) {
396
- patchedDelta ??= { ...current }
397
-
398
- patchedDelta![c.name] = getTextPatch(initial[c.name], current[c.name]);
399
- }
400
- });
401
-
402
- if (patchedDelta && this.wal) {
403
- patchedItems.push(patchedDelta)
404
- patched.push([
405
- this.wal.getIdObj(patchedDelta),
406
- this.wal.getDeltaObj(patchedDelta)
407
- ]);
408
- }
409
- }
410
- // console.log("json-stable-stringify ???")
411
-
412
- if (!patchedDelta) {
413
- remaining.push(current);
414
- }
415
- }))
416
- }
417
- }
418
-
419
- /**
420
- * There is a decent chance the patch update will fail.
421
- * As such, to prevent sync batch update failures, the patched updates are updated separately.
422
- * If patch update fails then sync batch normally without patch.
423
- */
424
- if (patched.length) {
425
- try {
426
- await this.tableHandler?.updateBatch!(patched);
427
- } catch (e) {
428
- console.log("failed to patch update", e);
429
- remaining = remaining.concat(patchedItems);
430
- }
431
- }
432
-
433
- return remaining.filter(d => d);
434
- }
435
-
436
- static create(opts: Omit<SyncedTableOptions, "onReady">): Promise<SyncedTable> {
437
- return new Promise((resolve, reject) => {
438
- try {
439
- const res = new SyncedTable({
440
- ...opts,
441
- onReady: () => {
442
- setTimeout(() => {
443
- resolve(res);
444
- }, 0)
445
- }
446
- })
447
- } catch (err) {
448
- reject(err);
449
- }
450
- })
451
- }
452
-
453
- /**
454
- * Returns a sync handler to all records within the SyncedTable instance
455
- * @param onChange change listener <(items: object[], delta: object[]) => any >
456
- * @param handlesOnData If true then $upsert and $unsync handles will be added on each data item. True by default;
457
- */
458
- sync<T extends AnyObject = AnyObject>(onChange: MultiChangeListener<T>, handlesOnData = true): MultiSyncHandles<T> {
459
- const { sub, handles } = getMultiSyncSubscription.bind(this)({
460
- onChange: onChange as MultiChangeListener<AnyObject>,
461
- handlesOnData
462
- });
463
-
464
- this.multiSubscriptions.push(sub as any);
465
- if (!this.skipFirstTrigger) {
466
- setTimeout(() => {
467
- const items = this.getItems<T>();
468
- sub.notify(items, items as any);
469
- }, 0);
470
- }
471
- return Object.freeze({ ...handles });
472
- }
473
-
474
- makeSingleSyncHandles<T extends AnyObject = AnyObject, Full extends boolean = false>(idObj: Partial<T>, onChange: SingleChangeListener<T, Full> | MultiChangeListener<T>): SingleSyncHandles<T, Full> {
475
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
476
- if (!idObj || !onChange) throw `syncOne(idObj, onChange) -> MISSING idObj or onChange`;
477
-
478
- const handles: SingleSyncHandles<T, Full> = {
479
- $get: () => this.getItem<T>(idObj).data!,
480
- $find: (idObject) => this.getItem<T>(idObject).data,
481
- $unsync: () => {
482
- return this.unsubscribe(onChange)
483
- },
484
- $delete: () => {
485
- return this.delete(idObj);
486
- },
487
- $update: (newData, opts) => {
488
- /* DROPPED SYNC BUG */
489
- if (!this.singleSubscriptions.length && !this.multiSubscriptions.length) {
490
- console.warn("No sync listeners");
491
- debug("nosync", this._singleSubscriptions, this._multiSubscriptions);
492
- }
493
- this.upsert([{ idObj, delta: newData, opts }]);
494
- },
495
- $cloneSync: (onChange) => this.syncOne<T, Full>(idObj, onChange),
496
- // TODO: add clone sync hook
497
- // $useCloneSync: () => {
498
- // const handles = this.syncOne<T, Full>(idObj, item => {
499
- // setItem()
500
- // });
501
- // return handles.$unsync;
502
- // },
503
- $cloneMultiSync: (onChange) => this.sync(onChange, true),
504
- };
505
-
506
- return handles;
507
- }
508
-
509
- /**
510
- * Returns a sync handler to a specific record within the SyncedTable instance
511
- * @param idObj object containing the target id_fields properties
512
- * @param onChange change listener <(item: object, delta: object) => any >
513
- * @param handlesOnData If true then $update, $delete and $unsync handles will be added on the data item. True by default;
514
- */
515
- syncOne<T extends AnyObject = AnyObject, Full extends boolean = false>(idObj: Partial<T>, onChange: SingleChangeListener<T, Full>, handlesOnData = true): SingleSyncHandles<T, Full> {
516
-
517
- const handles = this.makeSingleSyncHandles<T, Full>(idObj, onChange);
518
- const sub: SubscriptionSingle<T, Full> = {
519
- _onChange: onChange,
520
- idObj,
521
- handlesOnData,
522
- handles,
523
- notify: (data, delta) => {
524
- const newData: SyncDataItem<T, Full> = { ...data } as any;
525
-
526
- if (handlesOnData) {
527
- newData.$get = handles.$get;
528
- newData.$find = handles.$find;
529
- newData.$update = handles.$update;
530
- newData.$delete = handles.$delete;
531
- newData.$unsync = handles.$unsync;
532
- newData.$cloneSync = handles.$cloneSync as any;
533
- }
534
- return onChange(newData, delta)
535
- }
536
- };
537
-
538
-
539
- this.singleSubscriptions.push(sub as any);
540
-
541
- setTimeout(() => {
542
- const existingData = handles.$get();
543
- if (existingData) {
544
- sub.notify(existingData, existingData as any);
545
- }
546
- }, 0);
547
-
548
- return Object.freeze({ ...handles });
549
- }
550
-
551
- /**
552
- * Notifies multi subs with ALL data + deltas. Attaches handles on data if required
553
- * @param newData -> updates. Must include id_fields + updates
554
- */
555
- _notifySubscribers = (changes: Pick<ItemUpdated, "idObj" | "newItem" | "delta">[] = []) => {
556
- if (!this.isSynced) return;
557
-
558
- /* Deleted items (changes = []) do not trigger singleSubscriptions notify because it might break things */
559
- const items: AnyObject[] = [], deltas: AnyObject[] = [], ids: AnyObject[] = [];
560
- changes.map(({ idObj, newItem, delta }) => {
561
-
562
- /* Single subs do not care about the filter */
563
- this.singleSubscriptions.filter(s =>
564
-
565
- this.matchesIdObj(s.idObj, idObj)
566
-
567
- ).map(async s => {
568
- try {
569
- await s.notify(newItem, delta);
570
- } catch (e) {
571
- console.error("SyncedTable failed to notify: ", e)
572
- }
573
- });
574
-
575
- /* Preparing data for multi subs */
576
- if (this.matchesFilter(newItem)) {
577
- items.push(newItem);
578
- deltas.push(delta);
579
- ids.push(idObj);
580
- }
581
- });
582
-
583
-
584
- if (this.onChange || this.multiSubscriptions.length) {
585
- const allItems: AnyObject[] = [], allDeltas: AnyObject[] = [];
586
- this.getItems().map(d => {
587
- allItems.push({ ...d });
588
- const dIdx = items.findIndex(_d => this.matchesIdObj(d, _d));
589
- allDeltas.push(deltas[dIdx]!);
590
- });
591
-
592
- /* Notify main subscription */
593
- if (this.onChange) {
594
- try {
595
- this.onChange(allItems, allDeltas);
596
- } catch (e) {
597
- console.error("SyncedTable failed to notify onChange: ", e)
598
- }
599
- }
600
-
601
- /* Multisubs must not forget about the original filter */
602
- this.multiSubscriptions.map(async s => {
603
- try {
604
- await s.notify(allItems, allDeltas);
605
- } catch (e) {
606
- console.error("SyncedTable failed to notify: ", e)
607
- }
608
- });
609
- }
610
- }
611
-
612
- unsubscribe = (onChange: Function) => {
613
- this.singleSubscriptions = this.singleSubscriptions.filter(s => s._onChange !== onChange);
614
- this.multiSubscriptions = this.multiSubscriptions.filter(s => s._onChange !== onChange);
615
- debug("unsubscribe", this);
616
- return "ok";
617
- }
618
-
619
- getIdStr(d: AnyObject) {
620
- return this.id_fields.sort().map(key => `${d[key] || ""}`).join(".");
621
- }
622
- getIdObj(d: AnyObject) {
623
- const res: AnyObject = {};
624
- this.id_fields.sort().map(key => {
625
- res[key] = d[key];
626
- });
627
- return res;
628
- }
629
- getRowSyncObj(d: AnyObject): AnyObject {
630
- const res: AnyObject = {};
631
- [this.synced_field, ...this.id_fields].sort().map(key => {
632
- res[key] = d[key];
633
- });
634
- return res;
635
- }
636
-
637
- unsync = () => {
638
- this.dbSync?.unsync();
639
- }
640
-
641
- destroy = () => {
642
- this.unsync();
643
- this.multiSubscriptions = [];
644
- this.singleSubscriptions = [];
645
- this.itemsObj = {};
646
- this.items = [];
647
- this.onChange = undefined;
648
- }
649
-
650
- matchesFilter(item: AnyObject) {
651
- return Boolean(
652
- item &&
653
- (
654
- !this.filter ||
655
- isEmpty(this.filter) ||
656
- !Object.keys(this.filter).find(k => this.filter![k] !== item[k])
657
- )
658
- );
659
- }
660
- matchesIdObj(a: AnyObject, b: AnyObject) {
661
- return Boolean(a && b && !this.id_fields.sort().find(k => a[k] !== b[k]));
662
- }
663
-
664
- // TODO: offline-first deletes if allow_delete = true
665
- // setDeleted(idObj, fullArray){
666
- // let deleted: object[] = [];
667
-
668
- // if(fullArray) deleted = fullArray;
669
- // else {
670
- // deleted = this.getDeleted();
671
- // deleted.push(idObj);
672
- // }
673
- // if(hasWnd) window.localStorage.setItem(this.name + "_$$psql$$_deleted", <any>deleted);
674
- // }
675
- // getDeleted(){
676
- // const delStr = if(hasWnd) window.localStorage.getItem(this.name + "_$$psql$$_deleted") || '[]';
677
- // return JSON.parse(delStr);
678
- // }
679
- // syncDeleted = async () => {
680
- // try {
681
- // await Promise.all(this.getDeleted().map(async idObj => {
682
- // return this.db[this.name].delete(idObj);
683
- // }));
684
- // this.setDeleted(null, []);
685
- // return true;
686
- // } catch(e){
687
- // throw e;
688
- // }
689
- // }
690
-
691
- /**
692
- * Returns properties that are present in {n} and are different to {o}
693
- * @param o current full data item
694
- * @param n new data item
695
- */
696
- getDelta(o: AnyObject, n: AnyObject): AnyObject {
697
- if (isEmpty(o)) return { ...n };
698
- return Object.keys({ ...o, ...n })
699
- .filter(k => !this.id_fields.includes(k))
700
- .reduce((a, k) => {
701
- let delta = {};
702
- if (k in n && n[k] !== o[k]) {
703
- let deltaProp = { [k]: n[k] };
704
-
705
- /** If object then compare with stringify */
706
- if (n[k] && o[k] && typeof o[k] === "object") {
707
- if (JSON.stringify(n[k]) !== JSON.stringify(o[k])) {
708
- delta = deltaProp;
709
- }
710
- } else {
711
- delta = deltaProp;
712
- }
713
- }
714
- return { ...a, ...delta };
715
- }, {});
716
- }
717
-
718
- deleteAll() {
719
- this.getItems().map(d => this.delete(d));
720
- }
721
-
722
- get tableHandler(): Pick<TableHandler, "update" | "updateBatch" | "delete"> | undefined {
723
- const tblHandler = this.db[this.name];
724
- if(tblHandler?.update && tblHandler.updateBatch){
725
- return tblHandler as any;
726
- }
727
-
728
- return undefined;
729
- }
730
-
731
- delete = async (item: AnyObject, from_server = false) => {
732
-
733
- const idObj = this.getIdObj(item);
734
- this.setItem(idObj, undefined, true, true);
735
- if (!from_server && this.tableHandler?.delete) {
736
- await this.tableHandler.delete(idObj);
737
- }
738
- this._notifySubscribers();
739
- return true
740
- }
741
-
742
- /**
743
- * Ensures that all object keys match valid column names
744
- */
745
- checkItemCols = (item: AnyObject) => {
746
- if (this.columns.length) {
747
- const badCols = Object.keys({ ...item })
748
- .filter(k =>
749
- !this.columns.find(c => c.name === k)
750
- );
751
- if (badCols.length) {
752
- throw (`Unexpected columns in sync item update: ` + badCols.join(", "));
753
- }
754
- }
755
- }
756
-
757
- /**
758
- * Upserts data locally -> notify subs -> sends to server if required
759
- * synced_field is populated if data is not from server
760
- * @param items <{ idObj: object, delta: object }[]> Data items that changed
761
- * @param from_server : <boolean> If false then updates will be sent to server
762
- */
763
- upsert = async (items: ItemUpdate[], from_server = false): Promise<any> => {
764
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
765
- if ((!items || !items.length) && !from_server) throw "No data provided for upsert";
766
-
767
- /* If data has been deleted then wait for it to sync with server before continuing */
768
- // if(from_server && this.getDeleted().length){
769
- // await this.syncDeleted();
770
- // }
771
-
772
-
773
- const results: ItemUpdated[] = [];
774
- let status: ItemUpdated["status"];
775
- const walItems: WALItem[] = [];
776
- await Promise.all(items.map(async (item, i) => {
777
- // let d = { ...item.idObj, ...item.delta };
778
- const idObj = { ...item.idObj };
779
- let delta = { ...item.delta };
780
-
781
- /* Convert undefined to null because:
782
- 1) JSON.stringify drops these keys
783
- 2) Postgres does not have undefined
784
- */
785
- Object.keys(delta).map(k => {
786
- if (delta[k] === undefined) delta[k] = null;
787
- })
788
-
789
- if (!from_server) {
790
- this.checkItemCols({ ...item.delta, ...item.idObj });
791
- }
792
-
793
- const oItm = this.getItem(idObj),
794
- oldIdx = oItm.index,
795
- oldItem = oItm.data;
796
-
797
- /* Calc delta if missing or if from server */
798
- if ((from_server || isEmpty(delta)) && !isEmpty(oldItem)) {
799
- delta = this.getDelta(oldItem || {}, delta)
800
- }
801
-
802
- /* Add synced if local update */
803
- /** Will need to check client clock shift */
804
- if (!from_server) {
805
- delta[this.synced_field] = Date.now();
806
- }
807
-
808
- let newItem = { ...oldItem, ...delta, ...idObj };
809
- if (oldItem && !from_server) {
810
-
811
- /**
812
- * Merge deep
813
- */
814
- if (item.opts?.deepMerge) {
815
- newItem = mergeDeep({ ...oldItem, ...idObj }, { ...delta });
816
- }
817
- }
818
-
819
- /* Update existing -> Expecting delta */
820
- if (oldItem) {
821
- status = oldItem[this.synced_field] < newItem[this.synced_field] ? "updated" : "unchanged";
822
-
823
- /* Insert new item */
824
- } else {
825
- status = "inserted";
826
- }
827
-
828
- this.setItem(newItem, oldIdx);
829
-
830
- // if(!status) throw "changeInfo status missing"
831
- const changeInfo: ItemUpdated = { idObj, delta, oldItem, newItem, status, from_server };
832
-
833
- // const idStr = this.getIdStr(idObj);
834
- /* IF Local updates then Keep any existing oldItem to revert to the earliest working item */
835
- if (!from_server) {
836
-
837
- /* Patch server data if necessary and update separately to account for errors */
838
- // let updatedWithPatch = false;
839
- // if(this.columns && this.columns.length && (this.patchText || this.patchJSON)){
840
- // // const jCols = this.columns.filter(c => c.data_type === "json")
841
- // const txtCols = this.columns.filter(c => c.data_type === "text");
842
- // if(this.patchText && txtCols.length && this.db[this.name].update){
843
- // let patchedDelta;
844
- // txtCols.map(c => {
845
- // if(c.name in changeInfo.delta){
846
- // patchedDelta = patchedDelta || {
847
- // ...changeInfo.delta,
848
- // }
849
- // patchedDelta[c.name] = getTextPatch(changeInfo.oldItem[c.name], changeInfo.delta[c.name]);
850
- // }
851
- // });
852
- // if(patchedDelta){
853
- // try {
854
- // await this.db[this.name].update(idObj, patchedDelta);
855
- // updatedWithPatch = true;
856
- // } catch(e) {
857
- // console.log("failed to patch update", e)
858
- // }
859
-
860
- // }
861
- // // console.log("json-stable-stringify ???")
862
- // }
863
- // }
864
-
865
- walItems.push({
866
- initial: oldItem,
867
- current: { ...newItem }
868
- });
869
- }
870
- if (changeInfo.delta && !isEmpty(changeInfo.delta)) {
871
- results.push(changeInfo);
872
- }
873
-
874
- /* TODO: Deletes from server */
875
- // if(allow_deletes){
876
- // items = this.getItems();
877
- // }
878
-
879
- return true;
880
- })).catch(err => {
881
- console.error("SyncedTable failed upsert: ", err)
882
- });
883
-
884
- this.notifyWal?.addData(results.map(d => ({ initial: d.oldItem, current: d.newItem })))
885
-
886
- /* Push to server */
887
- if (!from_server && walItems.length) {
888
- this.wal?.addData(walItems);
889
- }
890
- }
891
-
892
- /* Returns an item by idObj from the local store */
893
- getItem<T = AnyObject>(idObj: Partial<T>): { data?: T, index: number } {
894
- const index = -1;
895
- let d;
896
- if (this.storageType === STORAGE_TYPES.localStorage) {
897
- const items = this.getItems();
898
- d = items.find(d => this.matchesIdObj(d, idObj));
899
- } else if (this.storageType === STORAGE_TYPES.array) {
900
- d = this.items.find(d => this.matchesIdObj(d, idObj));
901
- } else {
902
- this.itemsObj = this.itemsObj || {};
903
- d = ({ ...this.itemsObj })[this.getIdStr(idObj)];
904
- }
905
-
906
- return { data: quickClone(d), index };
907
- }
908
-
909
- /**
910
- *
911
- * @param item data to be inserted/updated/deleted. Must include id_fields
912
- * @param index (optional) index within array
913
- * @param isFullData
914
- * @param deleteItem
915
- */
916
- setItem(_item: AnyObject, index: number | undefined, isFullData = false, deleteItem = false) {
917
- const item = quickClone(_item);
918
- if (this.storageType === STORAGE_TYPES.localStorage) {
919
- let items = this.getItems();
920
- if (!deleteItem) {
921
- if (index !== undefined && items[index]) items[index] = isFullData ? { ...item } : { ...items[index], ...item };
922
- else items.push(item);
923
- } else items = items.filter(d => !this.matchesIdObj(d, item));
924
- if (hasWnd) window.localStorage.setItem(this.name, JSON.stringify(items));
925
- } else if (this.storageType === STORAGE_TYPES.array) {
926
- if (!deleteItem) {
927
- if (index !== undefined && !this.items[index]) {
928
- this.items.push(item);
929
- } else if (index !== undefined) {
930
- this.items[index] = isFullData ? { ...item } : { ...this.items[index], ...item };
931
- }
932
- } else this.items = this.items.filter(d => !this.matchesIdObj(d, item));
933
- } else {
934
- this.itemsObj = this.itemsObj || {};
935
- if (!deleteItem) {
936
- const existing = this.itemsObj[this.getIdStr(item)] || {};
937
- this.itemsObj[this.getIdStr(item)] = isFullData ? { ...item } : { ...existing, ...item };
938
- } else {
939
- delete this.itemsObj[this.getIdStr(item)];
940
- }
941
- }
942
- }
943
-
944
- /**
945
- * Sets the current data
946
- * @param items data
947
- */
948
- setItems = (_items: AnyObject[]): void => {
949
- const items = quickClone(_items);
950
- if (this.storageType === STORAGE_TYPES.localStorage) {
951
- if (!hasWnd) throw "Cannot access window object. Choose another storage method (array OR object)";
952
- window.localStorage.setItem(this.name, JSON.stringify(items));
953
- } else if (this.storageType === STORAGE_TYPES.array) {
954
- this.items = items;
955
- } else {
956
- this.itemsObj = items.reduce((a, v) => ({
957
- ...a,
958
- [this.getIdStr(v)]: ({ ...v }),
959
- }), {});
960
- }
961
- }
962
-
963
- /**
964
- * Returns the current data ordered by synced_field ASC and matching the main filter;
965
- */
966
- getItems = <T extends AnyObject = AnyObject>(): T[] => {
967
-
968
- let items: AnyObject[] = [];
969
-
970
- if (this.storageType === STORAGE_TYPES.localStorage) {
971
- if (!hasWnd) throw "Cannot access window object. Choose another storage method (array OR object)";
972
- const cachedStr = window.localStorage.getItem(this.name);
973
- if (cachedStr) {
974
- try {
975
- items = JSON.parse(cachedStr);
976
- } catch (e) {
977
- console.error(e);
978
- }
979
- }
980
- } else if (this.storageType === STORAGE_TYPES.array) {
981
- items = this.items.map(d => ({ ...d }));
982
- } else {
983
- items = Object.values({ ...this.itemsObj });
984
- }
985
-
986
- if (this.id_fields.length && this.synced_field) {
987
- const s_fields = [this.synced_field, ...this.id_fields.sort()];
988
- items = items
989
- .filter(d => {
990
- return !this.filter || !getKeys(this.filter)
991
- .find(key =>
992
- d[key] !== this.filter![key]
993
- // typeof d[key] === typeof this.filter[key] &&
994
- // d[key].toString && this.filter[key].toString &&
995
- // d[key].toString() !== this.filter[key].toString()
996
- );
997
- })
998
- .sort((a, b) =>
999
- s_fields.map(key =>
1000
- (a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0) as any
1001
- ).find(v => v)
1002
- );
1003
- } else throw "id_fields AND/OR synced_field missing"
1004
- // this.items = items.filter(d => isEmpty(this.filter) || this.matchesFilter(d));
1005
- return quickClone(items) as any;
1006
- }
1007
-
1008
- /**
1009
- * Sync data request
1010
- * @param param0: SyncBatchRequest
1011
- */
1012
- getBatch = ({ from_synced, to_synced, offset, limit }: SyncBatchParams = { offset: 0, limit: undefined }) => {
1013
- const items = this.getItems();
1014
- // params = params || {};
1015
- // const { from_synced, to_synced, offset = 0, limit = null } = params;
1016
- let res = items.map(c => ({ ...c }))
1017
- .filter(c =>
1018
- (!Number.isFinite(from_synced) || +c[this.synced_field] >= +from_synced!) &&
1019
- (!Number.isFinite(to_synced) || +c[this.synced_field] <= +to_synced!)
1020
- );
1021
-
1022
- if (offset || limit) res = res.splice(offset ?? 0, limit || res.length);
1023
-
1024
- return res;
1025
- }
1026
- }
1027
-
1028
- /**
1029
- * immutable args
1030
- */
1031
- export default function mergeDeep(_target, _source) {
1032
- const target = _target? quickClone(_target) : _target;
1033
- const source = _source? quickClone(_source) : _source;
1034
- const output = Object.assign({}, target);
1035
- if (isObject(target) && isObject(source)) {
1036
- Object.keys(source).forEach(key => {
1037
- if (isObject(source[key])) {
1038
- if (!(key in target)){
1039
- Object.assign(output, { [key]: source[key] });
1040
- } else {
1041
- output[key] = mergeDeep(target[key], source[key]);
1042
- }
1043
- } else {
1044
- Object.assign(output, { [key]: source[key] });
1045
- }
1046
- });
1047
- }
1048
- return output;
1049
- }
1050
-
1051
- export function quickClone<T>(obj: T): T {
1052
- if(hasWnd && "structuredClone" in window && typeof window.structuredClone === "function"){
1053
- return window.structuredClone(obj);
1054
- }
1055
- if(Array.isArray(obj)){
1056
- return obj.slice(0).map(v => quickClone(v)) as any
1057
- } else if(isObject(obj)){
1058
- const result = {} as any;
1059
- getKeys(obj).map(k => {
1060
- result[k] = quickClone(obj[k]) as any;
1061
- })
1062
- return result;
1063
- }
1064
-
1065
- return obj;
1066
- }
1067
-
1068
- /**
1069
- * Type tests
1070
- */
1071
- (async () => {
1072
- const s: Sync<{ a: number, b: string; }> = 1 as any;
1073
- const sh = s({ a: 1 }, { } as any, (d) => { d });
1074
-
1075
- const syncTyped: Sync<{ col1: string; }, ()=> any> = 1 as any;
1076
-
1077
- // const sUntyped: Sync<AnyObject, any> = syncTyped;
1078
- })