meadow-integration 1.0.15 → 1.0.17

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.15",
3
+ "version": "1.0.17",
4
4
  "description": "Meadow Data Integration",
5
5
  "bin": {
6
6
  "mdwint": "source/cli/Meadow-Integration-CLI-Run.js"
@@ -16,7 +16,7 @@
16
16
  "author": "steven velozo <steven@velozo.com>",
17
17
  "license": "MIT",
18
18
  "devDependencies": {
19
- "quackage": "^1.0.63"
19
+ "quackage": "^1.0.64"
20
20
  },
21
21
  "mocha": {
22
22
  "diff": true,
@@ -41,6 +41,7 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
41
41
  this.PageSize = this.options.PageSize || 100;
42
42
  this.SyncDeletedRecords = this.options.SyncDeletedRecords || false;
43
43
  this.MaxRecordsPerEntity = this.options.MaxRecordsPerEntity || 0;
44
+ this.UseAdvancedIDPagination = this.options.UseAdvancedIDPagination || false;
44
45
 
45
46
  this.Meadow = false;
46
47
 
@@ -459,14 +460,21 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
459
460
  this.operation.createProgressTracker(tmpSyncState.EstimatedRecordCount, `FullSync-${this.EntitySchema.TableName}`);
460
461
  this.operation.printProgressTrackerStatus(`FullSync-${this.EntitySchema.TableName}`);
461
462
 
462
- // Generate paginated URL partials
463
- tmpSyncState.URLPartials = [];
464
- for (let i = 0; i < tmpRecordCap; i += this.PageSize)
463
+ if (this.UseAdvancedIDPagination)
465
464
  {
466
- tmpSyncState.URLPartials.push(`${this.EntitySchema.TableName}s/FilteredTo/FBV~${this.DefaultIdentifier}~GT~${tmpSyncState.Local.MaxIDEntity}~FSF~${this.DefaultIdentifier}~ASC~ASC/${i}/${this.PageSize}`);
465
+ this.fable.log.info(`${this.EntitySchema.TableName}: using advanced ID pagination (local: ${tmpSyncState.Local.RecordCount}/${tmpSyncState.Local.MaxIDEntity}, server: ${tmpSyncState.Server.RecordCount}/${tmpSyncState.Server.MaxIDEntity}, estimated new: ${tmpSyncState.EstimatedRecordCount}${this.MaxRecordsPerEntity > 0 ? `, capped at ${this.MaxRecordsPerEntity}` : ''})`);
467
466
  }
467
+ else
468
+ {
469
+ // Generate paginated URL partials
470
+ tmpSyncState.URLPartials = [];
471
+ for (let i = 0; i < tmpRecordCap; i += this.PageSize)
472
+ {
473
+ tmpSyncState.URLPartials.push(`${this.EntitySchema.TableName}s/FilteredTo/FBV~${this.DefaultIdentifier}~GT~${tmpSyncState.Local.MaxIDEntity}~FSF~${this.DefaultIdentifier}~ASC~ASC/${i}/${this.PageSize}`);
474
+ }
468
475
 
469
- this.fable.log.info(`${this.EntitySchema.TableName}: downloading ${tmpSyncState.URLPartials.length} pages (local: ${tmpSyncState.Local.RecordCount}/${tmpSyncState.Local.MaxIDEntity}, server: ${tmpSyncState.Server.RecordCount}/${tmpSyncState.Server.MaxIDEntity}, estimated new: ${tmpSyncState.EstimatedRecordCount}${this.MaxRecordsPerEntity > 0 ? `, capped at ${this.MaxRecordsPerEntity}` : ''})`);
476
+ this.fable.log.info(`${this.EntitySchema.TableName}: downloading ${tmpSyncState.URLPartials.length} pages (local: ${tmpSyncState.Local.RecordCount}/${tmpSyncState.Local.MaxIDEntity}, server: ${tmpSyncState.Server.RecordCount}/${tmpSyncState.Server.MaxIDEntity}, estimated new: ${tmpSyncState.EstimatedRecordCount}${this.MaxRecordsPerEntity > 0 ? `, capped at ${this.MaxRecordsPerEntity}` : ''})`);
477
+ }
470
478
 
471
479
  return fStageComplete();
472
480
  },
@@ -477,149 +485,216 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
477
485
  let tmpRecordsSkipped = 0;
478
486
  let tmpRecordsErrored = 0;
479
487
 
480
- this.fable.Utility.eachLimit(tmpSyncState.URLPartials, 1,
481
- (pURLPartial, fDownloadComplete) =>
482
- {
483
- tmpPageIndex++;
488
+ // Shared record-processing function used by both pagination modes
489
+ const fProcessPageRecords = (pBody, fPageProcessComplete) =>
490
+ {
491
+ this.fable.Utility.eachLimit(pBody, 5,
492
+ (pEntityRecord, fEntitySyncComplete) =>
493
+ {
494
+ const tmpRecord = pEntityRecord;
495
+ const tmpQuery = this.Meadow.query;
484
496
 
485
- this.fable.MeadowCloneRestClient.getJSON(pURLPartial,
486
- (pDownloadError, pResponse, pBody) =>
497
+ if ((typeof(tmpRecord[this.DefaultIdentifier]) !== 'undefined') && (tmpRecord[this.DefaultIdentifier] > 0))
487
498
  {
488
- if (pDownloadError)
489
- {
490
- this.fable.log.error(`${this.EntitySchema.TableName}: page ${tmpPageIndex} download error: ${pDownloadError}`);
491
- return fDownloadComplete();
492
- }
493
- if (pBody && Array.isArray(pBody) && pBody.length > 0)
494
- {
495
- this.fable.Utility.eachLimit(pBody, 5,
496
- (pEntityRecord, fEntitySyncComplete) =>
497
- {
498
- const tmpRecord = pEntityRecord;
499
- const tmpQuery = this.Meadow.query;
500
-
501
- if ((typeof(tmpRecord[this.DefaultIdentifier]) !== 'undefined') && (tmpRecord[this.DefaultIdentifier] > 0))
502
- {
503
- tmpQuery.addFilter(this.DefaultIdentifier, tmpRecord[this.DefaultIdentifier]);
504
- }
499
+ tmpQuery.addFilter(this.DefaultIdentifier, tmpRecord[this.DefaultIdentifier]);
500
+ }
505
501
 
506
- if (!tmpSyncState.HasDeletedColumn)
507
- {
508
- tmpQuery.setDisableDeleteTracking(true);
509
- }
502
+ if (!tmpSyncState.HasDeletedColumn)
503
+ {
504
+ tmpQuery.setDisableDeleteTracking(true);
505
+ }
510
506
 
511
- this.Meadow.doRead(tmpQuery,
512
- (pReadError, pQuery, pRecord) =>
513
- {
514
- if (pReadError)
515
- {
516
- tmpRecordsErrored++;
517
- return fEntitySyncComplete();
518
- }
519
- if (!pRecord)
520
- {
521
- // Record not found -- create it
522
- const tmpRecordToCommit = this.marshalRecord(tmpRecord);
507
+ this.Meadow.doRead(tmpQuery,
508
+ (pReadError, pQuery, pRecord) =>
509
+ {
510
+ if (pReadError)
511
+ {
512
+ tmpRecordsErrored++;
513
+ return fEntitySyncComplete();
514
+ }
515
+ if (!pRecord)
516
+ {
517
+ // Record not found -- create it
518
+ const tmpRecordToCommit = this.marshalRecord(tmpRecord);
523
519
 
524
- const tmpCreateQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
520
+ const tmpCreateQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
525
521
 
526
- tmpCreateQuery.setDisableAutoIdentity(true);
527
- tmpCreateQuery.setDisableAutoDateStamp(true);
528
- tmpCreateQuery.setDisableAutoUserStamp(true);
529
- tmpCreateQuery.setDisableDeleteTracking(true);
522
+ tmpCreateQuery.setDisableAutoIdentity(true);
523
+ tmpCreateQuery.setDisableAutoDateStamp(true);
524
+ tmpCreateQuery.setDisableAutoUserStamp(true);
525
+ tmpCreateQuery.setDisableDeleteTracking(true);
530
526
 
531
- tmpCreateQuery.AllowIdentityInsert = true;
527
+ tmpCreateQuery.AllowIdentityInsert = true;
532
528
 
533
- this.Meadow.doCreate(tmpCreateQuery,
534
- (pCreateError) =>
529
+ this.Meadow.doCreate(tmpCreateQuery,
530
+ (pCreateError) =>
531
+ {
532
+ if (pCreateError)
533
+ {
534
+ let tmpErrorStr = (typeof(pCreateError) === 'string') ? pCreateError : JSON.stringify(pCreateError);
535
+ if (tmpErrorStr.toLowerCase().indexOf('duplicate') > -1 || tmpErrorStr.toLowerCase().indexOf('unique') > -1)
536
+ {
537
+ // Duplicate key (likely GUID conflict) -- fall back to update
538
+ this.log.warn(`${this.EntitySchema.TableName}: duplicate key on create for ID ${tmpRecord[this.DefaultIdentifier]}; falling back to update.`);
539
+ const tmpUpdateQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
540
+ tmpUpdateQuery.setDisableAutoIdentity(true);
541
+ tmpUpdateQuery.setDisableAutoDateStamp(true);
542
+ tmpUpdateQuery.setDisableAutoUserStamp(true);
543
+ tmpUpdateQuery.setDisableDeleteTracking(true);
544
+ this.Meadow.doUpdate(tmpUpdateQuery,
545
+ (pUpdateError) =>
535
546
  {
536
- if (pCreateError)
547
+ if (pUpdateError)
537
548
  {
538
- let tmpErrorStr = (typeof(pCreateError) === 'string') ? pCreateError : JSON.stringify(pCreateError);
539
- if (tmpErrorStr.toLowerCase().indexOf('duplicate') > -1 || tmpErrorStr.toLowerCase().indexOf('unique') > -1)
540
- {
541
- // Duplicate key (likely GUID conflict) -- fall back to update
542
- this.log.warn(`${this.EntitySchema.TableName}: duplicate key on create for ID ${tmpRecord[this.DefaultIdentifier]}; falling back to update.`);
543
- const tmpUpdateQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
544
- tmpUpdateQuery.setDisableAutoIdentity(true);
545
- tmpUpdateQuery.setDisableAutoDateStamp(true);
546
- tmpUpdateQuery.setDisableAutoUserStamp(true);
547
- tmpUpdateQuery.setDisableDeleteTracking(true);
548
- this.Meadow.doUpdate(tmpUpdateQuery,
549
- (pUpdateError) =>
550
- {
551
- if (pUpdateError)
552
- {
553
- tmpRecordsErrored++;
554
- this.log.error(`${this.EntitySchema.TableName}: fallback update also failed for ID ${tmpRecord[this.DefaultIdentifier]}: ${pUpdateError}`);
555
- return fEntitySyncComplete();
556
- }
557
- tmpRecordsCreated++;
558
- this.operation.incrementProgressTrackerStatus(`FullSync-${this.EntitySchema.TableName}`, 1);
559
- return fEntitySyncComplete();
560
- });
561
- return;
562
- }
563
549
  tmpRecordsErrored++;
564
- this.log.error(`${this.EntitySchema.TableName}: doCreate error for ID ${tmpRecord[this.DefaultIdentifier]}: ${pCreateError}`);
550
+ this.log.error(`${this.EntitySchema.TableName}: fallback update also failed for ID ${tmpRecord[this.DefaultIdentifier]}: ${pUpdateError}`);
565
551
  return fEntitySyncComplete();
566
552
  }
567
553
  tmpRecordsCreated++;
568
554
  this.operation.incrementProgressTrackerStatus(`FullSync-${this.EntitySchema.TableName}`, 1);
569
555
  return fEntitySyncComplete();
570
556
  });
557
+ return;
571
558
  }
572
- else
573
- {
574
- tmpRecordsSkipped++;
575
- return fEntitySyncComplete();
576
- }
577
- });
578
- },
579
- (pEntitySyncError) =>
580
- {
581
- this.operation.printProgressTrackerStatus(`FullSync-${this.EntitySchema.TableName}`);
582
- if (pEntitySyncError)
583
- {
584
- this.log.error(`Problem or early completion syncing entity ${this.EntitySchema.TableName}: ${pEntitySyncError}`, pEntitySyncError);
585
- }
586
- return fDownloadComplete();
587
- });
588
- }
589
- else
590
- {
591
- if (Array.isArray(pBody) && pBody.length == 0)
559
+ tmpRecordsErrored++;
560
+ this.log.error(`${this.EntitySchema.TableName}: doCreate error for ID ${tmpRecord[this.DefaultIdentifier]}: ${pCreateError}`);
561
+ return fEntitySyncComplete();
562
+ }
563
+ tmpRecordsCreated++;
564
+ this.operation.incrementProgressTrackerStatus(`FullSync-${this.EntitySchema.TableName}`, 1);
565
+ return fEntitySyncComplete();
566
+ });
567
+ }
568
+ else
592
569
  {
593
- return fDownloadComplete(new Error('Records depleted!'));
570
+ tmpRecordsSkipped++;
571
+ return fEntitySyncComplete();
594
572
  }
595
- return fDownloadComplete();
596
- }
597
- });
598
- },
599
- (pDownloadError) =>
573
+ });
574
+ },
575
+ (pEntitySyncError) =>
576
+ {
577
+ this.operation.printProgressTrackerStatus(`FullSync-${this.EntitySchema.TableName}`);
578
+ if (pEntitySyncError)
579
+ {
580
+ this.log.error(`Problem or early completion syncing entity ${this.EntitySchema.TableName}: ${pEntitySyncError}`, pEntitySyncError);
581
+ }
582
+ return fPageProcessComplete(pEntitySyncError);
583
+ });
584
+ };
585
+
586
+ const fSyncComplete = (pDownloadError) =>
587
+ {
588
+ this.fable.log.info(`${this.EntitySchema.TableName}: sync complete — created: ${tmpRecordsCreated}, skipped: ${tmpRecordsSkipped}, errors: ${tmpRecordsErrored}`);
589
+ if (pDownloadError)
590
+ {
591
+ this.fable.log.error(`Error returned URL Partial .. this may not be an error: ${pDownloadError}`);
592
+ }
593
+
594
+ // Store sync results on the entity so callers can inspect the breakdown
595
+ this.syncResults = (
596
+ {
597
+ Created: tmpRecordsCreated,
598
+ Skipped: tmpRecordsSkipped,
599
+ Errors: tmpRecordsErrored,
600
+ Deleted: 0,
601
+ ServerRecordCount: tmpSyncState.Server.RecordCount,
602
+ LocalRecordCount: tmpSyncState.Local.RecordCount,
603
+ ServerMaxID: tmpSyncState.Server.MaxIDEntity,
604
+ LocalMaxID: tmpSyncState.Local.MaxIDEntity,
605
+ EstimatedNew: tmpSyncState.EstimatedRecordCount
606
+ });
607
+
608
+ fStageComplete();
609
+ };
610
+
611
+ if (this.UseAdvancedIDPagination)
612
+ {
613
+ // Advanced ID pagination: use keyset pagination (WHERE ID > lastMaxID)
614
+ // instead of OFFSET to avoid progressive table scan slowdown on large datasets.
615
+ let tmpLastMaxID = tmpSyncState.Local.MaxIDEntity;
616
+ let tmpTotalFetched = 0;
617
+
618
+ const fFetchPage = () =>
600
619
  {
601
- this.fable.log.info(`${this.EntitySchema.TableName}: sync complete — created: ${tmpRecordsCreated}, skipped: ${tmpRecordsSkipped}, errors: ${tmpRecordsErrored}`);
602
- if (pDownloadError)
620
+ if (tmpTotalFetched >= tmpRecordCap)
603
621
  {
604
- this.fable.log.error(`Error returned URL Partial .. this may not be an error: ${pDownloadError}`);
622
+ return fSyncComplete();
605
623
  }
606
624
 
607
- // Store sync results on the entity so callers can inspect the breakdown
608
- this.syncResults = (
625
+ tmpPageIndex++;
626
+ let tmpURL = `${this.EntitySchema.TableName}s/FilteredTo/FBV~${this.DefaultIdentifier}~GT~${tmpLastMaxID}~FSF~${this.DefaultIdentifier}~ASC~ASC/0/${this.PageSize}`;
627
+
628
+ this.fable.MeadowCloneRestClient.getJSON(tmpURL,
629
+ (pDownloadError, pResponse, pBody) =>
609
630
  {
610
- Created: tmpRecordsCreated,
611
- Skipped: tmpRecordsSkipped,
612
- Errors: tmpRecordsErrored,
613
- Deleted: 0,
614
- ServerRecordCount: tmpSyncState.Server.RecordCount,
615
- LocalRecordCount: tmpSyncState.Local.RecordCount,
616
- ServerMaxID: tmpSyncState.Server.MaxIDEntity,
617
- LocalMaxID: tmpSyncState.Local.MaxIDEntity,
618
- EstimatedNew: tmpSyncState.EstimatedRecordCount
631
+ if (pDownloadError)
632
+ {
633
+ this.fable.log.error(`${this.EntitySchema.TableName}: page ${tmpPageIndex} download error: ${pDownloadError}`);
634
+ return fSyncComplete(pDownloadError);
635
+ }
636
+ if (pBody && Array.isArray(pBody) && pBody.length > 0)
637
+ {
638
+ // Track the max ID from this page for the next page filter
639
+ for (let r = 0; r < pBody.length; r++)
640
+ {
641
+ if (pBody[r][this.DefaultIdentifier] > tmpLastMaxID)
642
+ {
643
+ tmpLastMaxID = pBody[r][this.DefaultIdentifier];
644
+ }
645
+ }
646
+ tmpTotalFetched += pBody.length;
647
+
648
+ fProcessPageRecords(pBody, (pProcessError) =>
649
+ {
650
+ if (pProcessError)
651
+ {
652
+ return fSyncComplete(pProcessError);
653
+ }
654
+ return setImmediate(fFetchPage);
655
+ });
656
+ }
657
+ else
658
+ {
659
+ // No more records
660
+ return fSyncComplete();
661
+ }
619
662
  });
663
+ };
664
+ fFetchPage();
665
+ }
666
+ else
667
+ {
668
+ // Standard offset-based pagination
669
+ this.fable.Utility.eachLimit(tmpSyncState.URLPartials, 1,
670
+ (pURLPartial, fDownloadComplete) =>
671
+ {
672
+ tmpPageIndex++;
620
673
 
621
- fStageComplete();
622
- });
674
+ this.fable.MeadowCloneRestClient.getJSON(pURLPartial,
675
+ (pDownloadError, pResponse, pBody) =>
676
+ {
677
+ if (pDownloadError)
678
+ {
679
+ this.fable.log.error(`${this.EntitySchema.TableName}: page ${tmpPageIndex} download error: ${pDownloadError}`);
680
+ return fDownloadComplete();
681
+ }
682
+ if (pBody && Array.isArray(pBody) && pBody.length > 0)
683
+ {
684
+ fProcessPageRecords(pBody, fDownloadComplete);
685
+ }
686
+ else
687
+ {
688
+ if (Array.isArray(pBody) && pBody.length == 0)
689
+ {
690
+ return fDownloadComplete(new Error('Records depleted!'));
691
+ }
692
+ return fDownloadComplete();
693
+ }
694
+ });
695
+ },
696
+ fSyncComplete);
697
+ }
623
698
  },
624
699
  ],
625
700
  (pError) =>
@@ -372,6 +372,10 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
372
372
  {
373
373
  this.log.error(`Fallback update also failed for ${this.EntitySchema.TableName} ID ${pServerRecord[this.DefaultIdentifier]}: ${pUpdateError}`);
374
374
  }
375
+ else
376
+ {
377
+ this._recordsUpdated++;
378
+ }
375
379
  return fCallback();
376
380
  });
377
381
  return;
@@ -379,6 +383,10 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
379
383
  this.log.error(`Error creating record ${this.EntitySchema.TableName}: ${pCreateError}`, pCreateError);
380
384
  return fCallback();
381
385
  }
386
+ else
387
+ {
388
+ this._recordsCreated++;
389
+ }
382
390
  return fCallback();
383
391
  });
384
392
  }
@@ -392,6 +400,10 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
392
400
  {
393
401
  this.log.error(`Error updating record ${this.EntitySchema.TableName}: ${pUpdateError}`, pUpdateError);
394
402
  }
403
+ else
404
+ {
405
+ this._recordsUpdated++;
406
+ }
395
407
  return fCallback();
396
408
  });
397
409
  }
@@ -407,12 +419,22 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
407
419
  return fCallback(null, 0);
408
420
  }
409
421
 
422
+ // Check the global cap before starting -- if we have already synced
423
+ // MaxRecordsPerEntity records across all stages, stop immediately.
424
+ if (this.MaxRecordsPerEntity > 0 && this._totalSyncedThisSync >= this.MaxRecordsPerEntity)
425
+ {
426
+ this.fable.log.info(`${this.EntitySchema.TableName}: global record cap reached (${this._totalSyncedThisSync}/${this.MaxRecordsPerEntity}); skipping pull.`);
427
+ return fCallback(null, 0);
428
+ }
429
+
410
430
  let tmpSyncedCount = 0;
411
431
  let tmpOffset = 0;
412
432
  let tmpDone = false;
413
433
 
434
+ // Apply per-call cap based on estimated count, then further limit by
435
+ // how many records remain before hitting the global MaxRecordsPerEntity.
414
436
  let tmpRecordCap = (this.MaxRecordsPerEntity > 0)
415
- ? Math.min(pEstimatedCount, this.MaxRecordsPerEntity)
437
+ ? Math.min(pEstimatedCount, this.MaxRecordsPerEntity - this._totalSyncedThisSync)
416
438
  : pEstimatedCount;
417
439
 
418
440
  const fFetchPage = () =>
@@ -443,6 +465,7 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
443
465
  () =>
444
466
  {
445
467
  tmpSyncedCount++;
468
+ this._totalSyncedThisSync++;
446
469
  // Use setImmediate to yield the event loop and prevent
447
470
  // stack overflow when SQLite callbacks complete synchronously
448
471
  return setImmediate(fRecordDone);
@@ -485,6 +508,12 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
485
508
  // the range from the server to bring local in sync.
486
509
  _bisectRange(pMinID, pMaxID, pDepth, fCallback)
487
510
  {
511
+ // If the global record cap has been reached, stop bisecting
512
+ if (this.MaxRecordsPerEntity > 0 && this._totalSyncedThisSync >= this.MaxRecordsPerEntity)
513
+ {
514
+ return fCallback();
515
+ }
516
+
488
517
  const tmpRangeSize = pMaxID - pMinID + 1;
489
518
  const tmpIDCol = this.DefaultIdentifier;
490
519
  const tmpRangeFilter = `FBV~${tmpIDCol}~GE~${pMinID}~FBV~${tmpIDCol}~LE~${pMaxID}`;
@@ -768,7 +797,7 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
768
797
  {
769
798
  if (pReadError)
770
799
  {
771
- let tmpErrorStr = (typeof(pReadError) === 'string') ? pReadError : JSON.stringify(pReadError);
800
+ let tmpErrorStr = (typeof(pReadError) === 'string') ? pReadError : String(pReadError);
772
801
  if (tmpErrorStr.indexOf('Invalid column') > -1 || tmpErrorStr.indexOf('Invalid object') > -1 || tmpErrorStr.indexOf('no such column') > -1 || tmpErrorStr.indexOf('no such table') > -1)
773
802
  {
774
803
  this.log.warn(`${this.EntitySchema.TableName}: local table schema mismatch (${pReadError}); skipping sync.`);
@@ -783,6 +812,13 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
783
812
  {
784
813
  this.operation.createTimeStamp('EntityOngoingSync');
785
814
 
815
+ // Track total records synced across all stages to enforce MaxRecordsPerEntity globally
816
+ this._totalSyncedThisSync = 0;
817
+
818
+ // Track per-record create vs update counts for the sync report
819
+ this._recordsCreated = 0;
820
+ this._recordsUpdated = 0;
821
+
786
822
  const tmpSyncState = (
787
823
  {
788
824
  Local: { MaxIDEntity: -1, RecordCount: 0 },
@@ -941,7 +977,10 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
941
977
  // Create a progress tracker so callers (e.g. data-cloner UI) can see Total/Synced
942
978
  (fStageComplete) =>
943
979
  {
944
- this.operation.createProgressTracker(tmpSyncState.Server.RecordCount, `FullSync-${this.EntitySchema.TableName}`);
980
+ let tmpTrackerTotal = (this.MaxRecordsPerEntity > 0)
981
+ ? Math.min(tmpSyncState.Server.RecordCount, this.MaxRecordsPerEntity)
982
+ : tmpSyncState.Server.RecordCount;
983
+ this.operation.createProgressTracker(tmpTrackerTotal, `FullSync-${this.EntitySchema.TableName}`);
945
984
  return fStageComplete();
946
985
  },
947
986
 
@@ -1126,6 +1165,15 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
1126
1165
 
1127
1166
  this.fable.log.info(`${this.EntitySchema.TableName}: ongoing sync complete.`);
1128
1167
 
1168
+ // Store sync results so callers can inspect the breakdown
1169
+ this.syncResults = {
1170
+ Created: this._recordsCreated,
1171
+ Updated: this._recordsUpdated,
1172
+ Deleted: 0,
1173
+ ServerRecordCount: tmpSyncState.Server.RecordCount,
1174
+ LocalRecordCount: tmpSyncState.Local.RecordCount
1175
+ };
1176
+
1129
1177
  if (this.SyncDeletedRecords)
1130
1178
  {
1131
1179
  return this.syncDeletedRecords(() => { return fCallback(); });
@@ -65,6 +65,18 @@ class MeadowSync extends libFableServiceProviderBase
65
65
  this.MaxRecordsPerEntity = parseInt(this.options.MaxRecordsPerEntity, 10) || 0;
66
66
  }
67
67
 
68
+ // When true, use ID-based keyset pagination instead of OFFSET pagination.
69
+ // This avoids table scans on large datasets by filtering WHERE ID > lastMaxID.
70
+ this.UseAdvancedIDPagination = false;
71
+ if (this.fable.ProgramConfiguration.hasOwnProperty('UseAdvancedIDPagination'))
72
+ {
73
+ this.UseAdvancedIDPagination = !!this.fable.ProgramConfiguration.UseAdvancedIDPagination;
74
+ }
75
+ else if (this.options.hasOwnProperty('UseAdvancedIDPagination'))
76
+ {
77
+ this.UseAdvancedIDPagination = !!this.options.UseAdvancedIDPagination;
78
+ }
79
+
68
80
  // Tolerance window in milliseconds for cross-database timestamp precision differences.
69
81
  // Passed through to Ongoing sync entities for bisection date comparison.
70
82
  this.DateTimePrecisionMS = 1000;
@@ -114,6 +126,7 @@ class MeadowSync extends libFableServiceProviderBase
114
126
  SyncDeletedRecords: this.SyncDeletedRecords,
115
127
  MaxRecordsPerEntity: this.MaxRecordsPerEntity,
116
128
  DateTimePrecisionMS: this.DateTimePrecisionMS,
129
+ UseAdvancedIDPagination: this.UseAdvancedIDPagination,
117
130
  };
118
131
 
119
132
  let tmpSyncEntity;