prostgles-types 4.0.249 → 4.0.251
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/dist/JSONBSchemaValidation/JSONBSchema.d.ts +3 -3
- package/dist/WAL.d.ts +84 -0
- package/dist/WAL.d.ts.map +1 -0
- package/dist/WAL.js +232 -0
- package/dist/WAL.js.map +1 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/index_umd.js +1 -1
- package/dist/replication.d.ts +2 -2
- package/dist/replication.d.ts.map +1 -1
- package/dist/replication.js +1 -1
- package/dist/replication.js.map +1 -1
- package/dist/util.d.ts +1 -83
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +1 -236
- package/dist/util.js.map +1 -1
- package/lib/WAL.ts +314 -0
- package/lib/index.ts +3 -1
- package/lib/replication.ts +3 -3
- package/lib/util.ts +0 -321
- package/package.json +1 -1
package/lib/util.ts
CHANGED
|
@@ -177,319 +177,6 @@ export function unpatchText(original: string | null, patch: TextPatch): string {
|
|
|
177
177
|
return res;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
/* Replication */
|
|
181
|
-
export type SyncTableInfo = {
|
|
182
|
-
id_fields: string[];
|
|
183
|
-
synced_field: string;
|
|
184
|
-
throttle: number;
|
|
185
|
-
batch_size: number;
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
export type BasicOrderBy = {
|
|
189
|
-
fieldName: string;
|
|
190
|
-
/**
|
|
191
|
-
* Used to ensure numbers are not left as strings in some cases
|
|
192
|
-
*/
|
|
193
|
-
tsDataType: TS_COLUMN_DATA_TYPES;
|
|
194
|
-
asc: boolean;
|
|
195
|
-
}[];
|
|
196
|
-
|
|
197
|
-
export type WALConfig = SyncTableInfo & {
|
|
198
|
-
/**
|
|
199
|
-
* Fired when new data is added and there is no sending in progress
|
|
200
|
-
*/
|
|
201
|
-
onSendStart?: () => any;
|
|
202
|
-
/**
|
|
203
|
-
* Fired on each data send batch
|
|
204
|
-
*/
|
|
205
|
-
onSend: (items: any[], fullItems: WALItem[]) => Promise<any>;
|
|
206
|
-
/**
|
|
207
|
-
* Fired after all data was sent or when a batch error is thrown
|
|
208
|
-
*/
|
|
209
|
-
onSendEnd?: (batch: any[], fullItems: WALItem[], error?: any) => any;
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Order by which the items will be synced. Defaults to [synced_field, ...id_fields.sort()]
|
|
213
|
-
*/
|
|
214
|
-
orderBy?: BasicOrderBy;
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Defaults to 2 seconds
|
|
218
|
-
*/
|
|
219
|
-
historyAgeSeconds?: number;
|
|
220
|
-
|
|
221
|
-
DEBUG_MODE?: boolean;
|
|
222
|
-
|
|
223
|
-
id?: string;
|
|
224
|
-
};
|
|
225
|
-
export type WALItem = {
|
|
226
|
-
initial?: AnyObject;
|
|
227
|
-
delta?: AnyObject;
|
|
228
|
-
current: AnyObject;
|
|
229
|
-
};
|
|
230
|
-
export type WALItemsObj = Record<string, WALItem>;
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Used to throttle and combine updates sent to server
|
|
234
|
-
* This allows a high rate of optimistic updates on the client
|
|
235
|
-
*/
|
|
236
|
-
export class WAL {
|
|
237
|
-
/**
|
|
238
|
-
* Instantly merged records for prepared for update
|
|
239
|
-
*/
|
|
240
|
-
private changed: WALItemsObj = {};
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Batch of records (removed from this.changed) that are currently being sent
|
|
244
|
-
*/
|
|
245
|
-
private sending: WALItemsObj = {};
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Historic data used to reduce data pushes from server to client
|
|
249
|
-
*/
|
|
250
|
-
private sentHistory: Record<string, AnyObject> = {};
|
|
251
|
-
|
|
252
|
-
private options: WALConfig;
|
|
253
|
-
private callbacks: { cb: Function; idStrs: string[] }[] = [];
|
|
254
|
-
|
|
255
|
-
constructor(args: WALConfig) {
|
|
256
|
-
this.options = { ...args };
|
|
257
|
-
if (!this.options.orderBy) {
|
|
258
|
-
const { synced_field, id_fields } = args;
|
|
259
|
-
this.options.orderBy = [synced_field, ...id_fields.sort()].map((fieldName) => ({
|
|
260
|
-
fieldName,
|
|
261
|
-
tsDataType: fieldName === synced_field ? "number" : "string",
|
|
262
|
-
asc: true,
|
|
263
|
-
}));
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
sort = (a?: AnyObject, b?: AnyObject): number => {
|
|
268
|
-
const { orderBy } = this.options;
|
|
269
|
-
if (!orderBy || !a || !b) return 0;
|
|
270
|
-
|
|
271
|
-
return (
|
|
272
|
-
orderBy
|
|
273
|
-
.map((ob) => {
|
|
274
|
-
/* TODO: add fullData to changed items + ensure orderBy is in select */
|
|
275
|
-
if (!(ob.fieldName in a) || !(ob.fieldName in b)) {
|
|
276
|
-
throw `Replication error: \n some orderBy fields missing from data`;
|
|
277
|
-
}
|
|
278
|
-
let v1 = ob.asc ? a[ob.fieldName] : b[ob.fieldName],
|
|
279
|
-
v2 = ob.asc ? b[ob.fieldName] : a[ob.fieldName];
|
|
280
|
-
|
|
281
|
-
let vNum = +v1 - +v2,
|
|
282
|
-
vStr =
|
|
283
|
-
v1 < v2 ? -1
|
|
284
|
-
: v1 == v2 ? 0
|
|
285
|
-
: 1;
|
|
286
|
-
return ob.tsDataType === "number" && Number.isFinite(vNum) ? vNum : vStr;
|
|
287
|
-
})
|
|
288
|
-
.find((v) => v) || 0
|
|
289
|
-
);
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
isSending(): boolean {
|
|
293
|
-
const result = this.isOnSending || !(isEmpty(this.sending) && isEmpty(this.changed));
|
|
294
|
-
if (this.options.DEBUG_MODE) {
|
|
295
|
-
console.log(this.options.id, " CHECKING isSending ->", result);
|
|
296
|
-
}
|
|
297
|
-
return result;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Used by server to avoid unnecessary data push to client.
|
|
302
|
-
* This can happen due to the same data item having been previously pushed by the client
|
|
303
|
-
* @param item data item
|
|
304
|
-
* @returns boolean
|
|
305
|
-
*/
|
|
306
|
-
isInHistory = (item: AnyObject): boolean => {
|
|
307
|
-
if (!item) throw "Provide item";
|
|
308
|
-
const itemSyncVal = item[this.options.synced_field];
|
|
309
|
-
if (!Number.isFinite(+itemSyncVal))
|
|
310
|
-
throw "Provided item Synced field value is missing/invalid ";
|
|
311
|
-
|
|
312
|
-
const existing = this.sentHistory[this.getIdStr(item)];
|
|
313
|
-
const existingSyncVal = existing?.[this.options.synced_field];
|
|
314
|
-
if (existing) {
|
|
315
|
-
if (!Number.isFinite(+existingSyncVal))
|
|
316
|
-
throw "Provided historic item Synced field value is missing/invalid";
|
|
317
|
-
if (+existingSyncVal === +itemSyncVal) {
|
|
318
|
-
return true;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
return false;
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
getIdStr(d: AnyObject): string {
|
|
325
|
-
return this.options.id_fields
|
|
326
|
-
.sort()
|
|
327
|
-
.map((key) => `${d[key] || ""}`)
|
|
328
|
-
.join(".");
|
|
329
|
-
}
|
|
330
|
-
getIdObj(d: AnyObject): AnyObject {
|
|
331
|
-
let res: AnyObject = {};
|
|
332
|
-
this.options.id_fields.sort().map((key) => {
|
|
333
|
-
res[key] = d[key];
|
|
334
|
-
});
|
|
335
|
-
return res;
|
|
336
|
-
}
|
|
337
|
-
getDeltaObj(d: AnyObject): AnyObject {
|
|
338
|
-
let res: AnyObject = {};
|
|
339
|
-
Object.keys(d).map((key) => {
|
|
340
|
-
if (!this.options.id_fields.includes(key)) {
|
|
341
|
-
res[key] = d[key];
|
|
342
|
-
}
|
|
343
|
-
});
|
|
344
|
-
return res;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
addData = (data: WALItem[]) => {
|
|
348
|
-
if (isEmpty(this.changed) && this.options.onSendStart) this.options.onSendStart();
|
|
349
|
-
|
|
350
|
-
data.map((d) => {
|
|
351
|
-
const { initial, current, delta } = { ...d };
|
|
352
|
-
if (!current) throw "Expecting { current: object, initial?: object }";
|
|
353
|
-
const idStr = this.getIdStr(current);
|
|
354
|
-
|
|
355
|
-
this.changed ??= {};
|
|
356
|
-
this.changed[idStr] ??= { initial, current, delta };
|
|
357
|
-
this.changed[idStr]!.current = {
|
|
358
|
-
...this.changed[idStr]!.current,
|
|
359
|
-
...current,
|
|
360
|
-
};
|
|
361
|
-
this.changed[idStr]!.delta = {
|
|
362
|
-
...this.changed[idStr]!.delta,
|
|
363
|
-
...delta,
|
|
364
|
-
};
|
|
365
|
-
});
|
|
366
|
-
return this.sendItems();
|
|
367
|
-
};
|
|
368
|
-
|
|
369
|
-
isOnSending = false;
|
|
370
|
-
isSendingTimeout?: ReturnType<typeof setTimeout> = undefined;
|
|
371
|
-
willDeleteHistory?: ReturnType<typeof setTimeout> = undefined;
|
|
372
|
-
private sendItems = async () => {
|
|
373
|
-
const {
|
|
374
|
-
DEBUG_MODE,
|
|
375
|
-
onSend,
|
|
376
|
-
onSendEnd,
|
|
377
|
-
batch_size,
|
|
378
|
-
throttle,
|
|
379
|
-
historyAgeSeconds = 2,
|
|
380
|
-
} = this.options;
|
|
381
|
-
|
|
382
|
-
// Sending data. stop here
|
|
383
|
-
if (this.isSendingTimeout || (this.sending && !isEmpty(this.sending))) return;
|
|
384
|
-
|
|
385
|
-
// Nothing to send. stop here
|
|
386
|
-
if (!this.changed || isEmpty(this.changed)) return;
|
|
387
|
-
|
|
388
|
-
// Prepare batch to send
|
|
389
|
-
let batchItems: AnyObject[] = [],
|
|
390
|
-
walBatch: WALItem[] = [],
|
|
391
|
-
batchObj: Record<string, AnyObject> = {};
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* Prepare and remove a batch from this.changed
|
|
395
|
-
*/
|
|
396
|
-
Object.keys(this.changed)
|
|
397
|
-
.sort((a, b) => this.sort(this.changed[a]!.current, this.changed[b]!.current))
|
|
398
|
-
.slice(0, batch_size)
|
|
399
|
-
.map((key) => {
|
|
400
|
-
let item = { ...this.changed[key] } as WALItem;
|
|
401
|
-
this.sending[key] = { ...item };
|
|
402
|
-
walBatch.push({ ...item });
|
|
403
|
-
|
|
404
|
-
/* Used for history */
|
|
405
|
-
batchObj[key] = { ...item.current };
|
|
406
|
-
|
|
407
|
-
delete this.changed[key];
|
|
408
|
-
});
|
|
409
|
-
batchItems = walBatch.map((d) => {
|
|
410
|
-
let result: AnyObject = {};
|
|
411
|
-
Object.keys(d.current).map((k) => {
|
|
412
|
-
const oldVal = d.initial?.[k];
|
|
413
|
-
const newVal = d.current[k];
|
|
414
|
-
/** Send only id fields and delta */
|
|
415
|
-
if (
|
|
416
|
-
[this.options.synced_field, ...this.options.id_fields].includes(k) ||
|
|
417
|
-
!areEqual(oldVal, newVal)
|
|
418
|
-
) {
|
|
419
|
-
result[k] = newVal;
|
|
420
|
-
}
|
|
421
|
-
});
|
|
422
|
-
return result;
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
if (DEBUG_MODE) {
|
|
426
|
-
console.log(this.options.id, " SENDING lr->", batchItems[batchItems.length - 1]);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Throttle next data send
|
|
430
|
-
if (!this.isSendingTimeout) {
|
|
431
|
-
this.isSendingTimeout = setTimeout(() => {
|
|
432
|
-
this.isSendingTimeout = undefined;
|
|
433
|
-
if (!isEmpty(this.changed)) {
|
|
434
|
-
this.sendItems();
|
|
435
|
-
}
|
|
436
|
-
}, throttle);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
let error: any;
|
|
440
|
-
this.isOnSending = true;
|
|
441
|
-
try {
|
|
442
|
-
/* Deleted data should be sent normally through await db.table.delete(...) */
|
|
443
|
-
await onSend(batchItems, walBatch); //, deletedData);
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* Keep history if required
|
|
447
|
-
*/
|
|
448
|
-
if (historyAgeSeconds) {
|
|
449
|
-
this.sentHistory = {
|
|
450
|
-
...this.sentHistory,
|
|
451
|
-
...batchObj,
|
|
452
|
-
};
|
|
453
|
-
/**
|
|
454
|
-
* Delete history after some time
|
|
455
|
-
*/
|
|
456
|
-
if (!this.willDeleteHistory) {
|
|
457
|
-
this.willDeleteHistory = setTimeout(() => {
|
|
458
|
-
this.willDeleteHistory = undefined;
|
|
459
|
-
this.sentHistory = {};
|
|
460
|
-
}, historyAgeSeconds * 1000);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
} catch (err) {
|
|
464
|
-
error = err;
|
|
465
|
-
console.error("WAL onSend failed:", err, batchItems, walBatch);
|
|
466
|
-
}
|
|
467
|
-
this.isOnSending = false;
|
|
468
|
-
|
|
469
|
-
/* Fire any callbacks */
|
|
470
|
-
if (this.callbacks.length) {
|
|
471
|
-
const ids = Object.keys(this.sending);
|
|
472
|
-
this.callbacks.forEach((c, i) => {
|
|
473
|
-
c.idStrs = c.idStrs.filter((id) => ids.includes(id));
|
|
474
|
-
if (!c.idStrs.length) {
|
|
475
|
-
c.cb(error);
|
|
476
|
-
}
|
|
477
|
-
});
|
|
478
|
-
this.callbacks = this.callbacks.filter((cb) => cb.idStrs.length);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
this.sending = {};
|
|
482
|
-
if (DEBUG_MODE) {
|
|
483
|
-
console.log(this.options.id, " SENT lr->", batchItems[batchItems.length - 1]);
|
|
484
|
-
}
|
|
485
|
-
if (!isEmpty(this.changed)) {
|
|
486
|
-
this.sendItems();
|
|
487
|
-
} else {
|
|
488
|
-
if (onSendEnd) onSendEnd(batchItems, walBatch, error);
|
|
489
|
-
}
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
|
|
493
180
|
export function isEmpty(obj?: any): boolean {
|
|
494
181
|
for (var v in obj) return false;
|
|
495
182
|
return true;
|
|
@@ -521,14 +208,6 @@ export const getObjectEntries = <T extends Record<string, any>>(
|
|
|
521
208
|
return Object.entries(obj) as [keyof T, T[keyof T]][];
|
|
522
209
|
};
|
|
523
210
|
|
|
524
|
-
function areEqual(a: any, b: any) {
|
|
525
|
-
if (a === b) return true;
|
|
526
|
-
if (["number", "string", "boolean"].includes(typeof a)) {
|
|
527
|
-
return a === b;
|
|
528
|
-
}
|
|
529
|
-
return JSON.stringify(a) === JSON.stringify(b);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
211
|
export function isObject(obj: any | undefined): obj is Record<string, any> {
|
|
533
212
|
return Boolean(obj && typeof obj === "object" && !Array.isArray(obj));
|
|
534
213
|
}
|