prostgles-types 4.0.249 → 4.0.250

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/lib/WAL.ts ADDED
@@ -0,0 +1,314 @@
1
+ import { isEmpty, isEqual, type AnyObject, type TS_COLUMN_DATA_TYPES } from "./index";
2
+
3
+ /* Replication */
4
+ export type SyncTableInfo = {
5
+ id_fields: string[];
6
+ synced_field: string;
7
+ throttle: number;
8
+ batch_size: number;
9
+ };
10
+
11
+ export type BasicOrderBy = {
12
+ fieldName: string;
13
+ /**
14
+ * Used to ensure numbers are not left as strings in some cases
15
+ */
16
+ tsDataType: TS_COLUMN_DATA_TYPES;
17
+ asc: boolean;
18
+ }[];
19
+
20
+ export type WALConfig = SyncTableInfo & {
21
+ /**
22
+ * Fired when new data is added and there is no sending in progress
23
+ */
24
+ onSendStart?: () => any;
25
+ /**
26
+ * Fired on each data send batch
27
+ */
28
+ onSend: (items: AnyObject[], fullItems: WALItem[]) => Promise<any>;
29
+ /**
30
+ * Fired after all data was sent or when a batch error is thrown
31
+ */
32
+ onSendEnd?: (batch: AnyObject[], fullItems: WALItem[], error?: unknown) => any;
33
+
34
+ /**
35
+ * Order by which the items will be synced. Defaults to [synced_field, ...id_fields.sort()]
36
+ */
37
+ orderBy?: BasicOrderBy;
38
+
39
+ /**
40
+ * Defaults to 2 seconds
41
+ */
42
+ historyAgeSeconds?: number;
43
+
44
+ DEBUG_MODE?: boolean;
45
+
46
+ id?: string;
47
+ };
48
+ export type WALItem = {
49
+ initial?: AnyObject;
50
+ delta?: AnyObject;
51
+ current: AnyObject;
52
+ };
53
+ export type WALItemsObj = Record<string, WALItem>;
54
+
55
+ /**
56
+ * Used to throttle and combine updates sent to server
57
+ * This allows a high rate of optimistic updates on the client
58
+ */
59
+ export class WAL {
60
+ /**
61
+ * Instantly merged records for prepared for update
62
+ */
63
+ private changed: WALItemsObj = {};
64
+
65
+ /**
66
+ * Batch of records (removed from this.changed) that are currently being sent
67
+ */
68
+ private sending: WALItemsObj = {};
69
+
70
+ /**
71
+ * Historic data used to reduce data pushes from server to client
72
+ */
73
+ private sentHistory: Record<string, AnyObject> = {};
74
+
75
+ private options: WALConfig;
76
+ private callbacks: { cb: Function; idStrs: string[] }[] = [];
77
+
78
+ constructor(args: WALConfig) {
79
+ this.options = { ...args };
80
+ if (!this.options.orderBy) {
81
+ const { synced_field, id_fields } = args;
82
+ this.options.orderBy = [synced_field, ...id_fields.sort()].map((fieldName) => ({
83
+ fieldName,
84
+ tsDataType: fieldName === synced_field ? "number" : "string",
85
+ asc: true,
86
+ }));
87
+ }
88
+ }
89
+
90
+ sort = (a?: AnyObject, b?: AnyObject): number => {
91
+ const { orderBy } = this.options;
92
+ if (!orderBy || !a || !b) return 0;
93
+
94
+ return (
95
+ orderBy
96
+ .map((ob) => {
97
+ /* TODO: add fullData to changed items + ensure orderBy is in select */
98
+ if (!(ob.fieldName in a) || !(ob.fieldName in b)) {
99
+ throw `Replication error: \n some orderBy fields missing from data`;
100
+ }
101
+ let v1 = ob.asc ? a[ob.fieldName] : b[ob.fieldName],
102
+ v2 = ob.asc ? b[ob.fieldName] : a[ob.fieldName];
103
+
104
+ let vNum = +v1 - +v2,
105
+ vStr =
106
+ v1 < v2 ? -1
107
+ : v1 == v2 ? 0
108
+ : 1;
109
+ return ob.tsDataType === "number" && Number.isFinite(vNum) ? vNum : vStr;
110
+ })
111
+ .find((v) => v) || 0
112
+ );
113
+ };
114
+
115
+ isSending(): boolean {
116
+ const result = this.isOnSending || !(isEmpty(this.sending) && isEmpty(this.changed));
117
+ if (this.options.DEBUG_MODE) {
118
+ console.log(this.options.id, " CHECKING isSending ->", result);
119
+ }
120
+ return result;
121
+ }
122
+
123
+ /**
124
+ * Used by server to avoid unnecessary data push to client.
125
+ * This can happen due to the same data item having been previously pushed by the client
126
+ * @param item data item
127
+ * @returns boolean
128
+ */
129
+ isInHistory = (item: AnyObject): boolean => {
130
+ if (!item) throw "Provide item";
131
+ const itemSyncVal = item[this.options.synced_field];
132
+ if (!Number.isFinite(+itemSyncVal))
133
+ throw "Provided item Synced field value is missing/invalid ";
134
+
135
+ const existing = this.sentHistory[this.getIdStr(item)];
136
+ const existingSyncVal = existing?.[this.options.synced_field];
137
+ if (existing) {
138
+ if (!Number.isFinite(+existingSyncVal))
139
+ throw "Provided historic item Synced field value is missing/invalid";
140
+ if (+existingSyncVal === +itemSyncVal) {
141
+ return true;
142
+ }
143
+ }
144
+ return false;
145
+ };
146
+
147
+ getIdStr(d: AnyObject): string {
148
+ return this.options.id_fields
149
+ .sort()
150
+ .map((key) => `${d[key] || ""}`)
151
+ .join(".");
152
+ }
153
+ getIdObj(d: AnyObject): AnyObject {
154
+ let res: AnyObject = {};
155
+ this.options.id_fields.sort().map((key) => {
156
+ res[key] = d[key];
157
+ });
158
+ return res;
159
+ }
160
+ getDeltaObj(d: AnyObject): AnyObject {
161
+ let res: AnyObject = {};
162
+ Object.keys(d).map((key) => {
163
+ if (!this.options.id_fields.includes(key)) {
164
+ res[key] = d[key];
165
+ }
166
+ });
167
+ return res;
168
+ }
169
+
170
+ addData = (data: WALItem[]) => {
171
+ if (isEmpty(this.changed) && this.options.onSendStart) this.options.onSendStart();
172
+
173
+ data.map((d) => {
174
+ const { initial, current, delta } = { ...d };
175
+ if (!current) throw "Expecting { current: object, initial?: object }";
176
+ const idStr = this.getIdStr(current);
177
+
178
+ this.changed ??= {};
179
+ this.changed[idStr] ??= { initial, current, delta };
180
+ this.changed[idStr]!.current = {
181
+ ...this.changed[idStr]!.current,
182
+ ...current,
183
+ };
184
+ this.changed[idStr]!.delta = {
185
+ ...this.changed[idStr]!.delta,
186
+ ...delta,
187
+ };
188
+ });
189
+ return this.sendItems();
190
+ };
191
+
192
+ isOnSending = false;
193
+ isSendingTimeout?: ReturnType<typeof setTimeout> = undefined;
194
+ willDeleteHistory?: ReturnType<typeof setTimeout> = undefined;
195
+ private sendItems = async () => {
196
+ const {
197
+ DEBUG_MODE,
198
+ onSend,
199
+ onSendEnd,
200
+ batch_size,
201
+ throttle,
202
+ historyAgeSeconds = 2,
203
+ } = this.options;
204
+
205
+ // Sending data. stop here
206
+ if (this.isSendingTimeout || (this.sending && !isEmpty(this.sending))) return;
207
+
208
+ // Nothing to send. stop here
209
+ if (!this.changed || isEmpty(this.changed)) return;
210
+
211
+ // Prepare batch to send
212
+ let batchItems: AnyObject[] = [],
213
+ walBatch: WALItem[] = [],
214
+ batchObj: Record<string, AnyObject> = {};
215
+
216
+ /**
217
+ * Prepare and remove a batch from this.changed
218
+ */
219
+ Object.keys(this.changed)
220
+ .sort((a, b) => this.sort(this.changed[a]!.current, this.changed[b]!.current))
221
+ .slice(0, batch_size)
222
+ .map((key) => {
223
+ let item = { ...this.changed[key] } as WALItem;
224
+ this.sending[key] = { ...item };
225
+ walBatch.push({ ...item });
226
+
227
+ /* Used for history */
228
+ batchObj[key] = { ...item.current };
229
+
230
+ delete this.changed[key];
231
+ });
232
+ batchItems = walBatch.map((d) => {
233
+ let result: AnyObject = {};
234
+ Object.keys(d.current).map((k) => {
235
+ const oldVal = d.initial?.[k];
236
+ const newVal = d.current[k];
237
+ /** Send only id fields and delta */
238
+ if (
239
+ [this.options.synced_field, ...this.options.id_fields].includes(k) ||
240
+ !isEqual(oldVal, newVal)
241
+ ) {
242
+ result[k] = newVal;
243
+ }
244
+ });
245
+ return result;
246
+ });
247
+
248
+ if (DEBUG_MODE) {
249
+ console.log(this.options.id, " SENDING lr->", batchItems[batchItems.length - 1]);
250
+ }
251
+
252
+ // Throttle next data send
253
+ if (!this.isSendingTimeout) {
254
+ this.isSendingTimeout = setTimeout(() => {
255
+ this.isSendingTimeout = undefined;
256
+ if (!isEmpty(this.changed)) {
257
+ this.sendItems();
258
+ }
259
+ }, throttle);
260
+ }
261
+
262
+ let error: any;
263
+ this.isOnSending = true;
264
+ try {
265
+ /* Deleted data should be sent normally through await db.table.delete(...) */
266
+ await onSend(batchItems, walBatch); //, deletedData);
267
+
268
+ /**
269
+ * Keep history if required
270
+ */
271
+ if (historyAgeSeconds) {
272
+ this.sentHistory = {
273
+ ...this.sentHistory,
274
+ ...batchObj,
275
+ };
276
+ /**
277
+ * Delete history after some time
278
+ */
279
+ if (!this.willDeleteHistory) {
280
+ this.willDeleteHistory = setTimeout(() => {
281
+ this.willDeleteHistory = undefined;
282
+ this.sentHistory = {};
283
+ }, historyAgeSeconds * 1000);
284
+ }
285
+ }
286
+ } catch (err) {
287
+ error = err;
288
+ console.error("WAL onSend failed:", err, batchItems, walBatch);
289
+ }
290
+ this.isOnSending = false;
291
+
292
+ /* Fire any callbacks */
293
+ if (this.callbacks.length) {
294
+ const ids = Object.keys(this.sending);
295
+ this.callbacks.forEach((c, i) => {
296
+ c.idStrs = c.idStrs.filter((id) => ids.includes(id));
297
+ if (!c.idStrs.length) {
298
+ c.cb(error);
299
+ }
300
+ });
301
+ this.callbacks = this.callbacks.filter((cb) => cb.idStrs.length);
302
+ }
303
+
304
+ this.sending = {};
305
+ if (DEBUG_MODE) {
306
+ console.log(this.options.id, " SENT lr->", batchItems[batchItems.length - 1]);
307
+ }
308
+ if (!isEmpty(this.changed)) {
309
+ this.sendItems();
310
+ } else {
311
+ if (onSendEnd) onSendEnd(batchItems, walBatch, error);
312
+ }
313
+ };
314
+ }
package/lib/index.ts CHANGED
@@ -2,8 +2,9 @@ import * as AuthTypes from "./auth";
2
2
  import { FileColumnConfig } from "./files";
3
3
  import { AnyObject, ComplexFilter, FullFilter, ValueOf } from "./filters";
4
4
  import { JSONB } from "./JSONBSchemaValidation/JSONBSchema";
5
- import { getKeys, isDefined, type SyncTableInfo } from "./util";
5
+ import { getKeys, isDefined } from "./util";
6
6
  import { includes } from "./utilFuncs/includes";
7
+ import type { SyncTableInfo } from "./WAL";
7
8
  export const _PG_strings = [
8
9
  "bpchar",
9
10
  "char",
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prostgles-types",
3
- "version": "4.0.249",
3
+ "version": "4.0.250",
4
4
  "description": "",
5
5
  "main": "dist/index_umd.js",
6
6
  "types": "dist/index.d.ts",