meadow-integration 1.0.12 → 1.0.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meadow-integration",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
4
4
  "description": "Meadow Data Integration",
5
5
  "bin": {
6
6
  "mdwint": "source/cli/Meadow-Integration-CLI-Run.js"
@@ -42,6 +42,10 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
42
42
  this.SyncDeletedRecords = this.options.SyncDeletedRecords || false;
43
43
  this.MaxRecordsPerEntity = this.options.MaxRecordsPerEntity || 0;
44
44
 
45
+ // Minimum range size for bisection -- when a range is this small or smaller,
46
+ // pull all records in the range from the server instead of subdividing further.
47
+ this.BisectMinRangeSize = this.options.BisectMinRangeSize || 1000;
48
+
45
49
  this.Meadow = false;
46
50
 
47
51
  this.operation = new libMeadowOperation(this.fable);
@@ -144,6 +148,426 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
144
148
  return tmpRecordToCommit;
145
149
  }
146
150
 
151
+ // ---- REST / Local query helpers ----
152
+
153
+ // Format a date value for use in Meadow REST filter expressions (FBV).
154
+ _formatDateForFilter(pDate)
155
+ {
156
+ return this.fable.Dates.dayJS.utc(pDate).format('YYYY-MM-DDTHH:mm:ss.SSS');
157
+ }
158
+
159
+ // Get a count from the remote server, optionally filtered.
160
+ _getServerCount(pFilter, fCallback)
161
+ {
162
+ const tmpURL = pFilter
163
+ ? `${this.EntitySchema.TableName}s/Count/FilteredTo/${pFilter}`
164
+ : `${this.EntitySchema.TableName}s/Count`;
165
+ this.fable.MeadowCloneRestClient.getJSON(tmpURL,
166
+ (pError, pResponse, pBody) =>
167
+ {
168
+ if (pError)
169
+ {
170
+ return fCallback(pError);
171
+ }
172
+ if (pBody && pBody.hasOwnProperty('Count'))
173
+ {
174
+ return fCallback(null, pBody.Count);
175
+ }
176
+ return fCallback(null, 0);
177
+ });
178
+ }
179
+
180
+ // Get a page of records from the remote server with a Meadow filter expression.
181
+ _getServerRecords(pFilter, pOffset, pPageSize, fCallback)
182
+ {
183
+ const tmpURL = `${this.EntitySchema.TableName}s/FilteredTo/${pFilter}/${pOffset}/${pPageSize}`;
184
+ this.fable.MeadowCloneRestClient.getJSON(tmpURL,
185
+ (pError, pResponse, pBody) =>
186
+ {
187
+ if (pError)
188
+ {
189
+ return fCallback(pError);
190
+ }
191
+ if (pBody && Array.isArray(pBody))
192
+ {
193
+ return fCallback(null, pBody);
194
+ }
195
+ return fCallback(null, []);
196
+ });
197
+ }
198
+
199
+ // Get a count from the local database with optional ID range filters.
200
+ _getLocalCount(pMinID, pMaxID, fCallback)
201
+ {
202
+ const tmpQuery = this.Meadow.query;
203
+ if (pMinID > 0)
204
+ {
205
+ tmpQuery.addFilter(this.DefaultIdentifier, pMinID, '>=');
206
+ }
207
+ if (pMaxID > 0)
208
+ {
209
+ tmpQuery.addFilter(this.DefaultIdentifier, pMaxID, '<=');
210
+ }
211
+ if (!this._hasDeletedColumn)
212
+ {
213
+ tmpQuery.setDisableDeleteTracking(true);
214
+ }
215
+ this.Meadow.doCount(tmpQuery,
216
+ (pError, pQuery, pCount) =>
217
+ {
218
+ if (pError)
219
+ {
220
+ return fCallback(pError);
221
+ }
222
+ return fCallback(null, pCount);
223
+ });
224
+ }
225
+
226
+ // Get the max UpdateDate from local records in an ID range.
227
+ _getLocalMaxUpdateDate(pMinID, pMaxID, fCallback)
228
+ {
229
+ const tmpQuery = this.Meadow.query;
230
+ if (pMinID > 0)
231
+ {
232
+ tmpQuery.addFilter(this.DefaultIdentifier, pMinID, '>=');
233
+ }
234
+ if (pMaxID > 0)
235
+ {
236
+ tmpQuery.addFilter(this.DefaultIdentifier, pMaxID, '<=');
237
+ }
238
+ tmpQuery.setSort({ Column: 'UpdateDate', Direction: 'Descending' });
239
+ tmpQuery.setCap(1);
240
+ if (!this._hasDeletedColumn)
241
+ {
242
+ tmpQuery.setDisableDeleteTracking(true);
243
+ }
244
+ this.Meadow.doRead(tmpQuery,
245
+ (pError, pQuery, pRecord) =>
246
+ {
247
+ if (pError)
248
+ {
249
+ return fCallback(pError);
250
+ }
251
+ if (!pRecord || !pRecord.UpdateDate)
252
+ {
253
+ return fCallback(null, false);
254
+ }
255
+ return fCallback(null, pRecord.UpdateDate);
256
+ });
257
+ }
258
+
259
+ // Get the min UpdateDate from local records in an ID range.
260
+ _getLocalMinUpdateDate(pMinID, pMaxID, fCallback)
261
+ {
262
+ const tmpQuery = this.Meadow.query;
263
+ if (pMinID > 0)
264
+ {
265
+ tmpQuery.addFilter(this.DefaultIdentifier, pMinID, '>=');
266
+ }
267
+ if (pMaxID > 0)
268
+ {
269
+ tmpQuery.addFilter(this.DefaultIdentifier, pMaxID, '<=');
270
+ }
271
+ tmpQuery.setSort({ Column: 'UpdateDate', Direction: 'Ascending' });
272
+ tmpQuery.setCap(1);
273
+ if (!this._hasDeletedColumn)
274
+ {
275
+ tmpQuery.setDisableDeleteTracking(true);
276
+ }
277
+ this.Meadow.doRead(tmpQuery,
278
+ (pError, pQuery, pRecord) =>
279
+ {
280
+ if (pError)
281
+ {
282
+ return fCallback(pError);
283
+ }
284
+ if (!pRecord || !pRecord.UpdateDate)
285
+ {
286
+ return fCallback(null, false);
287
+ }
288
+ return fCallback(null, pRecord.UpdateDate);
289
+ });
290
+ }
291
+
292
+ // Upsert a single record from the server into the local database.
293
+ _upsertRecord(pServerRecord, fCallback)
294
+ {
295
+ const tmpRecordToCommit = this.marshalRecord(pServerRecord);
296
+
297
+ const tmpQuery = this.Meadow.query;
298
+ tmpQuery.addFilter(this.DefaultIdentifier, pServerRecord[this.DefaultIdentifier]);
299
+ if (!this._hasDeletedColumn)
300
+ {
301
+ tmpQuery.setDisableDeleteTracking(true);
302
+ }
303
+
304
+ this.Meadow.doRead(tmpQuery,
305
+ (pReadError, pQuery, pLocalRecord) =>
306
+ {
307
+ if (pReadError)
308
+ {
309
+ this.fable.log.error(`Error reading local record ${this.EntitySchema.TableName} ID ${pServerRecord[this.DefaultIdentifier]}: ${pReadError}`);
310
+ return fCallback();
311
+ }
312
+
313
+ const tmpSyncQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
314
+ tmpSyncQuery.setDisableAutoIdentity(true);
315
+ tmpSyncQuery.setDisableAutoDateStamp(true);
316
+ tmpSyncQuery.setDisableAutoUserStamp(true);
317
+ tmpSyncQuery.setDisableDeleteTracking(true);
318
+
319
+ if (!pLocalRecord)
320
+ {
321
+ // Record does not exist locally -- create
322
+ tmpSyncQuery.AllowIdentityInsert = true;
323
+ this.Meadow.doCreate(tmpSyncQuery,
324
+ (pCreateError) =>
325
+ {
326
+ if (pCreateError)
327
+ {
328
+ let tmpErrorStr = (typeof(pCreateError) === 'string') ? pCreateError : JSON.stringify(pCreateError);
329
+ if (tmpErrorStr.toLowerCase().indexOf('duplicate') > -1 || tmpErrorStr.toLowerCase().indexOf('unique') > -1)
330
+ {
331
+ // GUID conflict -- fall back to update
332
+ this.log.warn(`Duplicate key on create for ${this.EntitySchema.TableName} ID ${pServerRecord[this.DefaultIdentifier]}; falling back to update.`);
333
+ const tmpFallbackQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
334
+ tmpFallbackQuery.setDisableAutoIdentity(true);
335
+ tmpFallbackQuery.setDisableAutoDateStamp(true);
336
+ tmpFallbackQuery.setDisableAutoUserStamp(true);
337
+ tmpFallbackQuery.setDisableDeleteTracking(true);
338
+ this.Meadow.doUpdate(tmpFallbackQuery,
339
+ (pUpdateError) =>
340
+ {
341
+ if (pUpdateError)
342
+ {
343
+ this.log.error(`Fallback update also failed for ${this.EntitySchema.TableName} ID ${pServerRecord[this.DefaultIdentifier]}: ${pUpdateError}`);
344
+ }
345
+ return fCallback();
346
+ });
347
+ return;
348
+ }
349
+ this.log.error(`Error creating record ${this.EntitySchema.TableName}: ${pCreateError}`, pCreateError);
350
+ return fCallback();
351
+ }
352
+ return fCallback();
353
+ });
354
+ }
355
+ else
356
+ {
357
+ // Record exists locally -- update
358
+ this.Meadow.doUpdate(tmpSyncQuery,
359
+ (pUpdateError) =>
360
+ {
361
+ if (pUpdateError)
362
+ {
363
+ this.log.error(`Error updating record ${this.EntitySchema.TableName}: ${pUpdateError}`, pUpdateError);
364
+ }
365
+ return fCallback();
366
+ });
367
+ }
368
+ });
369
+ }
370
+
371
+ // Pull all records from server matching a filter expression and upsert them locally.
372
+ // Fetches in pages of this.PageSize.
373
+ _pullServerRecords(pFilter, pEstimatedCount, fCallback)
374
+ {
375
+ if (pEstimatedCount < 1)
376
+ {
377
+ return fCallback(null, 0);
378
+ }
379
+
380
+ let tmpSyncedCount = 0;
381
+ let tmpOffset = 0;
382
+ let tmpDone = false;
383
+
384
+ let tmpRecordCap = (this.MaxRecordsPerEntity > 0)
385
+ ? Math.min(pEstimatedCount, this.MaxRecordsPerEntity)
386
+ : pEstimatedCount;
387
+
388
+ const fFetchPage = () =>
389
+ {
390
+ if (tmpDone || tmpOffset >= tmpRecordCap)
391
+ {
392
+ return fCallback(null, tmpSyncedCount);
393
+ }
394
+
395
+ this._getServerRecords(pFilter, tmpOffset, this.PageSize,
396
+ (pError, pRecords) =>
397
+ {
398
+ if (pError)
399
+ {
400
+ this.fable.log.error(`Error fetching ${this.EntitySchema.TableName} page at offset ${tmpOffset}: ${pError}`);
401
+ return fCallback(pError, tmpSyncedCount);
402
+ }
403
+ if (!pRecords || pRecords.length < 1)
404
+ {
405
+ tmpDone = true;
406
+ return fCallback(null, tmpSyncedCount);
407
+ }
408
+
409
+ this.fable.Utility.eachLimit(pRecords, 5,
410
+ (pRecord, fRecordDone) =>
411
+ {
412
+ this._upsertRecord(pRecord,
413
+ () =>
414
+ {
415
+ tmpSyncedCount++;
416
+ return fRecordDone();
417
+ });
418
+ },
419
+ (pUpsertError) =>
420
+ {
421
+ tmpOffset += this.PageSize;
422
+ if (pRecords.length < this.PageSize)
423
+ {
424
+ tmpDone = true;
425
+ return fCallback(null, tmpSyncedCount);
426
+ }
427
+ this.fable.log.info(`${this.EntitySchema.TableName}: pulled ${tmpSyncedCount} of ~${tmpRecordCap} records...`);
428
+ return fFetchPage();
429
+ });
430
+ });
431
+ };
432
+
433
+ fFetchPage();
434
+ }
435
+
436
+ // ---- Bisection logic ----
437
+
438
+ // Compare a local ID range against the server. If counts or date boundaries
439
+ // differ, subdivide until the range is small enough, then pull all records in
440
+ // the range from the server to bring local in sync.
441
+ _bisectRange(pMinID, pMaxID, pDepth, fCallback)
442
+ {
443
+ const tmpRangeSize = pMaxID - pMinID + 1;
444
+ const tmpIDCol = this.DefaultIdentifier;
445
+ const tmpRangeFilter = `FBV~${tmpIDCol}~GE~${pMinID}~FBV~${tmpIDCol}~LE~${pMaxID}`;
446
+
447
+ // Get local stats for this range
448
+ this._getLocalCount(pMinID, pMaxID,
449
+ (pLocalCountError, pLocalCount) =>
450
+ {
451
+ if (pLocalCountError)
452
+ {
453
+ this.fable.log.warn(`${this.EntitySchema.TableName}: bisect local count error for range ${pMinID}-${pMaxID}: ${pLocalCountError}`);
454
+ return fCallback();
455
+ }
456
+
457
+ // Get server count for this range
458
+ this._getServerCount(tmpRangeFilter,
459
+ (pServerCountError, pServerCount) =>
460
+ {
461
+ if (pServerCountError)
462
+ {
463
+ this.fable.log.warn(`${this.EntitySchema.TableName}: bisect server count error for range ${pMinID}-${pMaxID}: ${pServerCountError}`);
464
+ return fCallback();
465
+ }
466
+
467
+ // If counts match, check UpdateDate boundaries for this range
468
+ if (pLocalCount === pServerCount)
469
+ {
470
+ if (!this._hasUpdateDate)
471
+ {
472
+ // No UpdateDate column -- counts match, assume in sync
473
+ return fCallback();
474
+ }
475
+
476
+ // Compare max and min UpdateDate for this range
477
+ this._getLocalMaxUpdateDate(pMinID, pMaxID,
478
+ (pLocalMaxErr, pLocalMaxDate) =>
479
+ {
480
+ if (pLocalMaxErr || !pLocalMaxDate)
481
+ {
482
+ return fCallback();
483
+ }
484
+
485
+ // Get server max UpdateDate for this range (1 record, sorted desc)
486
+ const tmpMaxDateFilter = `${tmpRangeFilter}~FSF~UpdateDate~DESC~DESC`;
487
+ this._getServerRecords(tmpMaxDateFilter, 0, 1,
488
+ (pServerMaxErr, pServerMaxRecords) =>
489
+ {
490
+ if (pServerMaxErr || !pServerMaxRecords || pServerMaxRecords.length < 1)
491
+ {
492
+ return fCallback();
493
+ }
494
+
495
+ const tmpServerMaxDate = pServerMaxRecords[0].UpdateDate;
496
+ const tmpMaxDateDiff = Math.abs(this.fable.Dates.dayJS.utc(pLocalMaxDate).diff(this.fable.Dates.dayJS.utc(tmpServerMaxDate)));
497
+
498
+ if (tmpMaxDateDiff < 5)
499
+ {
500
+ // Max dates match and counts match -- this range is in sync
501
+ return fCallback();
502
+ }
503
+
504
+ // Dates differ even though counts match -- records have been modified.
505
+ // If range is small enough, pull all records; otherwise subdivide.
506
+ this.fable.log.info(`${this.EntitySchema.TableName}: date mismatch in range ${pMinID}-${pMaxID} (local max: ${pLocalMaxDate}, server max: ${tmpServerMaxDate})`);
507
+ if (tmpRangeSize <= this.BisectMinRangeSize)
508
+ {
509
+ return this._pullRangeFromServer(pMinID, pMaxID, fCallback);
510
+ }
511
+ return this._subdivideRange(pMinID, pMaxID, pDepth, fCallback);
512
+ });
513
+ });
514
+ return;
515
+ }
516
+
517
+ // Counts differ
518
+ this.fable.log.info(`${this.EntitySchema.TableName}: count mismatch in range ${pMinID}-${pMaxID} (local: ${pLocalCount}, server: ${pServerCount})`);
519
+
520
+ if (tmpRangeSize <= this.BisectMinRangeSize)
521
+ {
522
+ return this._pullRangeFromServer(pMinID, pMaxID, fCallback);
523
+ }
524
+
525
+ return this._subdivideRange(pMinID, pMaxID, pDepth, fCallback);
526
+ });
527
+ });
528
+ }
529
+
530
+ // Split an ID range in half and bisect each half.
531
+ _subdivideRange(pMinID, pMaxID, pDepth, fCallback)
532
+ {
533
+ const tmpMidID = Math.floor((pMinID + pMaxID) / 2);
534
+
535
+ this.fable.log.info(`${this.EntitySchema.TableName}: subdividing range ${pMinID}-${pMaxID} at ID ${tmpMidID} (depth ${pDepth})`);
536
+
537
+ // Bisect lower half, then upper half
538
+ this._bisectRange(pMinID, tmpMidID, pDepth + 1,
539
+ () =>
540
+ {
541
+ this._bisectRange(tmpMidID + 1, pMaxID, pDepth + 1, fCallback);
542
+ });
543
+ }
544
+
545
+ // Pull all records from the server in an ID range and upsert them locally.
546
+ _pullRangeFromServer(pMinID, pMaxID, fCallback)
547
+ {
548
+ const tmpIDCol = this.DefaultIdentifier;
549
+ const tmpFilter = `FBV~${tmpIDCol}~GE~${pMinID}~FBV~${tmpIDCol}~LE~${pMaxID}~FSF~${tmpIDCol}~ASC~ASC`;
550
+ const tmpEstimatedCount = pMaxID - pMinID + 1;
551
+
552
+ this.fable.log.info(`${this.EntitySchema.TableName}: pulling range ${pMinID}-${pMaxID} from server (~${tmpEstimatedCount} records)`);
553
+
554
+ this._pullServerRecords(tmpFilter, tmpEstimatedCount,
555
+ (pError, pSyncedCount) =>
556
+ {
557
+ if (pError)
558
+ {
559
+ this.fable.log.warn(`${this.EntitySchema.TableName}: error pulling range ${pMinID}-${pMaxID}: ${pError}`);
560
+ }
561
+ else
562
+ {
563
+ this.fable.log.info(`${this.EntitySchema.TableName}: synced ${pSyncedCount} records in range ${pMinID}-${pMaxID}`);
564
+ }
565
+ return fCallback();
566
+ });
567
+ }
568
+
569
+ // ---- Deleted records sync ----
570
+
147
571
  syncDeletedRecords(fCallback)
148
572
  {
149
573
  const tmpDeletedColumn = this.EntitySchema.Columns.find((c) => c.Column == 'Deleted');
@@ -155,7 +579,6 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
155
579
 
156
580
  this.fable.log.info(`Checking for deleted records on server for ${this.EntitySchema.TableName}...`);
157
581
 
158
- // Get the count of deleted records from the server.
159
582
  // The explicit FBV~Deleted~EQ~1 filter overrides foxhound's automatic Deleted=0 filter.
160
583
  this.fable.MeadowCloneRestClient.getJSON(`${this.EntitySchema.TableName}s/Count/FilteredTo/FBV~Deleted~EQ~1`,
161
584
  (pError, pResponse, pBody) =>
@@ -277,149 +700,7 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
277
700
  });
278
701
  }
279
702
 
280
- addSyncAnticipateEntry(tmpSyncState, tmpAnticipate)
281
- {
282
- tmpAnticipate.anticipate(
283
- (fNext) =>
284
- {
285
- const tmpURLPartial = `${this.EntitySchema.TableName}s/FilteredTo/FBV~${this.DefaultIdentifier}~GT~${tmpSyncState.LastRequestedID}~FSF~${this.DefaultIdentifier}~ASC~ASC/0/${this.PageSize}`;
286
- this.fable.MeadowCloneRestClient.getJSON(tmpURLPartial,
287
- (pDownloadError, pResponse, pBody) =>
288
- {
289
- if (pDownloadError)
290
- {
291
- this.fable.log.error(`Error getting URL Partial [${tmpURLPartial}]: ${pDownloadError}`, { Error: pDownloadError });
292
- return fNext();
293
- }
294
- if (pBody && Array.isArray(pBody) && pBody.length > 0)
295
- {
296
- for (let i = 0; i < pBody.length; i++)
297
- {
298
- const tmpRecord = pBody[i];
299
-
300
- tmpAnticipate.anticipate(
301
- (fNextEntityRecordSync) =>
302
- {
303
- const tmpQuery = this.Meadow.query;
304
-
305
- if (tmpRecord[this.DefaultIdentifier] > tmpSyncState.LastRequestedID)
306
- {
307
- tmpSyncState.LastRequestedID = tmpRecord[this.DefaultIdentifier];
308
- }
309
-
310
- if ((typeof(tmpRecord[this.DefaultIdentifier]) !== 'undefined') && (tmpRecord[this.DefaultIdentifier] > 0))
311
- {
312
- tmpQuery.addFilter(this.DefaultIdentifier, tmpRecord[this.DefaultIdentifier]);
313
- }
314
-
315
- if (!tmpSyncState.HasDeletedColumn)
316
- {
317
- tmpQuery.setDisableDeleteTracking(true);
318
- }
319
-
320
- this.Meadow.doRead(tmpQuery,
321
- (pReadError, pQuery, pRecord) =>
322
- {
323
- if (pReadError)
324
- {
325
- this.fable.log.error(`Error reading record ${this.EntitySchema.TableName}: ${pReadError}`, { Error: pReadError, PassedRecord: tmpRecord });
326
- return fNextEntityRecordSync();
327
- }
328
-
329
- if (pRecord)
330
- {
331
- const tmpAgeDifference = this.fable.Dates.dayJS(tmpRecord.UpdateDate).diff(this.fable.Dates.dayJS(pRecord.UpdateDate));
332
-
333
- if (Math.abs(tmpAgeDifference) < 5)
334
- {
335
- return fNextEntityRecordSync();
336
- }
337
-
338
- this.fable.log.info(`Syncing ${this.EntitySchema.TableName} record ${tmpRecord[this.DefaultIdentifier]} with age difference of ${tmpAgeDifference} ms.`);
339
- }
340
-
341
- const tmpRecordToCommit = this.marshalRecord(tmpRecord);
342
-
343
- const tmpSyncQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
344
-
345
- tmpSyncQuery.setDisableAutoIdentity(true);
346
- tmpSyncQuery.setDisableAutoDateStamp(true);
347
- tmpSyncQuery.setDisableAutoUserStamp(true);
348
- tmpSyncQuery.setDisableDeleteTracking(true);
349
-
350
- if (!pRecord)
351
- {
352
- // Record not found -- create
353
- tmpSyncQuery.AllowIdentityInsert = true;
354
-
355
- this.Meadow.doCreate(tmpSyncQuery,
356
- (pCreateError) =>
357
- {
358
- if (pCreateError)
359
- {
360
- let tmpErrorStr = (typeof(pCreateError) === 'string') ? pCreateError : JSON.stringify(pCreateError);
361
- if (tmpErrorStr.toLowerCase().indexOf('duplicate') > -1 || tmpErrorStr.toLowerCase().indexOf('unique') > -1)
362
- {
363
- // Duplicate key (likely GUID conflict) -- fall back to update
364
- this.log.warn(`Duplicate key on create for ${this.EntitySchema.TableName} ID ${tmpRecord[this.DefaultIdentifier]}; falling back to update.`);
365
- const tmpFallbackQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
366
- tmpFallbackQuery.setDisableAutoIdentity(true);
367
- tmpFallbackQuery.setDisableAutoDateStamp(true);
368
- tmpFallbackQuery.setDisableAutoUserStamp(true);
369
- tmpFallbackQuery.setDisableDeleteTracking(true);
370
- this.Meadow.doUpdate(tmpFallbackQuery,
371
- (pUpdateError) =>
372
- {
373
- if (pUpdateError)
374
- {
375
- this.log.error(`Fallback update also failed for ${this.EntitySchema.TableName} ID ${tmpRecord[this.DefaultIdentifier]}: ${pUpdateError}`);
376
- return fNextEntityRecordSync();
377
- }
378
- this.operation.incrementProgressTrackerStatus(`UpdateSync-${this.EntitySchema.TableName}`, 1);
379
- return fNextEntityRecordSync();
380
- });
381
- return;
382
- }
383
- this.log.error(`Error creating record ${this.EntitySchema.TableName}: ${pCreateError}`, pCreateError);
384
- return fNextEntityRecordSync();
385
- }
386
- this.operation.incrementProgressTrackerStatus(`UpdateSync-${this.EntitySchema.TableName}`, 1);
387
- return fNextEntityRecordSync();
388
- });
389
- }
390
- else
391
- {
392
- // Record found -- update
393
- this.Meadow.doUpdate(tmpSyncQuery,
394
- (pUpdateError) =>
395
- {
396
- if (pUpdateError)
397
- {
398
- this.log.error(`Error updating record ${this.EntitySchema.TableName}: ${pUpdateError}`, pUpdateError);
399
- return fNextEntityRecordSync();
400
- }
401
- this.operation.incrementProgressTrackerStatus(`UpdateSync-${this.EntitySchema.TableName}`, 1);
402
- return fNextEntityRecordSync();
403
- });
404
- }
405
- });
406
- });
407
- }
408
- tmpSyncState.RequestsPerformed++;
409
- if (tmpSyncState.RequestsPerformed < tmpSyncState.EstimatedRequestCount)
410
- {
411
- this.fable.log.info(`Syncing ${this.EntitySchema.TableName} request ${tmpSyncState.RequestsPerformed} of ${tmpSyncState.EstimatedRequestCount}...`);
412
- this.addSyncAnticipateEntry(tmpSyncState, tmpAnticipate);
413
- }
414
- return fNext();
415
- }
416
- else
417
- {
418
- return fNext();
419
- }
420
- });
421
- });
422
- }
703
+ // ---- Main sync entry point ----
423
704
 
424
705
  sync(fCallback)
425
706
  {
@@ -454,53 +735,44 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
454
735
  {
455
736
  this.operation.createTimeStamp('EntityOngoingSync');
456
737
 
457
- let tmpAnticipate = this.fable.newAnticipate();
458
-
459
738
  const tmpSyncState = (
460
739
  {
461
- Local: { MaxIDEntity: -1, RecordCount: 0, HasUpdateDate: false, LatestUpdateDate: false },
462
- Server: { MaxIDEntity: -1, RecordCount: 0, HasUpdateDate: false, LatestUpdateDate: false },
740
+ Local: { MaxIDEntity: -1, RecordCount: 0 },
741
+ Server: { MaxIDEntity: -1, RecordCount: 0 },
463
742
  });
464
743
 
465
- this.fable.Utility.waterfall(
466
- [
467
- (fStageComplete) =>
468
- {
469
- if (!this.EntitySchema || !this.EntitySchema.MeadowSchema || !Array.isArray(this.EntitySchema.MeadowSchema.Schema))
470
- {
471
- return fStageComplete('MeadowSyncEntityOngoing requires a valid MeadowEntitySchema.MeadowSchema.Schema.');
472
- }
744
+ // Detect schema capabilities
745
+ this._hasUpdateDate = false;
746
+ this._hasDeletedColumn = false;
473
747
 
474
- for (let i = 0; i < this.EntitySchema.MeadowSchema.Schema.length; i++)
475
- {
476
- const tmpColumn = this.EntitySchema.MeadowSchema.Schema[i];
477
- if (tmpColumn.Column == 'UpdateDate')
478
- {
479
- tmpSyncState.Local.HasUpdateDate = true;
480
- tmpSyncState.Server.HasUpdateDate = true;
481
- }
482
- if (tmpColumn.Type == 'Deleted' || tmpColumn.Column == 'Deleted')
483
- {
484
- tmpSyncState.HasDeletedColumn = true;
485
- }
486
- }
748
+ if (this.EntitySchema && this.EntitySchema.MeadowSchema && Array.isArray(this.EntitySchema.MeadowSchema.Schema))
749
+ {
750
+ for (let i = 0; i < this.EntitySchema.MeadowSchema.Schema.length; i++)
751
+ {
752
+ const tmpColumn = this.EntitySchema.MeadowSchema.Schema[i];
753
+ if (tmpColumn.Column == 'UpdateDate')
754
+ {
755
+ this._hasUpdateDate = true;
756
+ }
757
+ if (tmpColumn.Type == 'Deleted' || tmpColumn.Column == 'Deleted')
758
+ {
759
+ this._hasDeletedColumn = true;
760
+ }
761
+ }
762
+ }
487
763
 
488
- if (tmpSyncState.Local.HasUpdateDate)
489
- {
490
- this.log.info(`Entity ${this.EntitySchema.TableName} has UpdateDate column.`);
491
- }
764
+ this.fable.log.info(`Syncing with ONGOING STRATEGY entity ${this.EntitySchema.TableName} (UpdateDate: ${this._hasUpdateDate}, Deleted: ${this._hasDeletedColumn})...`);
492
765
 
493
- this.log.info(`Syncing with UPDATE STRATEGY entity ${this.EntitySchema.TableName}...`);
494
- return fStageComplete();
495
- },
766
+ this.fable.Utility.waterfall(
767
+ [
768
+ // ---- Stage 1: Gather local stats ----
496
769
  (fStageComplete) =>
497
770
  {
498
- // Get the Max ID from local database
771
+ // Local max ID
499
772
  const tmpQuery = this.Meadow.query;
500
773
  tmpQuery.setSort({ Column: this.DefaultIdentifier, Direction: 'Descending' });
501
774
  tmpQuery.setCap(1);
502
- // Disable delete tracking if the table has no Deleted column
503
- if (!tmpSyncState.HasDeletedColumn)
775
+ if (!this._hasDeletedColumn)
504
776
  {
505
777
  tmpQuery.setDisableDeleteTracking(true);
506
778
  }
@@ -509,31 +781,32 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
509
781
  {
510
782
  if (pReadError)
511
783
  {
512
- this.fable.log.error(`Error reading local max entity ID ${this.EntitySchema.TableName}: ${pReadError}`, { Error: pReadError });
784
+ this.fable.log.error(`Error reading local max entity ID ${this.EntitySchema.TableName}: ${pReadError}`);
513
785
  return fStageComplete(`Error reading local max entity ID ${this.EntitySchema.TableName}: ${pReadError}`);
514
786
  }
515
- if (!pRecord)
787
+ if (pRecord)
516
788
  {
517
- this.fable.log.warn(`No records found in local ${this.EntitySchema.TableName}.`);
518
- return fStageComplete();
789
+ tmpSyncState.Local.MaxIDEntity = pRecord[this.DefaultIdentifier];
790
+ this.fable.log.info(`Found local max entity ID ${this.EntitySchema.TableName}: ${tmpSyncState.Local.MaxIDEntity}`);
791
+ }
792
+ else
793
+ {
794
+ this.fable.log.info(`No local records for ${this.EntitySchema.TableName}.`);
519
795
  }
520
- this.fable.log.info(`Found local max entity ID ${this.EntitySchema.TableName}: ${pRecord[this.DefaultIdentifier]}`);
521
- tmpSyncState.Local.MaxIDEntity = pRecord[this.DefaultIdentifier];
522
796
  return fStageComplete();
523
797
  });
524
798
  },
525
799
  (fStageComplete) =>
526
800
  {
527
- // Get the Max UpdateDate from local database — skip if table has no UpdateDate column
528
- if (!tmpSyncState.Local.HasUpdateDate)
801
+ // Local max UpdateDate
802
+ if (!this._hasUpdateDate)
529
803
  {
530
- this.fable.log.info(`No UpdateDate column for ${this.EntitySchema.TableName}; skipping UpdateDate check.`);
531
804
  return fStageComplete();
532
805
  }
533
806
  const tmpQuery = this.Meadow.query;
534
807
  tmpQuery.setSort({ Column: 'UpdateDate', Direction: 'Descending' });
535
808
  tmpQuery.setCap(1);
536
- if (!tmpSyncState.HasDeletedColumn)
809
+ if (!this._hasDeletedColumn)
537
810
  {
538
811
  tmpQuery.setDisableDeleteTracking(true);
539
812
  }
@@ -542,24 +815,22 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
542
815
  {
543
816
  if (pReadError)
544
817
  {
545
- this.fable.log.error(`Error reading local max UpdateDate ${this.EntitySchema.TableName}: ${pReadError}`, { Error: pReadError });
818
+ this.fable.log.error(`Error reading local max UpdateDate ${this.EntitySchema.TableName}: ${pReadError}`);
546
819
  return fStageComplete(`Error reading local max UpdateDate ${this.EntitySchema.TableName}: ${pReadError}`);
547
820
  }
548
- if (!pRecord)
821
+ if (pRecord && pRecord.UpdateDate)
549
822
  {
550
- this.fable.log.warn(`No records found in local checking UpdateDate ${this.EntitySchema.TableName}.`);
551
- return fStageComplete();
823
+ tmpSyncState.Local.MaxUpdateDate = pRecord.UpdateDate;
824
+ this.fable.log.info(`Found local max UpdateDate ${this.EntitySchema.TableName}: ${tmpSyncState.Local.MaxUpdateDate}`);
552
825
  }
553
- this.fable.log.info(`Found local max UpdateDate ${this.EntitySchema.TableName}: ${pRecord.UpdateDate}`);
554
- tmpSyncState.Local.MaxUpdateDate = pRecord.UpdateDate;
555
826
  return fStageComplete();
556
827
  });
557
828
  },
558
829
  (fStageComplete) =>
559
830
  {
560
- // Get the count from local database
831
+ // Local count
561
832
  const tmpQuery = this.Meadow.query;
562
- if (!tmpSyncState.HasDeletedColumn)
833
+ if (!this._hasDeletedColumn)
563
834
  {
564
835
  tmpQuery.setDisableDeleteTracking(true);
565
836
  }
@@ -568,16 +839,19 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
568
839
  {
569
840
  if (pCountError)
570
841
  {
571
- this.fable.log.error(`Error getting local count of ${this.EntitySchema.TableName}: ${pCountError}`, { Error: pCountError });
842
+ this.fable.log.error(`Error getting local count of ${this.EntitySchema.TableName}: ${pCountError}`);
572
843
  return fStageComplete(`Error getting local count of ${this.EntitySchema.TableName}: ${pCountError}`);
573
844
  }
574
845
  tmpSyncState.Local.RecordCount = pCount;
846
+ this.fable.log.info(`Local count ${this.EntitySchema.TableName}: ${tmpSyncState.Local.RecordCount}`);
575
847
  return fStageComplete();
576
848
  });
577
849
  },
850
+
851
+ // ---- Stage 2: Gather server stats ----
578
852
  (fStageComplete) =>
579
853
  {
580
- // Get the Max ID from server
854
+ // Server max ID
581
855
  this.fable.MeadowCloneRestClient.getJSON(`${this.EntitySchema.TableName}/Max/${this.DefaultIdentifier}`,
582
856
  (pError, pResponse, pBody) =>
583
857
  {
@@ -588,8 +862,8 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
588
862
  }
589
863
  if (pBody && pBody.hasOwnProperty(this.DefaultIdentifier))
590
864
  {
591
- this.fable.log.info(`Found server max entity ID ${this.EntitySchema.TableName}: ${pBody[this.DefaultIdentifier]}`);
592
865
  tmpSyncState.Server.MaxIDEntity = pBody[this.DefaultIdentifier];
866
+ this.fable.log.info(`Found server max entity ID ${this.EntitySchema.TableName}: ${tmpSyncState.Server.MaxIDEntity}`);
593
867
  }
594
868
  else
595
869
  {
@@ -600,85 +874,192 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
600
874
  },
601
875
  (fStageComplete) =>
602
876
  {
603
- // Get the Max UpdateDate from server
604
- this.fable.MeadowCloneRestClient.getJSON(`${this.EntitySchema.TableName}/Max/UpdateDate`,
605
- (pError, pResponse, pBody) =>
877
+ // Server count
878
+ this._getServerCount(null,
879
+ (pError, pCount) =>
606
880
  {
607
881
  if (pError)
608
882
  {
609
- this.fable.log.warn(`Could not get server max UpdateDate for ${this.EntitySchema.TableName} (${pError}); will sync by ID only.`);
883
+ this.fable.log.warn(`Could not get server count for ${this.EntitySchema.TableName} (${pError}); estimating from max ID.`);
884
+ tmpSyncState.Server.RecordCount = tmpSyncState.Server.MaxIDEntity > 0 ? tmpSyncState.Server.MaxIDEntity : 0;
610
885
  return fStageComplete();
611
886
  }
612
- if (pBody && pBody.hasOwnProperty(this.DefaultIdentifier))
613
- {
614
- this.fable.log.info(`Found server max UpdateDate ${this.EntitySchema.TableName}: ${pBody['UpdateDate']}`);
615
- tmpSyncState.Server.MaxUpdateDate = pBody.UpdateDate;
616
- }
617
- else
618
- {
619
- this.fable.log.warn(`No records found in server for max UpdateDate of ${this.EntitySchema.TableName}.`);
620
- }
887
+ tmpSyncState.Server.RecordCount = pCount;
888
+ this.fable.log.info(`Server count ${this.EntitySchema.TableName}: ${tmpSyncState.Server.RecordCount}`);
621
889
  return fStageComplete();
622
890
  });
623
891
  },
892
+
893
+ // ---- Stage 3: UpdateDate-based fast sync ----
894
+ // If we have UpdateDate, compare server record count up to our local
895
+ // max UpdateDate. If it matches local count, existing records are in
896
+ // sync and we only need to pull records newer than that date.
624
897
  (fStageComplete) =>
625
898
  {
626
- // Get the count from server
627
- this.fable.MeadowCloneRestClient.getJSON(`${this.EntitySchema.TableName}s/Count`,
628
- (pError, pResponse, pBody) =>
899
+ if (!this._hasUpdateDate || !tmpSyncState.Local.MaxUpdateDate)
900
+ {
901
+ this.fable.log.info(`${this.EntitySchema.TableName}: no UpdateDate available; skipping UpdateDate fast-sync.`);
902
+ tmpSyncState.UpdateDateSyncDone = false;
903
+ return fStageComplete();
904
+ }
905
+
906
+ const tmpDateStr = this._formatDateForFilter(tmpSyncState.Local.MaxUpdateDate);
907
+ const tmpIDCol = this.DefaultIdentifier;
908
+ const tmpBeforeFilter = `FBV~UpdateDate~LE~${tmpDateStr}`;
909
+
910
+ this.fable.log.info(`${this.EntitySchema.TableName}: checking server count with UpdateDate <= ${tmpDateStr}...`);
911
+
912
+ this._getServerCount(tmpBeforeFilter,
913
+ (pError, pServerCountBefore) =>
629
914
  {
630
915
  if (pError)
631
916
  {
632
- this.fable.log.warn(`Could not get server count for ${this.EntitySchema.TableName} (${pError}); estimating from max ID.`);
633
- tmpSyncState.Server.RecordCount = tmpSyncState.Server.MaxIDEntity > 0 ? tmpSyncState.Server.MaxIDEntity : 0;
917
+ this.fable.log.warn(`${this.EntitySchema.TableName}: could not get server count before UpdateDate (${pError}); falling back to bisection.`);
918
+ tmpSyncState.UpdateDateSyncDone = false;
634
919
  return fStageComplete();
635
920
  }
636
- if (pBody && pBody.hasOwnProperty('Count'))
921
+
922
+ this.fable.log.info(`${this.EntitySchema.TableName}: server has ${pServerCountBefore} records with UpdateDate <= ${tmpDateStr} (local has ${tmpSyncState.Local.RecordCount})`);
923
+
924
+ if (pServerCountBefore === tmpSyncState.Local.RecordCount)
637
925
  {
638
- this.fable.log.info(`Found server count for ${this.EntitySchema.TableName}: ${pBody.Count}`);
639
- tmpSyncState.Server.RecordCount = pBody.Count;
926
+ // Record counts match up to our max UpdateDate -- existing records are in sync.
927
+ this.fable.log.info(`${this.EntitySchema.TableName}: counts match up to local max UpdateDate; existing records appear in sync.`);
928
+ tmpSyncState.ExistingRecordsInSync = true;
640
929
  }
641
930
  else
642
931
  {
643
- this.fable.log.warn(`No records found in server based on count for ${this.EntitySchema.TableName}.`);
932
+ this.fable.log.info(`${this.EntitySchema.TableName}: count mismatch before max UpdateDate (local: ${tmpSyncState.Local.RecordCount}, server: ${pServerCountBefore}); will bisect existing records.`);
933
+ tmpSyncState.ExistingRecordsInSync = false;
644
934
  }
645
- return fStageComplete();
935
+
936
+ // Now pull records with UpdateDate > local max UpdateDate (new + modified on server)
937
+ const tmpAfterFilter = `FBV~UpdateDate~GT~${tmpDateStr}~FSF~${tmpIDCol}~ASC~ASC`;
938
+
939
+ this._getServerCount(`FBV~UpdateDate~GT~${tmpDateStr}`,
940
+ (pAfterError, pServerCountAfter) =>
941
+ {
942
+ if (pAfterError)
943
+ {
944
+ this.fable.log.warn(`${this.EntitySchema.TableName}: could not get server count after UpdateDate (${pAfterError}).`);
945
+ tmpSyncState.UpdateDateSyncDone = false;
946
+ return fStageComplete();
947
+ }
948
+
949
+ this.fable.log.info(`${this.EntitySchema.TableName}: ${pServerCountAfter} records on server with UpdateDate > ${tmpDateStr}; pulling...`);
950
+
951
+ if (pServerCountAfter < 1)
952
+ {
953
+ tmpSyncState.UpdateDateSyncDone = true;
954
+ return fStageComplete();
955
+ }
956
+
957
+ this._pullServerRecords(tmpAfterFilter, pServerCountAfter,
958
+ (pPullError, pSyncedCount) =>
959
+ {
960
+ if (pPullError)
961
+ {
962
+ this.fable.log.warn(`${this.EntitySchema.TableName}: error pulling new records: ${pPullError}`);
963
+ }
964
+ else
965
+ {
966
+ this.fable.log.info(`${this.EntitySchema.TableName}: pulled ${pSyncedCount} new/modified records via UpdateDate.`);
967
+ }
968
+ tmpSyncState.UpdateDateSyncDone = true;
969
+ return fStageComplete();
970
+ });
971
+ });
646
972
  });
647
973
  },
974
+
975
+ // ---- Stage 4: Bisect existing records if counts did not match ----
648
976
  (fStageComplete) =>
649
977
  {
650
- let tmpTotalRecords = (this.MaxRecordsPerEntity > 0)
651
- ? Math.min(tmpSyncState.Server.RecordCount, this.MaxRecordsPerEntity)
652
- : tmpSyncState.Server.RecordCount;
653
- tmpSyncState.EstimatedRequestCount = Math.ceil(tmpTotalRecords / this.PageSize);
654
- tmpSyncState.RequestsPerformed = 0;
655
- tmpSyncState.LastRequestedID = 0;
978
+ // If UpdateDate sync found existing records in sync, or if we have
979
+ // no local data yet, skip bisection.
980
+ if (tmpSyncState.ExistingRecordsInSync)
981
+ {
982
+ this.fable.log.info(`${this.EntitySchema.TableName}: existing records in sync; skipping bisection.`);
983
+ return fStageComplete();
984
+ }
985
+
986
+ // If we have no local records, there is nothing to bisect
987
+ if (tmpSyncState.Local.MaxIDEntity < 1)
988
+ {
989
+ this.fable.log.info(`${this.EntitySchema.TableName}: no local records; skipping bisection.`);
990
+ return fStageComplete();
991
+ }
992
+
993
+ // If the UpdateDate fast-sync already ran and pulled new records,
994
+ // refresh local count to see if we are now in sync
995
+ if (tmpSyncState.UpdateDateSyncDone)
996
+ {
997
+ return this._getLocalCount(0, 0,
998
+ (pError, pNewLocalCount) =>
999
+ {
1000
+ if (pError || pNewLocalCount === tmpSyncState.Server.RecordCount)
1001
+ {
1002
+ this.fable.log.info(`${this.EntitySchema.TableName}: counts now match after UpdateDate pull (${pNewLocalCount} local, ${tmpSyncState.Server.RecordCount} server); skipping bisection.`);
1003
+ return fStageComplete();
1004
+ }
656
1005
 
657
- this.operation.createProgressTracker(tmpSyncState.EstimatedRequestCount, `UpdateSync-${this.EntitySchema.TableName}`);
658
- this.operation.printProgressTrackerStatus(`UpdateSync-${this.EntitySchema.TableName}`);
1006
+ this.fable.log.info(`${this.EntitySchema.TableName}: counts still differ after UpdateDate pull (${pNewLocalCount} local, ${tmpSyncState.Server.RecordCount} server); bisecting existing records...`);
1007
+ return this._bisectRange(1, tmpSyncState.Local.MaxIDEntity, 0, fStageComplete);
1008
+ });
1009
+ }
659
1010
 
660
- return fStageComplete();
1011
+ // No UpdateDate available -- bisect the full ID range
1012
+ this.fable.log.info(`${this.EntitySchema.TableName}: bisecting full ID range 1-${tmpSyncState.Local.MaxIDEntity}...`);
1013
+ return this._bisectRange(1, tmpSyncState.Local.MaxIDEntity, 0, fStageComplete);
661
1014
  },
1015
+
1016
+ // ---- Stage 5: Pull any remaining new records by ID ----
1017
+ // If no UpdateDate sync ran (table lacks UpdateDate), pull records
1018
+ // with ID > local max ID.
662
1019
  (fStageComplete) =>
663
1020
  {
664
- if (tmpSyncState.EstimatedRequestCount < 1)
1021
+ if (tmpSyncState.UpdateDateSyncDone)
665
1022
  {
666
- this.fable.log.info(`No records to update sync for ${this.EntitySchema.TableName}.`);
1023
+ // UpdateDate sync already handled new records
667
1024
  return fStageComplete();
668
1025
  }
669
1026
 
670
- this.addSyncAnticipateEntry(tmpSyncState, tmpAnticipate);
1027
+ if (tmpSyncState.Server.MaxIDEntity <= tmpSyncState.Local.MaxIDEntity)
1028
+ {
1029
+ this.fable.log.info(`${this.EntitySchema.TableName}: no new records by ID (server max ${tmpSyncState.Server.MaxIDEntity} <= local max ${tmpSyncState.Local.MaxIDEntity}).`);
1030
+ return fStageComplete();
1031
+ }
1032
+
1033
+ const tmpIDCol = this.DefaultIdentifier;
1034
+ const tmpFilter = `FBV~${tmpIDCol}~GT~${tmpSyncState.Local.MaxIDEntity}~FSF~${tmpIDCol}~ASC~ASC`;
1035
+ const tmpEstimated = tmpSyncState.Server.MaxIDEntity - tmpSyncState.Local.MaxIDEntity;
1036
+
1037
+ this.fable.log.info(`${this.EntitySchema.TableName}: pulling new records with ID > ${tmpSyncState.Local.MaxIDEntity} (~${tmpEstimated} estimated)...`);
671
1038
 
672
- tmpAnticipate.wait(fStageComplete);
1039
+ this._pullServerRecords(tmpFilter, tmpEstimated,
1040
+ (pError, pSyncedCount) =>
1041
+ {
1042
+ if (pError)
1043
+ {
1044
+ this.fable.log.warn(`${this.EntitySchema.TableName}: error pulling new records by ID: ${pError}`);
1045
+ }
1046
+ else
1047
+ {
1048
+ this.fable.log.info(`${this.EntitySchema.TableName}: pulled ${pSyncedCount} new records by ID.`);
1049
+ }
1050
+ return fStageComplete();
1051
+ });
673
1052
  },
674
1053
  ],
675
1054
  (pError) =>
676
1055
  {
677
1056
  if (pError)
678
1057
  {
679
- this.fable.log.error(`Error performing Update sync ${this.EntitySchema.TableName}: ${pError}`, { Error: pError });
1058
+ this.fable.log.error(`Error performing ongoing sync ${this.EntitySchema.TableName}: ${pError}`, { Error: pError });
680
1059
  }
681
1060
 
1061
+ this.fable.log.info(`${this.EntitySchema.TableName}: ongoing sync complete.`);
1062
+
682
1063
  if (this.SyncDeletedRecords)
683
1064
  {
684
1065
  return this.syncDeletedRecords(() => { return fCallback(); });