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.
- package/.eslintignore +1 -0
- package/.prettierignore +2 -0
- package/.prettierrc +4 -0
- package/.vscode/settings.json +10 -1
- package/dist/Auth.d.ts +2 -2
- package/dist/Auth.d.ts.map +1 -1
- package/dist/Auth.js +28 -18
- package/dist/SyncedTable/SyncedTable.d.ts +12 -12
- package/dist/SyncedTable/SyncedTable.d.ts.map +1 -1
- package/dist/SyncedTable/SyncedTable.js +92 -78
- package/dist/SyncedTable/getMultiSyncSubscription.d.ts.map +1 -1
- package/dist/SyncedTable/getMultiSyncSubscription.js +11 -9
- package/dist/{getDBO.d.ts → getDbHandler.d.ts} +3 -3
- package/dist/getDbHandler.d.ts.map +1 -0
- package/dist/{subscriptionHandler.d.ts → getSubscriptionHandler.d.ts} +1 -1
- package/dist/getSubscriptionHandler.d.ts.map +1 -0
- package/dist/{syncHandler.d.ts → getSyncHandler.d.ts} +1 -1
- package/dist/getSyncHandler.d.ts.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.no-sync.js +1 -1
- package/dist/prostgles.d.ts +24 -0
- package/dist/prostgles.d.ts.map +1 -1
- package/dist/prostgles.js +11 -10
- package/package.json +3 -2
- package/tsconfig.json +1 -3
- package/dist/getDBO.d.ts.map +0 -1
- package/dist/subscriptionHandler.d.ts.map +0 -1
- package/dist/syncHandler.d.ts.map +0 -1
- package/dist/typeTests.d.ts +0 -2
- package/dist/typeTests.d.ts.map +0 -1
- package/dist/typeTests.js +0 -37
- package/lib/Auth.ts +0 -155
- package/lib/FunctionQueuer.ts +0 -71
- package/lib/SyncedTable/SyncedTable.ts +0 -1078
- package/lib/SyncedTable/getMultiSyncSubscription.ts +0 -67
- package/lib/SyncedTable/getSingleSyncSubscription.ts +0 -0
- package/lib/getDBO.ts +0 -152
- package/lib/getMethods.ts +0 -30
- package/lib/getSqlHandler.ts +0 -174
- package/lib/md5.ts +0 -183
- package/lib/prostgles-full-cdn.ts +0 -5
- package/lib/prostgles-full.ts +0 -8
- package/lib/prostgles.ts +0 -350
- package/lib/react-hooks.ts +0 -356
- package/lib/subscriptionHandler.ts +0 -211
- package/lib/syncHandler.ts +0 -201
- package/lib/typeTests.ts +0 -64
- package/lib/useProstglesClient.ts +0 -92
- package/tests/package-lock.json +0 -71
- package/tests/package.json +0 -17
- package/tests/test.ts +0 -10
- package/tests/tsconfig.json +0 -27
- /package/dist/{getDBO.js → getDbHandler.js} +0 -0
- /package/dist/{subscriptionHandler.js → getSubscriptionHandler.js} +0 -0
- /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
|
-
})
|