retold-data-service 2.0.18 → 2.0.20
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 +2 -2
- package/source/services/Retold-Data-Service-MeadowEndpoints.js +12 -2
- package/source/services/data-cloner/DataCloner-Command-Schema.js +4 -3
- package/source/services/data-cloner/DataCloner-Command-Sync.js +31 -4
- package/source/services/data-cloner/DataCloner-ProviderRegistry.js +1 -1
- package/source/services/data-cloner/Retold-Data-Service-DataCloner.js +47 -7
- package/source/services/data-cloner/pict-app/Pict-Application-DataCloner.js +2 -1
- package/source/services/data-cloner/pict-app/providers/Pict-Provider-DataCloner.js +102 -2
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Export.js +2 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Layout.js +28 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Sync.js +7 -0
- package/source/services/data-cloner/web/data-cloner.js +117 -3
- package/source/services/data-cloner/web/data-cloner.js.map +1 -1
- package/source/services/data-cloner/web/data-cloner.min.js +1 -1
- package/source/services/data-cloner/web/data-cloner.min.js.map +1 -1
- package/test/run-integration-tests.js +52 -12
- package/test/run-integration-tests.sh +221 -0
- package/test/integration-report.json +0 -311
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "retold-data-service",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.20",
|
|
4
4
|
"description": "Serve up a whole model!",
|
|
5
5
|
"main": "source/Retold-Data-Service.js",
|
|
6
6
|
"bin": {
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"meadow": "^2.0.33",
|
|
73
73
|
"meadow-connection-mysql": "^1.0.14",
|
|
74
74
|
"meadow-endpoints": "^4.0.14",
|
|
75
|
-
"meadow-integration": "^1.0.
|
|
75
|
+
"meadow-integration": "^1.0.16",
|
|
76
76
|
"meadow-migrationmanager": "^0.0.4",
|
|
77
77
|
"orator": "^6.0.4",
|
|
78
78
|
"orator-http-proxy": "^1.0.5",
|
|
@@ -137,6 +137,8 @@ class RetoldDataServiceMeadowEndpoints extends libFableServiceProviderBase
|
|
|
137
137
|
{
|
|
138
138
|
let tmpDALEntityName = tmpEntityList[i];
|
|
139
139
|
|
|
140
|
+
let tmpRoutesAlreadyConnected = this._MeadowEndpoints.hasOwnProperty(tmpDALEntityName);
|
|
141
|
+
|
|
140
142
|
if (this._DAL.hasOwnProperty(tmpDALEntityName))
|
|
141
143
|
{
|
|
142
144
|
this.fable.log.warn(`Entity [${tmpDALEntityName}] already exists in the DAL (from another model); overwriting.`);
|
|
@@ -152,8 +154,16 @@ class RetoldDataServiceMeadowEndpoints extends libFableServiceProviderBase
|
|
|
152
154
|
this._DAL[tmpDALEntityName].setProvider(tmpStorageProvider);
|
|
153
155
|
this.fable.log.info(`...initializing the ${tmpDALEntityName} Meadow Endpoints`);
|
|
154
156
|
this._MeadowEndpoints[tmpDALEntityName] = libMeadowEndpoints.new(this._DAL[tmpDALEntityName]);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
+
|
|
158
|
+
if (!tmpRoutesAlreadyConnected)
|
|
159
|
+
{
|
|
160
|
+
this.fable.log.info(`...mapping the ${tmpDALEntityName} Meadow Endpoints to Orator`);
|
|
161
|
+
this._MeadowEndpoints[tmpDALEntityName].connectRoutes(this.fable.OratorServiceServer);
|
|
162
|
+
}
|
|
163
|
+
else
|
|
164
|
+
{
|
|
165
|
+
this.fable.log.info(`...routes for ${tmpDALEntityName} already registered; skipping connectRoutes.`);
|
|
166
|
+
}
|
|
157
167
|
}
|
|
158
168
|
catch (pError)
|
|
159
169
|
{
|
|
@@ -274,10 +274,11 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
|
|
|
274
274
|
// Extract the entity name from the URL for error tracking
|
|
275
275
|
let tmpEntityHint = pURL.split('/')[0].replace(/s$/, '');
|
|
276
276
|
|
|
277
|
-
// Use the longer timeout for
|
|
278
|
-
|
|
277
|
+
// Use the longer timeout for Max and Count queries (which can be
|
|
278
|
+
// slow on large tables with millions of rows)
|
|
279
|
+
let tmpIsHeavyQuery = (pURL.indexOf('/Max/') > -1) || (pURL.match(/\/Count(\/|$)/) !== null);
|
|
279
280
|
let tmpPreviousTimeout = libHttps.globalAgent.options.timeout;
|
|
280
|
-
libHttps.globalAgent.options.timeout =
|
|
281
|
+
libHttps.globalAgent.options.timeout = tmpIsHeavyQuery
|
|
281
282
|
? tmpFable.MeadowCloneRestClient.maxRequestTimeout
|
|
282
283
|
: tmpFable.MeadowCloneRestClient.requestTimeout;
|
|
283
284
|
|
|
@@ -64,6 +64,16 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// Update UseAdvancedIDPagination on all sync entities
|
|
68
|
+
if (tmpBody.hasOwnProperty('UseAdvancedIDPagination'))
|
|
69
|
+
{
|
|
70
|
+
let tmpEntityNames = Object.keys(tmpFable.MeadowSync.MeadowSyncEntities);
|
|
71
|
+
for (let i = 0; i < tmpEntityNames.length; i++)
|
|
72
|
+
{
|
|
73
|
+
tmpFable.MeadowSync.MeadowSyncEntities[tmpEntityNames[i]].UseAdvancedIDPagination = !!tmpBody.UseAdvancedIDPagination;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
67
77
|
// Update DateTimePrecisionMS on MeadowSync and all sync entities
|
|
68
78
|
if (tmpBody.hasOwnProperty('DateTimePrecisionMS'))
|
|
69
79
|
{
|
|
@@ -173,7 +183,7 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
|
|
|
173
183
|
let tmpReinitEntities = Object.keys(tmpFable.MeadowSync.MeadowSyncEntities);
|
|
174
184
|
tmpFable.log.info(`Data Cloner: Re-created ${tmpReinitEntities.length} sync entities in ${tmpRequestedMode} mode`);
|
|
175
185
|
|
|
176
|
-
// Update SyncDeletedRecords and
|
|
186
|
+
// Update SyncDeletedRecords, MaxRecordsPerEntity, and UseAdvancedIDPagination on new entities
|
|
177
187
|
for (let i = 0; i < tmpReinitEntities.length; i++)
|
|
178
188
|
{
|
|
179
189
|
tmpFable.MeadowSync.MeadowSyncEntities[tmpReinitEntities[i]].SyncDeletedRecords = tmpCloneState.SyncDeletedRecords;
|
|
@@ -181,6 +191,10 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
|
|
|
181
191
|
{
|
|
182
192
|
tmpFable.MeadowSync.MeadowSyncEntities[tmpReinitEntities[i]].MaxRecordsPerEntity = tmpMaxRecords;
|
|
183
193
|
}
|
|
194
|
+
if (tmpBody.hasOwnProperty('UseAdvancedIDPagination'))
|
|
195
|
+
{
|
|
196
|
+
tmpFable.MeadowSync.MeadowSyncEntities[tmpReinitEntities[i]].UseAdvancedIDPagination = !!tmpBody.UseAdvancedIDPagination;
|
|
197
|
+
}
|
|
184
198
|
}
|
|
185
199
|
|
|
186
200
|
return fStartSync();
|
|
@@ -207,6 +221,13 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
|
|
|
207
221
|
let tmpSnap = { Status: tmpP.Status, Total: tmpP.Total || 0, Synced: tmpP.Synced || 0, Errors: tmpP.Errors || 0 };
|
|
208
222
|
if (tmpP.ErrorMessage) tmpSnap.ErrorMessage = tmpP.ErrorMessage;
|
|
209
223
|
|
|
224
|
+
// Include per-record breakdown fields if available (set after entity sync completes)
|
|
225
|
+
if (tmpP.hasOwnProperty('New')) tmpSnap.New = tmpP.New;
|
|
226
|
+
if (tmpP.hasOwnProperty('Updated')) tmpSnap.Updated = tmpP.Updated;
|
|
227
|
+
if (tmpP.hasOwnProperty('Unchanged')) tmpSnap.Unchanged = tmpP.Unchanged;
|
|
228
|
+
if (tmpP.hasOwnProperty('Deleted')) tmpSnap.Deleted = tmpP.Deleted;
|
|
229
|
+
if (tmpP.hasOwnProperty('ServerTotal')) tmpSnap.ServerTotal = tmpP.ServerTotal;
|
|
230
|
+
|
|
210
231
|
if ((tmpP.Status === 'Syncing' || tmpP.Status === 'Pending') && tmpFable.MeadowSync && tmpFable.MeadowSync.MeadowSyncEntities)
|
|
211
232
|
{
|
|
212
233
|
let tmpSyncEntity = tmpFable.MeadowSync.MeadowSyncEntities[tmpName];
|
|
@@ -300,9 +321,15 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
|
|
|
300
321
|
// Check for pre-count phase
|
|
301
322
|
if (tmpCloneState.SyncPhase === 'counting')
|
|
302
323
|
{
|
|
303
|
-
let tmpPC = tmpCloneState.PreCountProgress || { Counted: 0, TotalTables: 0 };
|
|
324
|
+
let tmpPC = tmpCloneState.PreCountProgress || { Counted: 0, TotalTables: 0, Tables: [] };
|
|
304
325
|
tmpMessage = `Analyzing tables: counted ${tmpPC.Counted} / ${tmpPC.TotalTables}...`;
|
|
305
326
|
|
|
327
|
+
let tmpCountElapsed = null;
|
|
328
|
+
if (tmpPC.StartTime)
|
|
329
|
+
{
|
|
330
|
+
tmpCountElapsed = fFormatDuration(Date.now() - tmpPC.StartTime);
|
|
331
|
+
}
|
|
332
|
+
|
|
306
333
|
pResponse.send(200,
|
|
307
334
|
{
|
|
308
335
|
Phase: tmpPhase,
|
|
@@ -315,10 +342,10 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
|
|
|
315
342
|
TotalTables: tmpPC.TotalTables,
|
|
316
343
|
TotalSynced: 0,
|
|
317
344
|
TotalRecords: 0,
|
|
318
|
-
Elapsed:
|
|
345
|
+
Elapsed: tmpCountElapsed,
|
|
319
346
|
SyncMode: tmpCloneState.SyncMode,
|
|
320
347
|
ETA: null,
|
|
321
|
-
PreCountGrandTotal: 0,
|
|
348
|
+
PreCountGrandTotal: tmpCloneState.PreCountGrandTotal || 0,
|
|
322
349
|
PreCountProgress: tmpPC,
|
|
323
350
|
ThroughputSamples: []
|
|
324
351
|
});
|
|
@@ -10,7 +10,7 @@ const _ProviderRegistry = {
|
|
|
10
10
|
SQLite: { moduleName: 'meadow-connection-sqlite', serviceName: 'MeadowSQLiteProvider', configKey: 'SQLite' },
|
|
11
11
|
MySQL: { moduleName: 'meadow-connection-mysql', serviceName: 'MeadowMySQLProvider', configKey: 'MySQL' },
|
|
12
12
|
MSSQL: { moduleName: 'meadow-connection-mssql', serviceName: 'MeadowMSSQLProvider', configKey: 'MSSQL' },
|
|
13
|
-
PostgreSQL: { moduleName: 'meadow-connection-postgresql', serviceName: '
|
|
13
|
+
PostgreSQL: { moduleName: 'meadow-connection-postgresql', serviceName: 'MeadowPostgreSQLProvider', configKey: 'PostgreSQL' },
|
|
14
14
|
Solr: { moduleName: 'meadow-connection-solr', serviceName: 'MeadowConnectionSolr', configKey: 'Solr' },
|
|
15
15
|
MongoDB: { moduleName: 'meadow-connection-mongodb', serviceName: 'MeadowConnectionMongoDB', configKey: 'MongoDB' },
|
|
16
16
|
RocksDB: { moduleName: 'meadow-connection-rocksdb', serviceName: 'MeadowConnectionRocksDB', configKey: 'RocksDB' },
|
|
@@ -642,7 +642,12 @@ class RetoldDataServiceDataCloner extends libFableServiceProviderBase
|
|
|
642
642
|
preCountTables(pTables, fCallback)
|
|
643
643
|
{
|
|
644
644
|
this._cloneState.SyncPhase = 'counting';
|
|
645
|
-
this._cloneState.PreCountProgress = {
|
|
645
|
+
this._cloneState.PreCountProgress = {
|
|
646
|
+
Counted: 0,
|
|
647
|
+
TotalTables: pTables.length,
|
|
648
|
+
StartTime: Date.now(),
|
|
649
|
+
Tables: []
|
|
650
|
+
};
|
|
646
651
|
this._cloneState.PreCountGrandTotal = 0;
|
|
647
652
|
|
|
648
653
|
this.fable.log.info(`Data Cloner: Pre-counting records for ${pTables.length} tables...`);
|
|
@@ -656,6 +661,7 @@ class RetoldDataServiceDataCloner extends libFableServiceProviderBase
|
|
|
656
661
|
return fNext();
|
|
657
662
|
}
|
|
658
663
|
|
|
664
|
+
let tmpTableStartTime = Date.now();
|
|
659
665
|
let tmpCountURL = `${pTableName}s/Count`;
|
|
660
666
|
this.fable.MeadowCloneRestClient.getJSON(tmpCountURL,
|
|
661
667
|
(pError, pResponse, pBody) =>
|
|
@@ -669,7 +675,14 @@ class RetoldDataServiceDataCloner extends libFableServiceProviderBase
|
|
|
669
675
|
{
|
|
670
676
|
this._cloneState.SyncProgress[pTableName].Total = tmpCount;
|
|
671
677
|
}
|
|
678
|
+
let tmpElapsedMs = Date.now() - tmpTableStartTime;
|
|
672
679
|
this._cloneState.PreCountProgress.Counted++;
|
|
680
|
+
this._cloneState.PreCountProgress.Tables.push({
|
|
681
|
+
Name: pTableName,
|
|
682
|
+
Count: tmpCount,
|
|
683
|
+
ElapsedMs: tmpElapsedMs,
|
|
684
|
+
Error: pError ? true : false
|
|
685
|
+
});
|
|
673
686
|
this._cloneState.PreCountGrandTotal += tmpCount;
|
|
674
687
|
return fNext();
|
|
675
688
|
});
|
|
@@ -866,15 +879,42 @@ class RetoldDataServiceDataCloner extends libFableServiceProviderBase
|
|
|
866
879
|
if (this._cloneState.SyncLogFileLogger)
|
|
867
880
|
{
|
|
868
881
|
let tmpLogPath = this._cloneState.SyncLogFilePath || '';
|
|
869
|
-
this._cloneState.SyncLogFileLogger
|
|
882
|
+
let tmpLogger = this._cloneState.SyncLogFileLogger;
|
|
883
|
+
|
|
884
|
+
// Log before closing so the message is captured in the file
|
|
885
|
+
this.fable.log.info(`Data Cloner: Log file closing — ${tmpLogPath}`);
|
|
886
|
+
|
|
887
|
+
// Remove the logger from fable.log stream arrays so no writes
|
|
888
|
+
// are dispatched to it after the underlying stream is closed.
|
|
889
|
+
let tmpStreamArrays = [
|
|
890
|
+
'logStreams', 'logStreamsTrace', 'logStreamsDebug',
|
|
891
|
+
'logStreamsInfo', 'logStreamsWarn', 'logStreamsError', 'logStreamsFatal'
|
|
892
|
+
];
|
|
893
|
+
for (let s = 0; s < tmpStreamArrays.length; s++)
|
|
870
894
|
{
|
|
871
|
-
this.
|
|
895
|
+
let tmpArr = this.fable.log[tmpStreamArrays[s]];
|
|
896
|
+
if (Array.isArray(tmpArr))
|
|
872
897
|
{
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
898
|
+
let tmpIdx = tmpArr.indexOf(tmpLogger);
|
|
899
|
+
if (tmpIdx > -1) tmpArr.splice(tmpIdx, 1);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
876
903
|
this._cloneState.SyncLogFileLogger = null;
|
|
877
904
|
this._cloneState.SyncLogFilePath = null;
|
|
905
|
+
|
|
906
|
+
// Now flush and close safely — no more writes can reach this stream
|
|
907
|
+
try
|
|
908
|
+
{
|
|
909
|
+
tmpLogger.flushBufferToLogFile(() =>
|
|
910
|
+
{
|
|
911
|
+
tmpLogger.closeWriter(() => {});
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
catch (pCloseErr)
|
|
915
|
+
{
|
|
916
|
+
// Ignore close errors
|
|
917
|
+
}
|
|
878
918
|
}
|
|
879
919
|
|
|
880
920
|
return;
|
|
@@ -915,7 +955,7 @@ class RetoldDataServiceDataCloner extends libFableServiceProviderBase
|
|
|
915
955
|
{
|
|
916
956
|
let tmpResults = tmpSyncEntity.syncResults;
|
|
917
957
|
tmpProgress.New = tmpResults.Created || 0;
|
|
918
|
-
tmpProgress.Updated = 0;
|
|
958
|
+
tmpProgress.Updated = tmpResults.Updated || 0;
|
|
919
959
|
tmpProgress.Unchanged = tmpResults.LocalRecordCount || 0;
|
|
920
960
|
tmpProgress.Deleted = tmpResults.Deleted || 0;
|
|
921
961
|
tmpProgress.ServerTotal = tmpResults.ServerRecordCount || 0;
|
|
@@ -73,7 +73,8 @@ class DataClonerApplication extends libPictApplication
|
|
|
73
73
|
'solrHost', 'solrPort', 'solrCore', 'solrPath',
|
|
74
74
|
'mongodbHost', 'mongodbPort', 'mongodbUser', 'mongodbPassword', 'mongodbDatabase', 'mongodbConnectionLimit',
|
|
75
75
|
'rocksdbFolder',
|
|
76
|
-
'bibliographFolder'
|
|
76
|
+
'bibliographFolder',
|
|
77
|
+
'syncMaxRecords'
|
|
77
78
|
]
|
|
78
79
|
};
|
|
79
80
|
|
|
@@ -295,6 +295,12 @@ class DataClonerProvider extends libPictProvider
|
|
|
295
295
|
{
|
|
296
296
|
document.getElementById('solrSecure').checked = tmpSolrSecure === 'true';
|
|
297
297
|
}
|
|
298
|
+
// Restore advanced ID pagination checkbox
|
|
299
|
+
let tmpAdvancedIDPagination = localStorage.getItem('dataCloner_syncAdvancedIDPagination');
|
|
300
|
+
if (tmpAdvancedIDPagination !== null)
|
|
301
|
+
{
|
|
302
|
+
document.getElementById('syncAdvancedIDPagination').checked = tmpAdvancedIDPagination === 'true';
|
|
303
|
+
}
|
|
298
304
|
}
|
|
299
305
|
|
|
300
306
|
initPersistence()
|
|
@@ -345,6 +351,16 @@ class DataClonerProvider extends libPictProvider
|
|
|
345
351
|
});
|
|
346
352
|
}
|
|
347
353
|
|
|
354
|
+
// Persist advanced ID pagination checkbox
|
|
355
|
+
let tmpAdvancedIDPaginationEl = document.getElementById('syncAdvancedIDPagination');
|
|
356
|
+
if (tmpAdvancedIDPaginationEl)
|
|
357
|
+
{
|
|
358
|
+
tmpAdvancedIDPaginationEl.addEventListener('change', function()
|
|
359
|
+
{
|
|
360
|
+
localStorage.setItem('dataCloner_syncAdvancedIDPagination', this.checked);
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
348
364
|
// Persist auto-process checkboxes
|
|
349
365
|
let tmpAutoIds = ['auto1', 'auto2', 'auto3', 'auto4', 'auto5'];
|
|
350
366
|
for (let a = 0; a < tmpAutoIds.length; a++)
|
|
@@ -447,7 +463,10 @@ class DataClonerProvider extends libPictProvider
|
|
|
447
463
|
}
|
|
448
464
|
if (pData.PreCountProgress && pData.PreCountProgress.Counted < pData.PreCountProgress.TotalTables)
|
|
449
465
|
{
|
|
450
|
-
|
|
466
|
+
let tmpCountedSoFar = pData.PreCountGrandTotal > 0
|
|
467
|
+
? ' (' + pData.PreCountGrandTotal.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') + ' records found)'
|
|
468
|
+
: '';
|
|
469
|
+
tmpMetaParts.push('<span class="live-status-meta-item">counting: ' + pData.PreCountProgress.Counted + ' / ' + pData.PreCountProgress.TotalTables + ' tables' + tmpCountedSoFar + '</span>');
|
|
451
470
|
}
|
|
452
471
|
if (pData.Errors > 0)
|
|
453
472
|
{
|
|
@@ -466,7 +485,12 @@ class DataClonerProvider extends libPictProvider
|
|
|
466
485
|
|
|
467
486
|
// Update progress bar
|
|
468
487
|
let tmpPct = 0;
|
|
469
|
-
if (pData.Phase === 'syncing' && pData.
|
|
488
|
+
if (pData.Phase === 'syncing' && pData.PreCountProgress && pData.PreCountProgress.Counted < pData.PreCountProgress.TotalTables)
|
|
489
|
+
{
|
|
490
|
+
// During counting phase, show table counting progress
|
|
491
|
+
tmpPct = Math.min((pData.PreCountProgress.Counted / pData.PreCountProgress.TotalTables) * 100, 99);
|
|
492
|
+
}
|
|
493
|
+
else if (pData.Phase === 'syncing' && pData.PreCountGrandTotal > 0 && pData.TotalSynced > 0)
|
|
470
494
|
{
|
|
471
495
|
tmpPct = Math.min((pData.TotalSynced / pData.PreCountGrandTotal) * 100, 99.9);
|
|
472
496
|
}
|
|
@@ -489,6 +513,16 @@ class DataClonerProvider extends libPictProvider
|
|
|
489
513
|
}
|
|
490
514
|
tmpProgressFill.style.width = Math.min(100, Math.round(tmpPct)) + '%';
|
|
491
515
|
|
|
516
|
+
// Auto-expand the detail view when sync starts so users see counting progress
|
|
517
|
+
if ((pData.Phase === 'syncing' || pData.Phase === 'stopping') && !this.pict.AppData.DataCloner.StatusDetailExpanded)
|
|
518
|
+
{
|
|
519
|
+
let tmpLayoutView = this.pict.views['DataCloner-Layout'];
|
|
520
|
+
if (tmpLayoutView && typeof tmpLayoutView.toggleStatusDetail === 'function')
|
|
521
|
+
{
|
|
522
|
+
tmpLayoutView.toggleStatusDetail();
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
492
526
|
// If the detail view is expanded, re-render it with fresh data
|
|
493
527
|
if (this.pict.AppData.DataCloner.StatusDetailExpanded)
|
|
494
528
|
{
|
|
@@ -558,6 +592,59 @@ class DataClonerProvider extends libPictProvider
|
|
|
558
592
|
.catch(function() { /* ignore poll errors */ });
|
|
559
593
|
}
|
|
560
594
|
|
|
595
|
+
renderCountingPhaseDetail(pContainer, pPreCountProgress)
|
|
596
|
+
{
|
|
597
|
+
let tmpTables = pPreCountProgress.Tables || [];
|
|
598
|
+
let tmpCounted = pPreCountProgress.Counted || 0;
|
|
599
|
+
let tmpTotal = pPreCountProgress.TotalTables || 0;
|
|
600
|
+
let tmpRunningTotal = 0;
|
|
601
|
+
|
|
602
|
+
let tmpHtml = '<div class="status-detail-section">';
|
|
603
|
+
tmpHtml += '<div class="status-detail-section-title">Counting Records (' + tmpCounted + ' / ' + tmpTotal + ' tables)</div>';
|
|
604
|
+
|
|
605
|
+
if (tmpTables.length > 0)
|
|
606
|
+
{
|
|
607
|
+
tmpHtml += '<table class="precount-table">';
|
|
608
|
+
tmpHtml += '<thead><tr><th>Table</th><th style="text-align:right">Records</th><th style="text-align:right">Time</th></tr></thead>';
|
|
609
|
+
tmpHtml += '<tbody>';
|
|
610
|
+
for (let i = 0; i < tmpTables.length; i++)
|
|
611
|
+
{
|
|
612
|
+
let tmpT = tmpTables[i];
|
|
613
|
+
tmpRunningTotal += tmpT.Count;
|
|
614
|
+
let tmpCountFmt = this.formatNumber(tmpT.Count);
|
|
615
|
+
let tmpTimeFmt = tmpT.ElapsedMs < 1000
|
|
616
|
+
? tmpT.ElapsedMs + 'ms'
|
|
617
|
+
: (tmpT.ElapsedMs / 1000).toFixed(1) + 's';
|
|
618
|
+
let tmpRowClass = tmpT.Error ? ' class="precount-error"' : '';
|
|
619
|
+
tmpHtml += '<tr' + tmpRowClass + '>';
|
|
620
|
+
tmpHtml += '<td>' + this.escapeHtml(tmpT.Name) + '</td>';
|
|
621
|
+
tmpHtml += '<td style="text-align:right; font-variant-numeric:tabular-nums">' + tmpCountFmt + '</td>';
|
|
622
|
+
tmpHtml += '<td style="text-align:right; font-variant-numeric:tabular-nums; color:#888">' + tmpTimeFmt + '</td>';
|
|
623
|
+
tmpHtml += '</tr>';
|
|
624
|
+
}
|
|
625
|
+
tmpHtml += '</tbody>';
|
|
626
|
+
tmpHtml += '<tfoot><tr>';
|
|
627
|
+
tmpHtml += '<td><strong>Total</strong></td>';
|
|
628
|
+
tmpHtml += '<td style="text-align:right; font-variant-numeric:tabular-nums"><strong>' + this.formatNumber(tmpRunningTotal) + '</strong></td>';
|
|
629
|
+
tmpHtml += '<td></td>';
|
|
630
|
+
tmpHtml += '</tr></tfoot>';
|
|
631
|
+
tmpHtml += '</table>';
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Show pending indicator for remaining tables
|
|
635
|
+
let tmpRemaining = tmpTotal - tmpCounted;
|
|
636
|
+
if (tmpRemaining > 0)
|
|
637
|
+
{
|
|
638
|
+
tmpHtml += '<div class="precount-pending">';
|
|
639
|
+
tmpHtml += '<span class="precount-spinner"></span> ';
|
|
640
|
+
tmpHtml += tmpRemaining + ' table' + (tmpRemaining === 1 ? '' : 's') + ' remaining…';
|
|
641
|
+
tmpHtml += '</div>';
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
tmpHtml += '</div>';
|
|
645
|
+
pContainer.innerHTML = tmpHtml;
|
|
646
|
+
}
|
|
647
|
+
|
|
561
648
|
renderStatusDetail()
|
|
562
649
|
{
|
|
563
650
|
let tmpContainer = document.getElementById('DataCloner-StatusDetail-Container');
|
|
@@ -568,6 +655,19 @@ class DataClonerProvider extends libPictProvider
|
|
|
568
655
|
let tmpStatusData = tmpAppData.StatusDetailData;
|
|
569
656
|
let tmpReport = tmpAppData.LastReport;
|
|
570
657
|
|
|
658
|
+
// During the counting phase, show per-table counts as they arrive
|
|
659
|
+
if (tmpLiveStatus && tmpLiveStatus.PreCountProgress
|
|
660
|
+
&& tmpLiveStatus.PreCountProgress.Tables
|
|
661
|
+
&& tmpLiveStatus.Phase === 'syncing'
|
|
662
|
+
&& tmpLiveStatus.PreCountProgress.Counted < tmpLiveStatus.PreCountProgress.TotalTables)
|
|
663
|
+
{
|
|
664
|
+
this.renderCountingPhaseDetail(tmpContainer, tmpLiveStatus.PreCountProgress);
|
|
665
|
+
// Hide histogram during counting
|
|
666
|
+
let tmpHistContainer = document.getElementById('DataCloner-Throughput-Histogram');
|
|
667
|
+
if (tmpHistContainer) tmpHistContainer.style.display = 'none';
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
|
|
571
671
|
// Determine data source: live during sync, report after sync
|
|
572
672
|
let tmpTables = {};
|
|
573
673
|
let tmpThroughputSamples = [];
|
|
@@ -101,6 +101,7 @@ class DataClonerExportView extends libPictView
|
|
|
101
101
|
if (!isNaN(tmpPrecision) && tmpPrecision !== 1000) tmpConfig.Sync.DateTimePrecisionMS = tmpPrecision;
|
|
102
102
|
let tmpMaxRecords = parseInt(document.getElementById('syncMaxRecords').value, 10);
|
|
103
103
|
if (tmpMaxRecords > 0) tmpConfig.Sync.MaxRecords = tmpMaxRecords;
|
|
104
|
+
if (document.getElementById('syncAdvancedIDPagination').checked) tmpConfig.Sync.UseAdvancedIDPagination = true;
|
|
104
105
|
|
|
105
106
|
return tmpConfig;
|
|
106
107
|
}
|
|
@@ -169,6 +170,7 @@ class DataClonerExportView extends libPictView
|
|
|
169
170
|
let tmpSelectedTables = this.pict.views['DataCloner-Schema'].getSelectedTables();
|
|
170
171
|
tmpConfig.Sync.SyncEntityList = tmpSelectedTables.length > 0 ? tmpSelectedTables : [];
|
|
171
172
|
tmpConfig.Sync.SyncEntityOptions = {};
|
|
173
|
+
if (document.getElementById('syncAdvancedIDPagination').checked) tmpConfig.Sync.UseAdvancedIDPagination = true;
|
|
172
174
|
|
|
173
175
|
// ---- SessionManager ----
|
|
174
176
|
tmpConfig.SessionManager = { Sessions: {} };
|
|
@@ -339,6 +339,34 @@ select { background: #fff; width: 100%; padding: 8px 12px; border: 1px solid #cc
|
|
|
339
339
|
font-size: 0.78em; color: #888; margin-top: 4px; padding-left: 18px;
|
|
340
340
|
font-family: monospace; max-height: 80px; overflow-y: auto;
|
|
341
341
|
}
|
|
342
|
+
|
|
343
|
+
/* Pre-count Table */
|
|
344
|
+
.precount-table {
|
|
345
|
+
width: 100%; border-collapse: collapse; font-size: 0.88em;
|
|
346
|
+
}
|
|
347
|
+
.precount-table thead th {
|
|
348
|
+
text-align: left; font-weight: 600; color: #555; font-size: 0.85em;
|
|
349
|
+
text-transform: uppercase; letter-spacing: 0.3px;
|
|
350
|
+
padding: 4px 8px 6px; border-bottom: 2px solid #e9ecef;
|
|
351
|
+
}
|
|
352
|
+
.precount-table tbody td {
|
|
353
|
+
padding: 4px 8px; border-bottom: 1px solid #f0f0f0;
|
|
354
|
+
}
|
|
355
|
+
.precount-table tbody tr:last-child td { border-bottom: 1px solid #e9ecef; }
|
|
356
|
+
.precount-table tfoot td {
|
|
357
|
+
padding: 6px 8px 2px; font-size: 0.95em;
|
|
358
|
+
}
|
|
359
|
+
.precount-error td { color: #dc3545; }
|
|
360
|
+
.precount-pending {
|
|
361
|
+
color: #888; font-size: 0.85em; font-style: italic; padding: 8px 0 2px;
|
|
362
|
+
display: flex; align-items: center; gap: 6px;
|
|
363
|
+
}
|
|
364
|
+
.precount-spinner {
|
|
365
|
+
display: inline-block; width: 12px; height: 12px;
|
|
366
|
+
border: 2px solid #ddd; border-top-color: #4a90d9;
|
|
367
|
+
border-radius: 50%; animation: precount-spin 0.8s linear infinite;
|
|
368
|
+
}
|
|
369
|
+
@keyframes precount-spin { to { transform: rotate(360deg); } }
|
|
342
370
|
`,
|
|
343
371
|
Templates:
|
|
344
372
|
[
|
|
@@ -17,6 +17,7 @@ class DataClonerSyncView extends libPictView
|
|
|
17
17
|
let tmpSyncMode = document.querySelector('input[name="syncMode"]:checked').value;
|
|
18
18
|
let tmpMaxRecords = parseInt(document.getElementById('syncMaxRecords').value, 10) || 0;
|
|
19
19
|
let tmpLogToFile = document.getElementById('syncLogFile').checked;
|
|
20
|
+
let tmpAdvancedIDPagination = document.getElementById('syncAdvancedIDPagination').checked;
|
|
20
21
|
|
|
21
22
|
if (tmpSelectedTables.length === 0)
|
|
22
23
|
{
|
|
@@ -32,6 +33,7 @@ class DataClonerSyncView extends libPictView
|
|
|
32
33
|
let tmpPostBody = { Tables: tmpSelectedTables, PageSize: tmpPageSize, DateTimePrecisionMS: tmpDateTimePrecisionMS, SyncDeletedRecords: tmpSyncDeletedRecords, SyncMode: tmpSyncMode };
|
|
33
34
|
if (tmpMaxRecords > 0) tmpPostBody.MaxRecordsPerEntity = tmpMaxRecords;
|
|
34
35
|
if (tmpLogToFile) tmpPostBody.LogToFile = true;
|
|
36
|
+
if (tmpAdvancedIDPagination) tmpPostBody.UseAdvancedIDPagination = true;
|
|
35
37
|
this.pict.providers.DataCloner.api('POST', '/clone/sync/start', tmpPostBody)
|
|
36
38
|
.then(function(pData)
|
|
37
39
|
{
|
|
@@ -522,6 +524,11 @@ module.exports.default_configuration =
|
|
|
522
524
|
<label for="syncDeletedRecords">Sync deleted records (fetch records marked Deleted=1 on source and mirror locally)</label>
|
|
523
525
|
</div>
|
|
524
526
|
|
|
527
|
+
<div class="checkbox-row">
|
|
528
|
+
<input type="checkbox" id="syncAdvancedIDPagination">
|
|
529
|
+
<label for="syncAdvancedIDPagination">Use advanced ID pagination (faster for large tables; uses keyset pagination instead of OFFSET)</label>
|
|
530
|
+
</div>
|
|
531
|
+
|
|
525
532
|
<div class="inline-group" style="margin-top:8px; margin-bottom:4px">
|
|
526
533
|
<div style="flex:0 0 200px">
|
|
527
534
|
<label for="syncMaxRecords">Max Records per Entity</label>
|