@mindline/sync 1.0.109 → 1.0.111
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/index.d.ts +34 -5
- package/dist/sync.es.js +523 -469
- package/dist/sync.es.js.map +1 -1
- package/dist/sync.umd.js +33 -33
- package/dist/sync.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/index.d.ts +8 -3
- package/src/index.ts +218 -98
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -251,7 +251,6 @@ declare module "@mindline/sync" {
|
|
|
251
251
|
tenantNodes: TenantNode[];
|
|
252
252
|
pb_startTS: number;
|
|
253
253
|
pb_progress: number;
|
|
254
|
-
pb_increment: number;
|
|
255
254
|
pb_idle: number;
|
|
256
255
|
pb_idleMax: number;
|
|
257
256
|
pb_total: number;
|
|
@@ -268,12 +267,18 @@ declare module "@mindline/sync" {
|
|
|
268
267
|
batchIdArray: Array<Object>,
|
|
269
268
|
setRefreshDeltaTrigger: (workspace: string) => void,
|
|
270
269
|
setReadersTotal: (readersTotal: number) => void,
|
|
270
|
+
setReadersExcluded: (readersExcluded: number) => void,
|
|
271
271
|
setReadersCurrent: (readersCurrent: number) => void,
|
|
272
272
|
setWritersTotal: (writersTotal: number) => void,
|
|
273
|
+
setWritersExcluded: (writersExcluded: number) => void,
|
|
273
274
|
setWritersCurrent: (writersCurrent: number) => void,
|
|
274
275
|
setMilestones: (milestones: Milestone[]) => void,
|
|
275
276
|
setConfigSyncResult: (result: string) => void,
|
|
276
|
-
|
|
277
|
+
setSyncProgress: (progress: number) => void,
|
|
278
|
+
bClearLocalStorage: boolean,
|
|
279
|
+
message: string,
|
|
280
|
+
instance?: IPublicClientApplication,
|
|
281
|
+
authorizedUser?: User): void;
|
|
277
282
|
startSync(instance: IPublicClientApplication, authorizedUser: User | null | undefined, config: SyncConfig | null | undefined): APIResult;
|
|
278
283
|
}
|
|
279
284
|
export class TenantNode {
|
|
@@ -359,4 +364,4 @@ declare module "@mindline/sync" {
|
|
|
359
364
|
// ======================= Azure REST API ===============================
|
|
360
365
|
export function canListRootAssignments(instance: IPublicClientApplication, user: User): Promise<boolean>;
|
|
361
366
|
export function elevateGlobalAdminToUserAccessAdmin(instance: IPublicClientApplication, user: User): Promise<boolean>;
|
|
362
|
-
}
|
|
367
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
//index.ts - published interface - AAD implementations, facade to Mindline SyncConfig API
|
|
2
|
-
import
|
|
3
|
-
import { AccountInfo } from "@azure/msal-common";
|
|
2
|
+
import type { AccountInfo } from "@azure/msal-common";
|
|
4
3
|
import { IPublicClientApplication, AuthenticationResult } from "@azure/msal-browser"
|
|
5
4
|
import { deserializeArray } from 'class-transformer';
|
|
6
5
|
import users from "./users.json";
|
|
@@ -918,11 +917,24 @@ export class BatchArray {
|
|
|
918
917
|
tenantNodes: TenantNode[];
|
|
919
918
|
pb_startTS: number;
|
|
920
919
|
pb_progress: number;
|
|
921
|
-
pb_increment: number;
|
|
922
920
|
pb_idle: number;
|
|
923
|
-
pb_idleMax: number;
|
|
924
921
|
pb_total: number;
|
|
925
|
-
pb_timer:
|
|
922
|
+
pb_timer: ReturnType<typeof setInterval> | null;
|
|
923
|
+
|
|
924
|
+
// Polling: /api/stats is the single source of truth for hydration.
|
|
925
|
+
private readonly pollIntervalSeconds: number = 5;
|
|
926
|
+
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
927
|
+
private pollLastUpdatedByBatchId: Record<string, string> = {};
|
|
928
|
+
private pollInstance: IPublicClientApplication | null = null;
|
|
929
|
+
private pollAuthorizedUser: User | null = null;
|
|
930
|
+
private pollBatchIdArray: Array<any> = [];
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
// Store UI callbacks so we can update the UI from poll/completion events.
|
|
934
|
+
private setIdleText: ((idleText: string) => void) | null = null;
|
|
935
|
+
|
|
936
|
+
// Handler used by polling: we reuse the same stats-processing logic.
|
|
937
|
+
private statsHydrationHandler: ((batchId: string, stats: any) => void) | null = null;
|
|
926
938
|
milestoneArray: MilestoneArray;
|
|
927
939
|
constructor(
|
|
928
940
|
config: SyncConfig | null,
|
|
@@ -933,16 +945,16 @@ export class BatchArray {
|
|
|
933
945
|
this.init(config, syncPortalGlobalState, bClearLocalStorage);
|
|
934
946
|
this.pb_startTS = 0;
|
|
935
947
|
this.pb_progress = 0;
|
|
936
|
-
this.pb_increment = .25;
|
|
937
948
|
this.pb_timer = null;
|
|
938
949
|
this.pb_idle = 0;
|
|
939
|
-
this.pb_idleMax = 0;
|
|
940
950
|
this.pb_total = 0;
|
|
941
951
|
this.milestoneArray = new MilestoneArray(false);
|
|
942
952
|
}
|
|
943
953
|
clearStoredBatchIds(): void {
|
|
944
954
|
if (storageAvailable()) {
|
|
945
955
|
localStorage.setItem("BatchIdArray", "[]");
|
|
956
|
+
// Also clear any persisted UI progress for the in-flight batch restore.
|
|
957
|
+
localStorage.removeItem("BatchIdArrayProgress");
|
|
946
958
|
}
|
|
947
959
|
}
|
|
948
960
|
// populate tenantNodes based on config tenants
|
|
@@ -1026,59 +1038,116 @@ export class BatchArray {
|
|
|
1026
1038
|
});
|
|
1027
1039
|
}
|
|
1028
1040
|
}
|
|
1029
|
-
initializeProgressBar(setSyncProgress: (progress: number) => void,
|
|
1041
|
+
initializeProgressBar(setSyncProgress: (progress: number) => void, _setConfigSyncResult: (result: string) => void, setIdleText: (idleText: string) => void, setMilestones: (milestones: Milestone[]) => void): void {
|
|
1042
|
+
// Save callback so completion/polling can update the idle text too.
|
|
1043
|
+
this.setIdleText = setIdleText;
|
|
1044
|
+
|
|
1045
|
+
|
|
1030
1046
|
this.pb_startTS = Date.now();
|
|
1031
1047
|
this.pb_progress = 0;
|
|
1032
|
-
this.pb_increment = .25;
|
|
1033
1048
|
this.pb_idle = 0;
|
|
1034
|
-
this.pb_idleMax = 0;
|
|
1035
1049
|
this.pb_total = 0;
|
|
1050
|
+
setSyncProgress(this.pb_progress);
|
|
1051
|
+
setIdleText("Starting sync...");
|
|
1036
1052
|
this.pb_timer = setInterval(() => {
|
|
1037
|
-
//
|
|
1053
|
+
// Completion check is now handled by the SignalR handler via isSyncCompleted()
|
|
1054
|
+
// This timer only increments elapsed time; actual completion logic lives in the handler.
|
|
1038
1055
|
console.log("this.tenantNodes", this.tenantNodes)
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
setSyncProgress(this.pb_progress);
|
|
1045
|
-
setIdleText(`Complete. [max idle: ${this.pb_idleMax}]`);
|
|
1046
|
-
this.clearStoredBatchIds();
|
|
1047
|
-
}
|
|
1048
|
-
else {
|
|
1049
|
-
// if we've gone 60 seconds without a signalR message, finish the sync
|
|
1050
|
-
this.pb_total = this.pb_total + 1;
|
|
1051
|
-
this.pb_idle = this.pb_idle + 1;
|
|
1052
|
-
this.pb_idleMax = Math.max(this.pb_idle, this.pb_idleMax);
|
|
1053
|
-
setIdleText(`${this.pb_total} seconds elapsed. Last update ${this.pb_idle} seconds ago. [max idle: ${this.pb_idleMax}/60]`);
|
|
1054
|
-
if (this.pb_idle >= 60) {
|
|
1055
|
-
if (this.milestoneArray.milestones[0].Write == null) {
|
|
1056
|
-
//this.milestoneArray.write(setMilestones); -- allow sync to cntinue
|
|
1057
|
-
setConfigSyncResult(`sync continuing, but no update for ${this.pb_idle} seconds`);
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
// if we get to 100, the progress bar stops but SignalR or countdown timer completes the sync
|
|
1061
|
-
if (this.pb_progress < 100) {
|
|
1062
|
-
this.pb_progress = Math.min(100, this.pb_progress + this.pb_increment);
|
|
1063
|
-
setSyncProgress(this.pb_progress);
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1056
|
+
|
|
1057
|
+
this.pb_total = this.pb_total + 1;
|
|
1058
|
+
this.pb_idle = this.pb_idle + 1;
|
|
1059
|
+
|
|
1060
|
+
setIdleText(`${this.pb_total} seconds elapsed. Last update ${this.pb_idle} seconds ago.`);
|
|
1066
1061
|
}, 1000);
|
|
1067
1062
|
this.milestoneArray.start(setMilestones);
|
|
1068
1063
|
}
|
|
1064
|
+
|
|
1065
|
+
private stopPolling(): void {
|
|
1066
|
+
if (this.pollTimer != null) {
|
|
1067
|
+
clearInterval(this.pollTimer);
|
|
1068
|
+
this.pollTimer = null;
|
|
1069
|
+
}
|
|
1070
|
+
this.pollInstance = null;
|
|
1071
|
+
this.pollAuthorizedUser = null;
|
|
1072
|
+
this.pollBatchIdArray = [];
|
|
1073
|
+
this.pollLastUpdatedByBatchId = {};
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
private async pollStatsOnce(): Promise<void> {
|
|
1077
|
+
// Polling-only mode (single source of truth: /api/stats).
|
|
1078
|
+
if (this.pb_timer == null) return;
|
|
1079
|
+
if (this.pollInstance == null || this.pollAuthorizedUser == null) return;
|
|
1080
|
+
if (!this.statsHydrationHandler) return;
|
|
1081
|
+
if (!this.pollBatchIdArray || this.pollBatchIdArray.length === 0) return;
|
|
1082
|
+
|
|
1083
|
+
for (const batchPair of this.pollBatchIdArray) {
|
|
1084
|
+
const batchId: string | undefined = batchPair?.BatchId;
|
|
1085
|
+
if (!batchId) continue;
|
|
1086
|
+
|
|
1087
|
+
const statsResult: APIResult = await readerStats(this.pollInstance, this.pollAuthorizedUser, batchId);
|
|
1088
|
+
if (!statsResult.result || !statsResult.array || !statsResult.array[0]) continue;
|
|
1089
|
+
|
|
1090
|
+
const payload: any = statsResult.array[0];
|
|
1091
|
+
const lastUpdated: string | undefined = payload.lastUpdated;
|
|
1092
|
+
const prevLastUpdated = this.pollLastUpdatedByBatchId[batchId];
|
|
1093
|
+
this.pollLastUpdatedByBatchId[batchId] = lastUpdated ?? "";
|
|
1094
|
+
|
|
1095
|
+
// Only feed the handler if backend state changed.
|
|
1096
|
+
if (!lastUpdated || lastUpdated === prevLastUpdated) continue;
|
|
1097
|
+
|
|
1098
|
+
this.statsHydrationHandler(batchId, payload.stats);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1069
1101
|
uninitializeProgressBar(setSyncProgress: (progress: number) => void, setConfigSyncResult: (result: string) => void, setIdleText: (idleText: string) => void, setMilestones: (milestones: Milestone[]) => void): void {
|
|
1070
1102
|
this.pb_startTS = 0;
|
|
1071
1103
|
this.pb_progress = 0;
|
|
1072
1104
|
setSyncProgress(this.pb_progress);
|
|
1073
1105
|
setConfigSyncResult("sync failed to execute");
|
|
1074
|
-
this.pb_increment = 0;
|
|
1075
1106
|
clearInterval(this.pb_timer!);
|
|
1076
1107
|
this.pb_timer = null;
|
|
1077
1108
|
this.pb_idle = 0;
|
|
1078
|
-
this.
|
|
1079
|
-
setIdleText(`No updates seen for ${this.pb_idle} seconds. [max idle: ${this.pb_idleMax}]`);
|
|
1109
|
+
setIdleText(`No updates seen for ${this.pb_idle} seconds.`);
|
|
1080
1110
|
this.milestoneArray.unstart(setMilestones);
|
|
1081
1111
|
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Calculates the sync progress percentage (0-99%) based on completed read and write operations.
|
|
1114
|
+
*
|
|
1115
|
+
* The progress formula is: (completedReads + completedWrites) / (total * 2) * 100
|
|
1116
|
+
*
|
|
1117
|
+
* Progress is divided by (total * 2) because the sync process has two phases:
|
|
1118
|
+
* 1. Reading phase: read users from source + excluded users
|
|
1119
|
+
* 2. Writing phase: write users to target + excluded users + deferred users
|
|
1120
|
+
*
|
|
1121
|
+
* Each phase can contribute up to 50% of the total progress. The result is capped at 99%
|
|
1122
|
+
* to prevent premature completion (100% is only shown when sync is truly complete).
|
|
1123
|
+
*
|
|
1124
|
+
* @param total - Total number of users to sync
|
|
1125
|
+
* @param read - Number of users read from source
|
|
1126
|
+
* @param written - Number of users written to target
|
|
1127
|
+
* @param excluded - Number of users excluded from sync
|
|
1128
|
+
* @param deferred - Number of users deferred (postponed) during writing
|
|
1129
|
+
* @returns Progress percentage from 0 to 99
|
|
1130
|
+
*/
|
|
1131
|
+
calculateProgress(total: number, read: number, written: number, excluded: number, deferred: number): number {
|
|
1132
|
+
if (total <= 0) {
|
|
1133
|
+
return 0;
|
|
1134
|
+
}
|
|
1135
|
+
const completedReads = Math.min(total, read + excluded);
|
|
1136
|
+
const completedWrites = Math.min(total, written + excluded + deferred);
|
|
1137
|
+
const completedUnits = completedReads + completedWrites;
|
|
1138
|
+
return Math.min(99, Math.round((completedUnits / (total * 2)) * 100));
|
|
1139
|
+
}
|
|
1140
|
+
updateProgressFromTotals(total: number, read: number, written: number, excluded: number, deferred: number, setSyncProgress: (progress: number) => void): void {
|
|
1141
|
+
const progress = this.calculateProgress(total, read, written, excluded, deferred);
|
|
1142
|
+
this.pb_progress = Math.max(this.pb_progress, progress);
|
|
1143
|
+
setSyncProgress(this.pb_progress);
|
|
1144
|
+
|
|
1145
|
+
// Persist progress so a page refresh can restore the same (or higher) percent.
|
|
1146
|
+
// Note: This is intentionally monotonic because pb_progress is monotonic.
|
|
1147
|
+
if (storageAvailable()) {
|
|
1148
|
+
localStorage.setItem("BatchIdArrayProgress", String(this.pb_progress));
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1082
1151
|
initializeSignalR(
|
|
1083
1152
|
config: SyncConfig | null | undefined,
|
|
1084
1153
|
syncPortalGlobalState: InitInfo | null,
|
|
@@ -1092,25 +1161,29 @@ export class BatchArray {
|
|
|
1092
1161
|
setWritersCurrent: (writersCurrent: number) => void,
|
|
1093
1162
|
setMilestones: (milestones: Milestone[]) => void,
|
|
1094
1163
|
setConfigSyncResult: (result: string) => void,
|
|
1164
|
+
setSyncProgress: (progress: number) => void,
|
|
1095
1165
|
bClearLocalStorage: boolean,
|
|
1096
|
-
message: string
|
|
1166
|
+
message: string,
|
|
1167
|
+
instance?: IPublicClientApplication,
|
|
1168
|
+
authorizedUser?: User
|
|
1097
1169
|
): void {
|
|
1098
1170
|
bClearLocalStorage = bClearLocalStorage;
|
|
1171
|
+
message = message;
|
|
1172
|
+
|
|
1173
|
+
|
|
1099
1174
|
// we have just completed a successful POST to startSync
|
|
1100
1175
|
this.milestoneArray.post(setMilestones);
|
|
1101
1176
|
setConfigSyncResult("started sync, waiting for updates...");
|
|
1102
1177
|
// re-initialize batch array with Configuration updated by the succcessful POST to startSync
|
|
1103
1178
|
this.init(config, syncPortalGlobalState, false);
|
|
1104
|
-
// define
|
|
1105
|
-
let handler = (
|
|
1106
|
-
console.log(message);
|
|
1107
|
-
let item = JSON.parse(message);
|
|
1179
|
+
// define a stats hydration handler (used by polling)
|
|
1180
|
+
let handler = (batchId: string, statsarray: any) => {
|
|
1108
1181
|
// reset the countdown timer every time we get a message
|
|
1109
1182
|
this.pb_idle = 0;
|
|
1110
|
-
// find the associated tenant for this
|
|
1111
|
-
let matchingPair: any | undefined = batchIdArray.find((o: any) => o.BatchId ==
|
|
1183
|
+
// find the associated tenant for this batchId
|
|
1184
|
+
let matchingPair: any | undefined = batchIdArray.find((o: any) => o.BatchId == batchId);
|
|
1112
1185
|
if (matchingPair == null) {
|
|
1113
|
-
console.log(`Batch ${
|
|
1186
|
+
console.log(`Batch ${batchId} not found in batchIdArray.`);
|
|
1114
1187
|
debugger;
|
|
1115
1188
|
return;
|
|
1116
1189
|
}
|
|
@@ -1121,8 +1194,7 @@ export class BatchArray {
|
|
|
1121
1194
|
return;
|
|
1122
1195
|
}
|
|
1123
1196
|
tenantNode.batchId = matchingPair.BatchId;
|
|
1124
|
-
// process stats for this
|
|
1125
|
-
let statsarray = item.Stats; // get the array of statistics
|
|
1197
|
+
// process stats for this batch (one batch per tenant node)
|
|
1126
1198
|
let statskeys = Object.keys(statsarray); // get the keys of the array
|
|
1127
1199
|
let statsvalues = Object.values(statsarray); // get the values of the array
|
|
1128
1200
|
// does this tenantnode/batch have nothing to sync?
|
|
@@ -1180,9 +1252,11 @@ export class BatchArray {
|
|
|
1180
1252
|
}
|
|
1181
1253
|
}
|
|
1182
1254
|
tenantNode.nothingtosync = bTotalCountZero && bCurrentCountZero;
|
|
1255
|
+
|
|
1256
|
+
|
|
1183
1257
|
if (statskeys[j].startsWith("Writer")) {
|
|
1184
1258
|
// parse tid from Writer key
|
|
1185
|
-
let tidRegexp = /
|
|
1259
|
+
let tidRegexp = /Writer\/TID:(.+)\/TotalCount/;
|
|
1186
1260
|
if (bCurrentCount) tidRegexp = /Writer\/TID:(.+)\/CurrentCount/;
|
|
1187
1261
|
if (bExcludedCount) tidRegexp = /Writer\/TID:(.+)\/ExtCount/;
|
|
1188
1262
|
if (bDeferredCount) tidRegexp = /Writer\/TID:(.+)\/DeferredCount/;
|
|
@@ -1204,7 +1278,7 @@ export class BatchArray {
|
|
|
1204
1278
|
writerNode.total = Math.max(Number(tenantNode.total), writerNode.total);
|
|
1205
1279
|
writerNode.batchId = matchingPair.BatchId;
|
|
1206
1280
|
if (bTotalCount) {
|
|
1207
|
-
writerNode.total = Math.max(Number(
|
|
1281
|
+
writerNode.total = Math.max(Number(statsvalues[j]), writerNode.total);
|
|
1208
1282
|
console.log(`----- ${writerNode.name} TID: ${writerNode.tid} batchId: ${writerNode.batchId}`);
|
|
1209
1283
|
console.log(`----- ${writerNode.name} Total To Write: ${writerNode.total}`);
|
|
1210
1284
|
}
|
|
@@ -1238,6 +1312,7 @@ export class BatchArray {
|
|
|
1238
1312
|
let writerTotal: number = 0;
|
|
1239
1313
|
let writerCurrent: number = 0;
|
|
1240
1314
|
let writerExcluded: number = 0;
|
|
1315
|
+
let writerDeferred: number = 0;
|
|
1241
1316
|
this.tenantNodes.map((sourceTenantNode: TenantNode) => {
|
|
1242
1317
|
sourceTenantNode.targets.map((writerNode: TenantNode) => {
|
|
1243
1318
|
bWritingComplete &&= (writerNode.status == "complete" || writerNode.status == "failed");
|
|
@@ -1245,13 +1320,34 @@ export class BatchArray {
|
|
|
1245
1320
|
writerTotal += Math.max(writerNode.total, sourceTenantNode.total);
|
|
1246
1321
|
writerCurrent += writerNode.written;
|
|
1247
1322
|
writerExcluded += writerNode.excluded;
|
|
1323
|
+
writerDeferred += writerNode.deferred;
|
|
1248
1324
|
});
|
|
1249
1325
|
bNothingToSync &&= sourceTenantNode.nothingtosync;
|
|
1250
|
-
|
|
1326
|
+
// Reading completion should be derived from read/excluded totals, not from source status.
|
|
1327
|
+
bReadingComplete &&= (sourceTenantNode.total > 0 && (sourceTenantNode.read + sourceTenantNode.excluded) >= sourceTenantNode.total);
|
|
1251
1328
|
readerTotal += sourceTenantNode.total;
|
|
1252
1329
|
readerCurrent += sourceTenantNode.read;
|
|
1253
1330
|
readerExcluded += sourceTenantNode.excluded;
|
|
1254
1331
|
});
|
|
1332
|
+
|
|
1333
|
+
// New completion condition: sync is complete when:
|
|
1334
|
+
// (Reader/TotalCount - Reader/ExtCount) === (Writer/CurrentCount + Writer/DeferredCount)
|
|
1335
|
+
// This means all users that should be written (total minus excluded) have been processed.
|
|
1336
|
+
const usersToSync = readerTotal - readerExcluded;
|
|
1337
|
+
const usersProcessed = writerCurrent + writerDeferred;
|
|
1338
|
+
const isSyncCompleted = usersToSync > 0 && usersToSync === usersProcessed;
|
|
1339
|
+
|
|
1340
|
+
if (isSyncCompleted) {
|
|
1341
|
+
bWritingComplete = true;
|
|
1342
|
+
|
|
1343
|
+
// Mark all nodes as complete since sync condition is satisfied.
|
|
1344
|
+
this.tenantNodes.forEach((src) => {
|
|
1345
|
+
src.targets.forEach((t) => {
|
|
1346
|
+
if (t.status !== "failed") t.status = "complete";
|
|
1347
|
+
});
|
|
1348
|
+
if (src.status !== "failed") src.status = "complete";
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1255
1351
|
// set linear gauge max and current values
|
|
1256
1352
|
setReadersTotal(readerTotal);
|
|
1257
1353
|
setReadersCurrent(readerCurrent);
|
|
@@ -1259,11 +1355,33 @@ export class BatchArray {
|
|
|
1259
1355
|
setWritersTotal(Math.max(writerTotal, readerTotal));
|
|
1260
1356
|
setWritersCurrent(writerCurrent);
|
|
1261
1357
|
setWritersExcluded(writerExcluded);
|
|
1358
|
+
|
|
1359
|
+
// update progress bar based on backend totals (monotonic)
|
|
1360
|
+
// This avoids progress decreasing after refresh when local timer state restarts.
|
|
1361
|
+
// Excluded users count as completed for both read and write.
|
|
1362
|
+
// Deferred users count as completed for write.
|
|
1363
|
+
this.updateProgressFromTotals(
|
|
1364
|
+
Math.max(readerTotal, writerTotal),
|
|
1365
|
+
readerCurrent,
|
|
1366
|
+
writerCurrent,
|
|
1367
|
+
Math.max(readerExcluded, writerExcluded),
|
|
1368
|
+
// Deferred users count as completed for write.
|
|
1369
|
+
writerDeferred,
|
|
1370
|
+
setSyncProgress
|
|
1371
|
+
);
|
|
1262
1372
|
// check to see if there was nothing to sync
|
|
1263
1373
|
if (bNothingToSync) {
|
|
1264
1374
|
this.milestoneArray.write(setMilestones);
|
|
1265
|
-
|
|
1375
|
+
this.stopPolling();
|
|
1266
1376
|
setConfigSyncResult("nothing to sync");
|
|
1377
|
+
// force completion visuals
|
|
1378
|
+
if (this.pb_timer) {
|
|
1379
|
+
clearInterval(this.pb_timer);
|
|
1380
|
+
this.pb_timer = null;
|
|
1381
|
+
}
|
|
1382
|
+
this.pb_progress = 100;
|
|
1383
|
+
setSyncProgress(this.pb_progress);
|
|
1384
|
+
this.setIdleText?.(`Complete (nothing to sync).`);
|
|
1267
1385
|
this.clearStoredBatchIds();
|
|
1268
1386
|
console.log(`Setting config sync result: "nothing to sync"`);
|
|
1269
1387
|
}
|
|
@@ -1275,19 +1393,21 @@ export class BatchArray {
|
|
|
1275
1393
|
console.log(`Setting config sync result: "reading complete"`);
|
|
1276
1394
|
// trigger refresh delta tokens
|
|
1277
1395
|
setRefreshDeltaTrigger(config!.workspaceId);
|
|
1278
|
-
// change to % per second to complete in 12x as long as it took to get here
|
|
1279
|
-
let readTS = Date.now();
|
|
1280
|
-
let secsElapsed = (readTS - this.pb_startTS) / 1000;
|
|
1281
|
-
let expectedPercentDone = 8.5;
|
|
1282
|
-
let expectedPercentPerSecond = secsElapsed / expectedPercentDone;
|
|
1283
|
-
this.pb_increment = expectedPercentPerSecond;
|
|
1284
|
-
console.log(`Setting increment: ${this.pb_increment}% per second`);
|
|
1285
1396
|
}
|
|
1286
1397
|
// with that out of the way, is writing complete?
|
|
1287
|
-
|
|
1398
|
+
// Completion is determined by the new formula: usersToSync === usersProcessed
|
|
1399
|
+
if (bWritingComplete && isSyncCompleted) {
|
|
1288
1400
|
this.milestoneArray.write(setMilestones);
|
|
1289
|
-
|
|
1401
|
+
this.stopPolling();
|
|
1290
1402
|
setConfigSyncResult("sync complete");
|
|
1403
|
+
// force completion visuals
|
|
1404
|
+
if (this.pb_timer) {
|
|
1405
|
+
clearInterval(this.pb_timer);
|
|
1406
|
+
this.pb_timer = null;
|
|
1407
|
+
}
|
|
1408
|
+
this.pb_progress = 100;
|
|
1409
|
+
setSyncProgress(this.pb_progress);
|
|
1410
|
+
this.setIdleText?.(`Complete.`);
|
|
1291
1411
|
this.clearStoredBatchIds();
|
|
1292
1412
|
console.log(`Setting config sync result: "complete"`);
|
|
1293
1413
|
}
|
|
@@ -1304,38 +1424,26 @@ export class BatchArray {
|
|
|
1304
1424
|
}
|
|
1305
1425
|
}
|
|
1306
1426
|
|
|
1307
|
-
//
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
connection.onreconnected(connectionId => {
|
|
1328
|
-
console.assert(connection.state === signalR.HubConnectionState.Connected);
|
|
1329
|
-
console.log(`Connection reestablished. Connected with connectionId "${connectionId}".`);
|
|
1330
|
-
});
|
|
1331
|
-
// restart when you get a close event
|
|
1332
|
-
connection.onclose(async () => {
|
|
1333
|
-
console.log(`Connection closing. Attempting restart.`);
|
|
1334
|
-
await connection.start();
|
|
1335
|
-
});
|
|
1336
|
-
// start and display any caught exceptions in the console
|
|
1337
|
-
connection.start().catch(console.error);
|
|
1338
|
-
});
|
|
1427
|
+
// allow polling to feed the same handler
|
|
1428
|
+
this.statsHydrationHandler = handler;
|
|
1429
|
+
|
|
1430
|
+
// Start polling (only if caller provided credentials)
|
|
1431
|
+
if (instance && authorizedUser) {
|
|
1432
|
+
this.pollInstance = instance;
|
|
1433
|
+
this.pollAuthorizedUser = authorizedUser;
|
|
1434
|
+
this.pollBatchIdArray = batchIdArray as any[];
|
|
1435
|
+
this.stopPolling(); // clear any previous poller state
|
|
1436
|
+
this.pollInstance = instance;
|
|
1437
|
+
this.pollAuthorizedUser = authorizedUser;
|
|
1438
|
+
this.pollBatchIdArray = batchIdArray as any[];
|
|
1439
|
+
|
|
1440
|
+
// Hydrate immediately (no need to wait pollIntervalSeconds)
|
|
1441
|
+
void this.pollStatsOnce();
|
|
1442
|
+
|
|
1443
|
+
this.pollTimer = setInterval(() => {
|
|
1444
|
+
void this.pollStatsOnce();
|
|
1445
|
+
}, this.pollIntervalSeconds * 1000);
|
|
1446
|
+
}
|
|
1339
1447
|
}
|
|
1340
1448
|
// start a sync cycle
|
|
1341
1449
|
async startSync(instance: IPublicClientApplication, authorizedUser: User | null | undefined, config: SyncConfig | null | undefined): Promise<APIResult> {
|
|
@@ -1383,8 +1491,20 @@ export class TenantNode {
|
|
|
1383
1491
|
this.deferred = deferred;
|
|
1384
1492
|
if (this.read === 0 && this.written === 0) this.status = "not started";
|
|
1385
1493
|
if (this.read > 0) {
|
|
1386
|
-
if (this.read + this.excluded < this.total)
|
|
1387
|
-
|
|
1494
|
+
if (this.read + this.excluded < this.total) {
|
|
1495
|
+
this.status = "in progress";
|
|
1496
|
+
}
|
|
1497
|
+
else if (this.read + this.excluded === this.total) {
|
|
1498
|
+
// For source nodes (nodes with targets), reading complete doesn't mean the whole branch is complete.
|
|
1499
|
+
// Avoid reporting "complete" while any target is still running.
|
|
1500
|
+
if (this.targets != null && this.targets.length > 0) {
|
|
1501
|
+
const allTargetsTerminal = this.targets.every(t => t.status === "complete" || t.status === "failed");
|
|
1502
|
+
this.status = allTargetsTerminal ? "complete" : "in progress";
|
|
1503
|
+
}
|
|
1504
|
+
else {
|
|
1505
|
+
this.status = "complete";
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1388
1508
|
}
|
|
1389
1509
|
else if (this.written > 0) {
|
|
1390
1510
|
if (this.written + this.deferred + this.excluded < this.total) this.status = "in progress";
|